<?xml version="1.0" encoding="utf-8"?>
<feed xml:lang="en-us" xmlns="http://www.w3.org/2005/Atom"><title>Simon Willison's Weblog: heroku</title><link href="http://simonwillison.net/" rel="alternate"/><link href="http://simonwillison.net/tags/heroku.atom" rel="self"/><id>http://simonwillison.net/</id><updated>2026-02-06T18:44:21+00:00</updated><author><name>Simon Willison</name></author><entry><title>An Update on Heroku</title><link href="https://simonwillison.net/2026/Feb/6/an-update-on-heroku/#atom-tag" rel="alternate"/><published>2026-02-06T18:44:21+00:00</published><updated>2026-02-06T18:44:21+00:00</updated><id>https://simonwillison.net/2026/Feb/6/an-update-on-heroku/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://www.heroku.com/blog/an-update-on-heroku/"&gt;An Update on Heroku&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
An ominous headline to see on the official Heroku blog and yes, it's bad news.&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Today, Heroku is transitioning to a sustaining engineering model focused on stability, security, reliability, and support. Heroku remains an actively supported, production-ready platform, with an emphasis on maintaining quality and operational excellence rather than introducing new features. We know changes like this can raise questions, and we want to be clear about what this means for customers.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;Based on context I'm guessing a "sustaining engineering model" (this definitely isn't a widely used industry term) means that they'll keep the lights on and that's it.&lt;/p&gt;
&lt;p&gt;This is a very frustrating piece of corporate communication. "We want to be clear about what this means for customers" - then proceeds to &lt;em&gt;not be clear&lt;/em&gt; about what this means for customers.&lt;/p&gt;
&lt;p&gt;Why are they doing this? Here's their explanation:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;We’re focusing our product and engineering investments on areas where we can deliver the greatest long-term customer value, including helping organizations build and deploy enterprise-grade AI in a secure and trusted way.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;My blog is the only project I have left running on Heroku. I guess I'd better migrate it away (probably to Fly) before Salesforce lose interest completely.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/salesforce"&gt;salesforce&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;&lt;/p&gt;



</summary><category term="salesforce"/><category term="heroku"/><category term="fly"/></entry><entry><title>How I automate my Substack newsletter with content from my blog</title><link href="https://simonwillison.net/2025/Nov/19/how-i-automate-my-substack-newsletter/#atom-tag" rel="alternate"/><published>2025-11-19T22:00:34+00:00</published><updated>2025-11-19T22:00:34+00:00</updated><id>https://simonwillison.net/2025/Nov/19/how-i-automate-my-substack-newsletter/#atom-tag</id><summary type="html">
    &lt;p&gt;I sent out &lt;a href="https://simonw.substack.com/p/trying-out-gemini-3-pro-with-audio"&gt;my weekly-ish Substack newsletter&lt;/a&gt; this morning and took the opportunity to record &lt;a href="https://www.youtube.com/watch?v=BoPZltKDM-s"&gt;a YouTube video&lt;/a&gt; demonstrating my process and describing the different components that make it work. There's a &lt;em&gt;lot&lt;/em&gt; of digital duct tape involved, taking the content from Django+Heroku+PostgreSQL to GitHub Actions to SQLite+Datasette+Fly.io to JavaScript+Observable and finally to Substack.&lt;/p&gt;

&lt;p&gt;&lt;lite-youtube videoid="BoPZltKDM-s" js-api="js-api"
  title="How I automate my Substack newsletter with content from my blog"
  playlabel="Play: How I automate my Substack newsletter with content from my blog"
&gt; &lt;/lite-youtube&gt;&lt;/p&gt;

&lt;p&gt;The core process is the same as I described &lt;a href="https://simonwillison.net/2023/Apr/4/substack-observable/"&gt;back in 2023&lt;/a&gt;. I have an Observable notebook called &lt;a href="https://observablehq.com/@simonw/blog-to-newsletter"&gt;blog-to-newsletter&lt;/a&gt; which fetches content from my blog's database, filters out anything that has been in the newsletter before, formats what's left as HTML and offers a big "Copy rich text newsletter to clipboard" button.&lt;/p&gt;
&lt;p&gt;&lt;img src="https://static.simonwillison.net/static/2025/copy-to-newsletter.jpg" alt="Screenshot of the interface. An item in a list says 9080: Trying out Gemini 3 Pro with audio transcription and a new pelican benchmark. A huge button reads Copy rich text newsletter to clipboard - below is a smaller button that says Copy just the links/quotes/TILs. A Last X days slider is set to 2. There are checkboxes for SKip content sent in prior newsletters and only include post content prior to the cutoff comment." style="max-width: 100%;" /&gt;&lt;/p&gt;
&lt;p&gt;I click that button, paste the result into the Substack editor, tweak a few things and hit send. The whole process usually takes just a few minutes.&lt;/p&gt;
&lt;p&gt;I make very minor edits:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;I set the title and the subheading for the newsletter. This is often a direct copy of the title of the featured blog post.&lt;/li&gt;
&lt;li&gt;Substack turns YouTube URLs into embeds, which often isn't what I want - especially if I have a YouTube URL inside a code example.&lt;/li&gt;
&lt;li&gt;Blocks of preformatted text often have an extra blank line at the end, which I remove.&lt;/li&gt;
&lt;li&gt;Occasionally I'll make a content edit - removing a piece of content that doesn't fit the newsletter, or fixing a time reference like "yesterday" that doesn't make sense any more.&lt;/li&gt;
&lt;li&gt;I pick the featured image for the newsletter and add some tags.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That's the whole process!&lt;/p&gt;
&lt;h4 id="the-observable-notebook"&gt;The Observable notebook&lt;/h4&gt;
&lt;p&gt;The most important cell in the Observable notebook is this one:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-s1"&gt;raw_content&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
  &lt;span class="pl-c1"&gt;return&lt;/span&gt; &lt;span class="pl-s1"&gt;await&lt;/span&gt; &lt;span class="pl-kos"&gt;(&lt;/span&gt;
    &lt;span class="pl-k"&gt;await&lt;/span&gt; &lt;span class="pl-en"&gt;fetch&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
      &lt;span class="pl-s"&gt;`https://datasette.simonwillison.net/simonwillisonblog.json?sql=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-en"&gt;encodeURIComponent&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;        &lt;span class="pl-s1"&gt;sql&lt;/span&gt;&lt;/span&gt;&lt;/span&gt;
&lt;span class="pl-s"&gt;&lt;span class="pl-s1"&gt;      &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;&amp;amp;_shape=array&amp;amp;numdays=&lt;span class="pl-s1"&gt;&lt;span class="pl-kos"&gt;${&lt;/span&gt;&lt;span class="pl-s1"&gt;numDays&lt;/span&gt;&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/span&gt;`&lt;/span&gt;
    &lt;span class="pl-kos"&gt;)&lt;/span&gt;
  &lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;json&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
&lt;span class="pl-kos"&gt;}&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This uses the JavaScript &lt;code&gt;fetch()&lt;/code&gt; function to pull data from my blog's Datasette instance, using a very complex SQL query that is composed elsewhere in the notebook.&lt;/p&gt;
&lt;p&gt;Here's a link to &lt;a href="https://datasette.simonwillison.net/simonwillisonblog?sql=with+content+as+%28%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27entry%27+as+type%2C%0D%0A++++title%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27%3Ch3%3E%3Ca+href%3D%22%27+%7C%7C+%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%0D%0A++++++%7C%7C+%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27+%7C%7C+%27%22%3E%27+%0D%0A++++++%7C%7C+title+%7C%7C+%27%3C%2Fa%3E+-+%27+%7C%7C+date%28created%29+%7C%7C+%27%3C%2Fh3%3E%27+%7C%7C+body%0D%0A++++++as+html%2C%0D%0A++++%27null%27+as+json%2C%0D%0A++++%27%27+as+external_url%0D%0A++from+blog_entry%0D%0A++union+all%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27blogmark%27+as+type%2C%0D%0A++++link_title%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27%3Cp%3E%3Cstrong%3ELink%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C+%27+%3Ca+href%3D%22%27%7C%7C+link_url+%7C%7C+%27%22%3E%27%0D%0A++++++%7C%7C+link_title+%7C%7C+%27%3C%2Fa%3E%3A%3C%2Fp%3E%3Cp%3E%27+%7C%7C+%27+%27+%7C%7C+replace%28commentary%2C+%27%0D%0A%27%2C+%27%3Cbr%3E%27%29+%7C%7C+%27%3C%2Fp%3E%27%0D%0A++++++as+html%2C%0D%0A++++json_object%28%0D%0A++++++%27created%27%2C+date%28created%29%2C%0D%0A++++++%27link_url%27%2C+link_url%2C%0D%0A++++++%27link_title%27%2C+link_title%2C%0D%0A++++++%27commentary%27%2C+commentary%2C%0D%0A++++++%27use_markdown%27%2C+use_markdown%0D%0A++++%29+as+json%2C%0D%0A++link_url+as+external_url%0D%0A++from+blog_blogmark%0D%0A++union+all%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27quotation%27+as+type%2C%0D%0A++++source%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27%3Cstrong%3Equote%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C%0D%0A++++%27%3Cblockquote%3E%3Cp%3E%3Cem%3E%27+%7C%7C%0D%0A++++replace%28quotation%2C+%27%0D%0A%27%2C+%27%3Cbr%3E%27%29+%7C%7C+%0D%0A++++%27%3C%2Fem%3E%3C%2Fp%3E%3C%2Fblockquote%3E%3Cp%3E%3Ca+href%3D%22%27+%7C%7C%0D%0A++++coalesce%28source_url%2C+%27%23%27%29+%7C%7C+%27%22%3E%27+%7C%7C+source+%7C%7C+%27%3C%2Fa%3E%27+%7C%7C%0D%0A++++case+%0D%0A++++++++when+nullif%28trim%28context%29%2C+%27%27%29+is+not+null+%0D%0A++++++++then+%27%2C+%27+%7C%7C+context+%0D%0A++++++++else+%27%27+%0D%0A++++end+%7C%7C%0D%0A++++%27%3C%2Fp%3E%27+as+html%2C%0D%0A++++%27null%27+as+json%2C%0D%0A++++source_url+as+external_url%0D%0A++from+blog_quotation%0D%0A++union+all%0D%0A++select%0D%0A++++id%2C%0D%0A++++%27note%27+as+type%2C%0D%0A++++case%0D%0A++++++when+title+is+not+null+and+title+%3C%3E+%27%27+then+title%0D%0A++++++else+%27Note+on+%27+%7C%7C+date%28created%29%0D%0A++++end%2C%0D%0A++++created%2C%0D%0A++++slug%2C%0D%0A++++%27No+HTML%27%2C%0D%0A++++json_object%28%0D%0A++++++%27created%27%2C+date%28created%29%2C%0D%0A++++++%27link_url%27%2C+%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%0D%0A++++++%7C%7C+%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27%2C%0D%0A++++++%27link_title%27%2C+%27%27%2C%0D%0A++++++%27commentary%27%2C+body%2C%0D%0A++++++%27use_markdown%27%2C+1%0D%0A++++%29%2C%0D%0A++++%27%27+as+external_url%0D%0A++from+blog_note%0D%0A++union+all%0D%0A++select%0D%0A++++rowid%2C%0D%0A++++%27til%27+as+type%2C%0D%0A++++title%2C%0D%0A++++created%2C%0D%0A++++%27null%27+as+slug%2C%0D%0A++++%27%3Cp%3E%3Cstrong%3ETIL%3C%2Fstrong%3E+%27+%7C%7C+date%28created%29+%7C%7C+%27+%3Ca+href%3D%22%27%7C%7C+%27https%3A%2F%2Ftil.simonwillison.net%2F%27+%7C%7C+topic+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%22%3E%27+%7C%7C+title+%7C%7C+%27%3C%2Fa%3E%3A%27+%7C%7C+%27+%27+%7C%7C+substr%28html%2C+1%2C+instr%28html%2C+%27%3C%2Fp%3E%27%29+-+1%29+%7C%7C+%27+%26%238230%3B%3C%2Fp%3E%27+as+html%2C%0D%0A++++%27null%27+as+json%2C%0D%0A++++%27https%3A%2F%2Ftil.simonwillison.net%2F%27+%7C%7C+topic+%7C%7C+%27%2F%27+%7C%7C+slug+as+external_url%0D%0A++from+til%0D%0A%29%2C%0D%0Acollected+as+%28%0D%0A++select%0D%0A++++id%2C%0D%0A++++type%2C%0D%0A++++title%2C%0D%0A++++case%0D%0A++++++when+type+%3D+%27til%27%0D%0A++++++then+external_url%0D%0A++++++else+%27https%3A%2F%2Fsimonwillison.net%2F%27+%7C%7C+strftime%28%27%25Y%2F%27%2C+created%29%0D%0A++++++%7C%7C+substr%28%27JanFebMarAprMayJunJulAugSepOctNovDec%27%2C+%28strftime%28%27%25m%27%2C+created%29+-+1%29+*+3+%2B+1%2C+3%29+%7C%7C+%0D%0A++++++%27%2F%27+%7C%7C+cast%28strftime%28%27%25d%27%2C+created%29+as+integer%29+%7C%7C+%27%2F%27+%7C%7C+slug+%7C%7C+%27%2F%27%0D%0A++++++end+as+url%2C%0D%0A++++created%2C%0D%0A++++html%2C%0D%0A++++json%2C%0D%0A++++external_url%2C%0D%0A++++case%0D%0A++++++when+type+%3D+%27entry%27+then+%28%0D%0A++++++++select+json_group_array%28tag%29%0D%0A++++++++from+blog_tag%0D%0A++++++++join+blog_entry_tags+on+blog_tag.id+%3D+blog_entry_tags.tag_id%0D%0A++++++++where+blog_entry_tags.entry_id+%3D+content.id%0D%0A++++++%29%0D%0A++++++when+type+%3D+%27blogmark%27+then+%28%0D%0A++++++++select+json_group_array%28tag%29%0D%0A++++++++from+blog_tag%0D%0A++++++++join+blog_blogmark_tags+on+blog_tag.id+%3D+blog_blogmark_tags.tag_id%0D%0A++++++++where+blog_blogmark_tags.blogmark_id+%3D+content.id%0D%0A++++++%29%0D%0A++++++when+type+%3D+%27quotation%27+then+%28%0D%0A++++++++select+json_group_array%28tag%29%0D%0A++++++++from+blog_tag%0D%0A++++++++join+blog_quotation_tags+on+blog_tag.id+%3D+blog_quotation_tags.tag_id%0D%0A++++++++where+blog_quotation_tags.quotation_id+%3D+content.id%0D%0A++++++%29%0D%0A++++++else+%27%5B%5D%27%0D%0A++++end+as+tags%0D%0A++from+content%0D%0A++where+created+%3E%3D+date%28%27now%27%2C+%27-%27+%7C%7C+%3Anumdays+%7C%7C+%27+days%27%29+++%0D%0A++order+by+created+desc%0D%0A%29%0D%0Aselect+id%2C+type%2C+title%2C+url%2C+created%2C+html%2C+json%2C+external_url%2C+tags%0D%0Afrom+collected+%0D%0Aorder+by+%0D%0A++case+type+%0D%0A++++when+%27entry%27+then+0+%0D%0A++++else+1+%0D%0A++end%2C%0D%0A++case+type+%0D%0A++++when+%27entry%27+then+created+%0D%0A++++else+-strftime%28%27%25s%27%2C+created%29+%0D%0A++end+desc%3B&amp;amp;numdays=7"&gt;see and execute that query&lt;/a&gt; directly in Datasette. It's 143 lines of convoluted SQL that assembles most of the HTML for the newsletter using SQLite string concatenation! An illustrative snippet:&lt;/p&gt;
&lt;div class="highlight highlight-source-sql"&gt;&lt;pre&gt;with content &lt;span class="pl-k"&gt;as&lt;/span&gt; (
  &lt;span class="pl-k"&gt;select&lt;/span&gt;
    id,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;entry&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; type,
    title,
    created,
    slug,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;h3&amp;gt;&amp;lt;a href="&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;https://simonwillison.net/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%Y/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created)
      &lt;span class="pl-k"&gt;||&lt;/span&gt; substr(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;JanFebMarAprMayJunJulAugSepOctNovDec&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, (strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%m&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;-&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;) &lt;span class="pl-k"&gt;*&lt;/span&gt; &lt;span class="pl-c1"&gt;3&lt;/span&gt; &lt;span class="pl-k"&gt;+&lt;/span&gt; &lt;span class="pl-c1"&gt;1&lt;/span&gt;, &lt;span class="pl-c1"&gt;3&lt;/span&gt;) 
      &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; cast(strftime(&lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;%d&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt;, created) &lt;span class="pl-k"&gt;as&lt;/span&gt; &lt;span class="pl-k"&gt;integer&lt;/span&gt;) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; slug &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;/&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;"&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; 
      &lt;span class="pl-k"&gt;||&lt;/span&gt; title &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/a&amp;gt; - &lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-k"&gt;date&lt;/span&gt;(created) &lt;span class="pl-k"&gt;||&lt;/span&gt; &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&amp;lt;/h3&amp;gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;||&lt;/span&gt; body
      &lt;span class="pl-k"&gt;as&lt;/span&gt; html,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;null&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; json,
    &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;span class="pl-pds"&gt;'&lt;/span&gt;&lt;/span&gt; &lt;span class="pl-k"&gt;as&lt;/span&gt; external_url
  &lt;span class="pl-k"&gt;from&lt;/span&gt; blog_entry
  &lt;span class="pl-k"&gt;union all&lt;/span&gt;
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; ...&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;My blog's URLs look like &lt;code&gt;/2025/Nov/18/gemini-3/&lt;/code&gt; - this SQL constructs that three letter month abbreviation from the month number using a substring operation.&lt;/p&gt;
&lt;p&gt;This is a &lt;em&gt;terrible&lt;/em&gt; way to assemble HTML, but I've stuck with it because it amuses me.&lt;/p&gt;
&lt;p&gt;The rest of the Observable notebook takes that data, filters out anything that links to content mentioned in the previous newsletters and composes it into a block of HTML that can be copied using that big button.&lt;/p&gt;
&lt;p&gt;Here's the recipe it uses to turn HTML into rich text content on a clipboard suitable for Substack. I can't remember how I figured this out but it's very effective:&lt;/p&gt;
&lt;div class="highlight highlight-source-js"&gt;&lt;pre&gt;&lt;span class="pl-v"&gt;Object&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;assign&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;
  &lt;span class="pl-en"&gt;html&lt;/span&gt;&lt;span class="pl-s"&gt;`&lt;span class="pl-kos"&gt;&amp;lt;&lt;/span&gt;&lt;span class="pl-ent"&gt;button&lt;/span&gt; &lt;span class="pl-c1"&gt;style&lt;/span&gt;="&lt;span class="pl-s"&gt;font-size: 1.4em; padding: 0.3em 1em; font-weight: bold;&lt;/span&gt;"&lt;span class="pl-kos"&gt;&amp;gt;&lt;/span&gt;Copy rich text newsletter to clipboard`&lt;/span&gt;&lt;span class="pl-kos"&gt;,&lt;/span&gt;
  &lt;span class="pl-kos"&gt;{&lt;/span&gt;
    &lt;span class="pl-en"&gt;onclick&lt;/span&gt;: &lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt; &lt;span class="pl-c1"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="pl-kos"&gt;{&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;htmlContent&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;newsletterHTML&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Create a temporary element to hold the HTML content&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;tempElement&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;createElement&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"div"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-s1"&gt;tempElement&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;innerHTML&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-s1"&gt;htmlContent&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;appendChild&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;tempElement&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Select the HTML content&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;range&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;createRange&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-s1"&gt;range&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;selectNode&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;tempElement&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-c"&gt;// Copy the selected HTML content to the clipboard&lt;/span&gt;
      &lt;span class="pl-k"&gt;const&lt;/span&gt; &lt;span class="pl-s1"&gt;selection&lt;/span&gt; &lt;span class="pl-c1"&gt;=&lt;/span&gt; &lt;span class="pl-smi"&gt;window&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;getSelection&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-s1"&gt;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeAllRanges&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-s1"&gt;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;addRange&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;range&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;execCommand&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s"&gt;"copy"&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-s1"&gt;selection&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeAllRanges&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
      &lt;span class="pl-smi"&gt;document&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-c1"&gt;body&lt;/span&gt;&lt;span class="pl-kos"&gt;.&lt;/span&gt;&lt;span class="pl-en"&gt;removeChild&lt;/span&gt;&lt;span class="pl-kos"&gt;(&lt;/span&gt;&lt;span class="pl-s1"&gt;tempElement&lt;/span&gt;&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;span class="pl-kos"&gt;;&lt;/span&gt;
    &lt;span class="pl-kos"&gt;}&lt;/span&gt;
  &lt;span class="pl-kos"&gt;}&lt;/span&gt;
&lt;span class="pl-kos"&gt;)&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h4 id="from-django-postgresql-to-datasette-sqlite"&gt;From Django+Postgresql to Datasette+SQLite&lt;/h4&gt;
&lt;p&gt;My blog itself is a Django application hosted on Heroku, with data stored in Heroku PostgreSQL. Here's &lt;a href="https://github.com/simonw/simonwillisonblog"&gt;the source code for that Django application&lt;/a&gt;. I use the Django admin as my CMS.&lt;/p&gt;
&lt;p&gt;&lt;a href="https://datasette.io/"&gt;Datasette&lt;/a&gt; provides a JSON API over a SQLite database... which means something needs to convert that PostgreSQL database into a SQLite database that Datasette can use.&lt;/p&gt;
&lt;p&gt;My system for doing that lives in the &lt;a href="https://github.com/simonw/simonwillisonblog-backup"&gt;simonw/simonwillisonblog-backup&lt;/a&gt; GitHub repository. It uses GitHub Actions on a schedule that executes every two hours, fetching the latest data from PostgreSQL and converting that to SQLite.&lt;/p&gt;
&lt;p&gt;My &lt;a href="https://github.com/simonw/db-to-sqlite"&gt;db-to-sqlite&lt;/a&gt; tool is responsible for that conversion. I call it &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/dc5b9df272134ce051a5280b4de6d4daa9b2a9fc/.github/workflows/backup.yml#L44-L62"&gt;like this&lt;/a&gt;:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;db-to-sqlite \
  &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;$(&lt;/span&gt;heroku config:get DATABASE_URL -a simonwillisonblog &lt;span class="pl-k"&gt;|&lt;/span&gt; sed s/postgres:/postgresql+psycopg2:/&lt;span class="pl-pds"&gt;)&lt;/span&gt;&lt;/span&gt; \
  simonwillisonblog.db \
  --table auth_permission \
  --table auth_user \
  --table blog_blogmark \
  --table blog_blogmark_tags \
  --table blog_entry \
  --table blog_entry_tags \
  --table blog_quotation \
  --table blog_quotation_tags \
  --table blog_note \
  --table blog_note_tags \
  --table blog_tag \
  --table blog_previoustagname \
  --table blog_series \
  --table django_content_type \
  --table redirects_redirect&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;That &lt;code&gt;heroku config:get DATABASE_URL&lt;/code&gt; command uses Heroku credentials in an environment variable to fetch the database connection URL for my blog's PostgreSQL database (and fixes a small difference in the URL scheme).&lt;/p&gt;
&lt;p&gt;&lt;code&gt;db-to-sqlite&lt;/code&gt; can then export that data and write it to a SQLite database file called &lt;code&gt;simonwillisonblog.db&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;--table&lt;/code&gt; options specify the tables that should be included in the export.&lt;/p&gt;
&lt;p&gt;The repository does more than just that conversion: it also exports the resulting data to JSON files that live in the repository, which gives me a &lt;a href="https://github.com/simonw/simonwillisonblog-backup/commits/main/simonwillisonblog"&gt;commit history&lt;/a&gt; of changes I make to my content. This is a cheap way to get a revision history of my blog content without having to mess around with detailed history tracking inside the Django application itself.&lt;/p&gt;
&lt;p&gt;At the &lt;a href="https://github.com/simonw/simonwillisonblog-backup/blob/dc5b9df272134ce051a5280b4de6d4daa9b2a9fc/.github/workflows/backup.yml#L200-L204"&gt;end of my GitHub Actions workflow&lt;/a&gt; is this code that publishes the resulting database to Datasette running on &lt;a href="https://fly.io/"&gt;Fly.io&lt;/a&gt; using the &lt;a href="https://datasette.io/plugins/datasette-publish-fly"&gt;datasette publish fly&lt;/a&gt; plugin:&lt;/p&gt;
&lt;div class="highlight highlight-source-shell"&gt;&lt;pre&gt;datasette publish fly simonwillisonblog.db \
  -m metadata.yml \
  --app simonwillisonblog-backup \
  --branch 1.0a2 \
  --extra-options &lt;span class="pl-s"&gt;&lt;span class="pl-pds"&gt;"&lt;/span&gt;--setting sql_time_limit_ms 15000 --setting truncate_cells_html 10000 --setting allow_facet off&lt;span class="pl-pds"&gt;"&lt;/span&gt;&lt;/span&gt; \
  --install datasette-block-robots \
  &lt;span class="pl-c"&gt;&lt;span class="pl-c"&gt;#&lt;/span&gt; ... more plugins&lt;/span&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;As you can see, there are a lot of moving parts! Surprisingly it all mostly just works - I rarely have to intervene in the process, and the cost of those different components is pleasantly low.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/blogging"&gt;blogging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sql"&gt;sql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/youtube"&gt;youtube&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/observable"&gt;observable&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github-actions"&gt;github-actions&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/newsletter"&gt;newsletter&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/substack"&gt;substack&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/site-upgrades"&gt;site-upgrades&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="blogging"/><category term="django"/><category term="javascript"/><category term="postgresql"/><category term="sql"/><category term="sqlite"/><category term="youtube"/><category term="heroku"/><category term="datasette"/><category term="observable"/><category term="github-actions"/><category term="fly"/><category term="newsletter"/><category term="substack"/><category term="site-upgrades"/></entry><entry><title>django-simple-deploy</title><link href="https://simonwillison.net/2025/May/17/django-simple-deploy/#atom-tag" rel="alternate"/><published>2025-05-17T12:49:52+00:00</published><updated>2025-05-17T12:49:52+00:00</updated><id>https://simonwillison.net/2025/May/17/django-simple-deploy/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://django-simple-deploy.readthedocs.io/"&gt;django-simple-deploy&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Eric Matthes presented a lightning talk about this project at PyCon US this morning. "Django has a deploy command now". You can run it like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;pip install django-simple-deploy[fly_io]
# Add django_simple_deploy to INSTALLED_APPS.
python manage.py deploy --automate-all
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;It's plugin-based (&lt;a href="https://github.com/django-simple-deploy/django-simple-deploy/issues/313"&gt;inspired by Datasette!&lt;/a&gt;) and the project has stable plugins for three hosting platforms: &lt;a href="https://github.com/django-simple-deploy/dsd-flyio"&gt;dsd-flyio&lt;/a&gt;, &lt;a href="https://github.com/django-simple-deploy/dsd-heroku"&gt;dsd-heroku&lt;/a&gt; and &lt;a href="https://github.com/django-simple-deploy/dsd-platformsh"&gt;dsd-platformsh&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Currently in development: &lt;a href="https://github.com/django-simple-deploy/dsd-vps"&gt;dsd-vps&lt;/a&gt; - a plugin that should work with any VPS provider, using &lt;a href="https://www.paramiko.org/"&gt;Paramiko&lt;/a&gt; to connect to a newly created instance and &lt;a href="https://github.com/django-simple-deploy/dsd-vps/blob/a372fc7b7fd31cd2ad3cf22d68b9c9fecb65d17a/dsd_vps/utils.py"&gt;run all of the commands&lt;/a&gt; needed to start serving a Django application.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/paramiko"&gt;paramiko&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/plugins"&gt;plugins&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/fly"&gt;fly&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="paramiko"/><category term="plugins"/><category term="python"/><category term="heroku"/><category term="datasette"/><category term="fly"/></entry><entry><title>Giving software away for free</title><link href="https://simonwillison.net/2025/Apr/28/give-it-away-for-free/#atom-tag" rel="alternate"/><published>2025-04-28T16:10:58+00:00</published><updated>2025-04-28T16:10:58+00:00</updated><id>https://simonwillison.net/2025/Apr/28/give-it-away-for-free/#atom-tag</id><summary type="html">
    &lt;p&gt;If you want to create completely &lt;strong&gt;free software&lt;/strong&gt; for other people to use, the absolute best delivery mechanism right now is static HTML and JavaScript served from a free web host with an established reputation.&lt;/p&gt;
&lt;p&gt;Thanks to WebAssembly the set of potential software that can be served in this way is vast and, I think, under appreciated. &lt;a href="https://pyodide.org/"&gt;Pyodide&lt;/a&gt; means we can ship client-side Python applications now!&lt;/p&gt;
&lt;p&gt;This assumes that you would like your gift to the world to keep working for as long as possible, while granting you the freedom to lose interest and move onto other projects without needing to keep covering expenses far into the future.&lt;/p&gt;
&lt;p&gt;Even the cheapest hosting plan requires you to monitor and update billing details every few years. Domains have to be renewed. Anything that runs server-side will inevitably need to be upgraded someday - and the longer you wait between upgrades the harder those become.&lt;/p&gt;
&lt;p&gt;My top choice for this kind of thing in 2025 is GitHub, using GitHub Pages. It's free for public repositories and I haven't seen GitHub break a working URL that they have hosted in the 17+ years since they first launched.&lt;/p&gt;
&lt;p&gt;A few years ago I'd have recommended Heroku on the basis that their free plan had stayed reliable for more than a decade, but Salesforce took that accumulated goodwill and &lt;a href="https://blog.heroku.com/next-chapter"&gt;incinerated it in 2022&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It almost goes without saying that you should release it under an open source license. The license alone is not enough to ensure regular human beings can make use of what you have built though: give people a link to something that works!&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/html"&gt;html&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/javascript"&gt;javascript&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/open-source"&gt;open-source&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-standards"&gt;web-standards&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webassembly"&gt;webassembly&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/pyodide"&gt;pyodide&lt;/a&gt;&lt;/p&gt;



</summary><category term="github"/><category term="html"/><category term="javascript"/><category term="open-source"/><category term="web-standards"/><category term="heroku"/><category term="webassembly"/><category term="pyodide"/></entry><entry><title>Notes on upgrading by blog's Heroku database plan</title><link href="https://simonwillison.net/2024/Jun/15/heroku-database-upgrade/#atom-tag" rel="alternate"/><published>2024-06-15T22:29:02+00:00</published><updated>2024-06-15T22:29:02+00:00</updated><id>https://simonwillison.net/2024/Jun/15/heroku-database-upgrade/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/simonwillisonblog/issues/439"&gt;Notes on upgrading by blog&amp;#x27;s Heroku database plan&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Heroku discontinued the “Basic” PostgreSQL plan I’ve been using for my blog, so I just upgraded to the new “essential-0” tier. Here are my notes as a GitHub issue—it was very straightforward, and I’m really only linking to it now to test that writes to the new database work correctly.&lt;/p&gt;

&lt;p&gt;I try to create an issue like this any time I do even a minor ops task, mainly so I have somewhere to drop screenshots of any web UI interactions for future reference.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;&lt;/p&gt;



</summary><category term="heroku"/></entry><entry><title>Quoting Richard Schneeman</title><link href="https://simonwillison.net/2024/May/16/richard-schneeman/#atom-tag" rel="alternate"/><published>2024-05-16T05:44:39+00:00</published><updated>2024-05-16T05:44:39+00:00</updated><id>https://simonwillison.net/2024/May/16/richard-schneeman/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://lobste.rs/s/g9e3c1/heroku_on_two_standard_dynos#c_jj38of"&gt;&lt;p&gt;[...] by default Heroku will spin up multiple dynos in different availability zones. It also has multiple routers in different zones so if one zone should go completely offline, having a second dyno will mean that your app can still serve traffic.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://lobste.rs/s/g9e3c1/heroku_on_two_standard_dynos#c_jj38of"&gt;Richard Schneeman&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/scaling"&gt;scaling&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;&lt;/p&gt;



</summary><category term="scaling"/><category term="heroku"/></entry><entry><title>Heroku: Core Impact</title><link href="https://simonwillison.net/2022/May/16/heroku-core-impact/#atom-tag" rel="alternate"/><published>2022-05-16T04:24:50+00:00</published><updated>2022-05-16T04:24:50+00:00</updated><id>https://simonwillison.net/2022/May/16/heroku-core-impact/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://brandur.org/nanoglyphs/033-heroku"&gt;Heroku: Core Impact&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ex-Heroku engineer Brandur Leach pulls together some of the background information circulating concerning the now more than a month long Heroku security incident and provides some ex-insider commentary on what went right and what went wrong with a platform that left a huge, if somewhat underappreciated impact on the technology industry at large.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://news.ycombinator.com/item?id=31391272"&gt;Hacker News&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/brandur-leach"&gt;brandur-leach&lt;/a&gt;&lt;/p&gt;



</summary><category term="heroku"/><category term="brandur-leach"/></entry><entry><title>Quoting Adam Wiggins: Heroku Values</title><link href="https://simonwillison.net/2020/Dec/3/adam-wiggins-heroku-values/#atom-tag" rel="alternate"/><published>2020-12-03T21:25:10+00:00</published><updated>2020-12-03T21:25:10+00:00</updated><id>https://simonwillison.net/2020/Dec/3/adam-wiggins-heroku-values/#atom-tag</id><summary type="html">
    &lt;blockquote cite="https://gist.github.com/adamwiggins/5687294"&gt;&lt;p&gt;The value of a product is the number of problems it can solve divided by the amount of complexity the user needs to keep in their head to use it. Consider an iPhone vs a standard TV remote: an iPhone touchscreen can be used for countless different functions, but there's very little to remember about how it works (tap, drag, swipe, pinch). With a TV remote you have to remember what every button does; the more things you can use the remote for, the more buttons it has. We want to create iPhones, not TV remotes.&lt;/p&gt;&lt;/blockquote&gt;
&lt;p class="cite"&gt;&amp;mdash; &lt;a href="https://gist.github.com/adamwiggins/5687294"&gt;Adam Wiggins: Heroku Values&lt;/a&gt;&lt;/p&gt;

    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/product-management"&gt;product-management&lt;/a&gt;&lt;/p&gt;



</summary><category term="heroku"/><category term="product-management"/></entry><entry><title>Running Datasette on DigitalOcean App Platform</title><link href="https://simonwillison.net/2020/Oct/7/datasette-digitalocean-app-platform/#atom-tag" rel="alternate"/><published>2020-10-07T02:52:54+00:00</published><updated>2020-10-07T02:52:54+00:00</updated><id>https://simonwillison.net/2020/Oct/7/datasette-digitalocean-app-platform/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://til.simonwillison.net/til/til/digitalocean_datasette-on-digitalocean-app-platform.md"&gt;Running Datasette on DigitalOcean App Platform&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I spent some time with DigitalOcean’s new App Platform today, which is a Heroku-style PaaS that starts at $5/month. It looks like it could be a really good fit for Datasette. Disk is ephemeral, but if you’re publishing read-only data that doesn’t matter since you can build the SQLite database as part of the deployment and bundle it up in the Docker/Kubernetes container.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/simonw/status/1313673393690959872"&gt;@simonw&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/deployment"&gt;deployment&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/digitalocean"&gt;digitalocean&lt;/a&gt;&lt;/p&gt;



</summary><category term="deployment"/><category term="heroku"/><category term="datasette"/><category term="digitalocean"/></entry><entry><title>db-to-sqlite 1.0 release</title><link href="https://simonwillison.net/2019/Jul/1/db-to-sqlite/#atom-tag" rel="alternate"/><published>2019-07-01T01:35:40+00:00</published><updated>2019-07-01T01:35:40+00:00</updated><id>https://simonwillison.net/2019/Jul/1/db-to-sqlite/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/db-to-sqlite/releases/tag/1.0"&gt;db-to-sqlite 1.0 release&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I’ve released version 1.0 of my db-to-sqlite tool, which lets you create a SQLite database copy of any database supported by SQLAlchemy (I’ve tested it against MySQL and PostgreSQL). The tool has a bunch of new features: you can use --redact to redact specific columns, specify --table multiple times to copy a subset of tables, and the --all option now efficiently adds all foreign keys at the end of the import. The project now has unit tests which run against MySQL and PostgreSQL in Travis CI. Also included in the README: a shell one-liner for creating a local SQLite copy of a remote Heroku Postgres database based on extracting the connection string from a Heroku config environment variable.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/mysql"&gt;mysql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/projects"&gt;projects&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sqlite"&gt;sqlite&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/datasette"&gt;datasette&lt;/a&gt;&lt;/p&gt;



</summary><category term="mysql"/><category term="postgresql"/><category term="projects"/><category term="sqlite"/><category term="heroku"/><category term="datasette"/></entry><entry><title>s3monkey: A Python library that allows you to interact with Amazon S3 Buckets as if they are your local filesystem.</title><link href="https://simonwillison.net/2018/Feb/21/s3monkey/#atom-tag" rel="alternate"/><published>2018-02-21T17:54:36+00:00</published><updated>2018-02-21T17:54:36+00:00</updated><id>https://simonwillison.net/2018/Feb/21/s3monkey/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/kennethreitz/s3monkey"&gt;s3monkey: A Python library that allows you to interact with Amazon S3 Buckets as if they are your local filesystem.&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
A particularly devious hack by Kenneth Reitz—provides a context manager within which various Python filesystem APIs such as open() and os.listdir() are monkeypatched to operate against an S3 bucket instead. Kenneth built it to make it easier to work with files from apps running on Heroku. Under the hood it uses pyfakefs, a filesystem mocking library originally released by Google.

    &lt;p&gt;&lt;small&gt;&lt;/small&gt;Via &lt;a href="https://twitter.com/kennethreitz/status/966364124232941569"&gt;@kennethreitz&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3"&gt;s3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/monkeypatch"&gt;monkeypatch&lt;/a&gt;&lt;/p&gt;



</summary><category term="python"/><category term="s3"/><category term="heroku"/><category term="monkeypatch"/></entry><entry><title>Try hosting on PyPy by simonw</title><link href="https://simonwillison.net/2017/Nov/5/try-hosting-on-pypy/#atom-tag" rel="alternate"/><published>2017-11-05T19:17:29+00:00</published><updated>2017-11-05T19:17:29+00:00</updated><id>https://simonwillison.net/2017/Nov/5/try-hosting-on-pypy/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://github.com/simonw/simonwillisonblog/pull/11"&gt;Try hosting on PyPy by simonw&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I had a go at hosting my blog on PyPy. Thanks to the combination of Travis CI, Sentry and Heroku it was pretty easy to give it a go—I had to swap psycopg2  for psycopg2cffi and switch to the currently undocumented pypy3-5.8.0 Heroku runtime (pypy3-5.5.0 is only compatible with Python 3.3, which Django 2.0 does not support). I ran it in production for a few minutes and didn’t get any Sentry errors but did end up using more Heroku dyno memory than I’m comfortable with—see the graph I posted in a comment. I’m going to stick with CPython 3.6 for the moment. Amusingly I did almost all of the work on this on my phone! Travis CI means it’s easy to create and test a branch through GitHub’s web UI, and deploying a tested branch to Heroku is then just a button click.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/pypy"&gt;pypy&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/travis"&gt;travis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sentry"&gt;sentry&lt;/a&gt;&lt;/p&gt;



</summary><category term="pypy"/><category term="python"/><category term="heroku"/><category term="travis"/><category term="sentry"/></entry><entry><title>Using “import refs” to iteratively import data into Django</title><link href="https://simonwillison.net/2017/Nov/4/import-refs/#atom-tag" rel="alternate"/><published>2017-11-04T19:17:00+00:00</published><updated>2017-11-04T19:17:00+00:00</updated><id>https://simonwillison.net/2017/Nov/4/import-refs/#atom-tag</id><summary type="html">
    &lt;p&gt;I’ve been writing a few scripts to backfill my blog with content I originally posted elsewhere. So far I’ve imported &lt;a href="https://simonwillison.net/tags/quora/"&gt;answers I posted on Quora&lt;/a&gt; (&lt;a href="https://simonwillison.net/2017/Oct/1/ship/"&gt;background&lt;/a&gt;), &lt;a href="https://simonwillison.net/tags/askmetafilter/"&gt;answers I posted on Ask MetaFilter&lt;/a&gt; and &lt;a href="https://simonwillison.net/2017/Oct/8/missing-content/"&gt;content I recovered from the Internet Archive&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;I started out writing custom import scripts (like &lt;a href="https://github.com/simonw/simonwillisonblog/blob/e737be8b4228229e833fe7a9ec698f3e262cd094/blog/management/commands/import_quora.py"&gt;this Quora one&lt;/a&gt;), but I’ve now built a generalized mechanism for this which I thought was worth writing up.&lt;/p&gt;
&lt;p&gt;Any of my content imports now take the form of a JSON document, which looks something like this:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;[
  {
    &amp;quot;body&amp;quot;: &amp;quot;&amp;lt;p&amp;gt;&amp;lt;em&amp;gt;My answer to ...&amp;lt;/em&amp;gt;&amp;lt;/p&amp;gt;&amp;quot;,
    &amp;quot;tags&amp;quot;: [
      &amp;quot;backpacks&amp;quot;,
      &amp;quot;laptops&amp;quot;,
      &amp;quot;style&amp;quot;,
      &amp;quot;accessories&amp;quot;,
      &amp;quot;bags&amp;quot;
    ],
    &amp;quot;title&amp;quot;: &amp;quot;I need a new backpack&amp;quot;,
    &amp;quot;datetime&amp;quot;: &amp;quot;2005-01-16T14:08:00&amp;quot;,
    &amp;quot;import_ref&amp;quot;: &amp;quot;askmetafilter:14075&amp;quot;,
    &amp;quot;type&amp;quot;: &amp;quot;entry&amp;quot;,
    &amp;quot;slug&amp;quot;: &amp;quot;i-need-a-new-backpack&amp;quot;
  }
]
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Two larger examples: the &lt;a href="https://gist.github.com/simonw/5a5bc1f58297d2c7d68dd7448a4d6614"&gt;missing content I extracted from the Internet Archive&lt;/a&gt;, and &lt;a href="https://gist.github.com/simonw/857572d9b36cd1e791c730790ed489ef"&gt;the answers I scraped from Ask MetaFilter&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; property can be set to &lt;code&gt;entry&lt;/code&gt;, &lt;code&gt;quotation&lt;/code&gt; or &lt;code&gt;blogmark&lt;/code&gt; and specifies which type of content should be imported. The &lt;code&gt;datetime&lt;/code&gt;, &lt;code&gt;slug&lt;/code&gt; and &lt;code&gt;tags&lt;/code&gt; fields are common across all three types - the other fields differ for each type.&lt;/p&gt;
&lt;p&gt;The most interesting field here is &lt;code&gt;import_ref&lt;/code&gt;. This is optional, but if provided forms a unique reference associated with that item of content. I then use that reference in a call Django’s &lt;a href="https://docs.djangoproject.com/en/1.11/ref/models/querysets/#update-or-create"&gt;&lt;code&gt;update_or_create()&lt;/code&gt;&lt;/a&gt; method. This means I can run the same import multiple times - the first run will create objects, while subsequent runs update objects in place.&lt;/p&gt;
&lt;p&gt;The end result is that I can incrementally improve the scrapers I am writing, re-importing the resulting JSON to update previously imported records in-place. In addition to hacking on my blog, I’ve been using this pattern for some API integrations at work recently and it’s worked out very well.&lt;/p&gt;
&lt;p&gt;&lt;code&gt;import_ref&lt;/code&gt; is defined on my models as a unique, nullable text field:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;    import_ref = models.TextField(max_length=64, null=True, unique=True)
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Since the Django admin doesn’t handle nullable fields well by default, I &lt;a href="https://github.com/simonw/simonwillisonblog/blob/e737be8b4228229e833fe7a9ec698f3e262cd094/blog/admin.py#L19"&gt;added &lt;code&gt;import_ref&lt;/code&gt; to my &lt;code&gt;readonly_fields&lt;/code&gt; property&lt;/a&gt; in my admin configuration to avoid accidentally setting it to a blank string when editing through the admin interface.&lt;/p&gt;
&lt;p&gt;Here’s my completed &lt;a href="https://github.com/simonw/simonwillisonblog/blob/739a8cb49cfd49da5c643e41027af04d484e2aef/blog/management/commands/import_blog_json.py"&gt;&lt;code&gt;import_blog_json&lt;/code&gt; management command&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;My workflow for importing data is now pretty streamlined. I write the scrapers in &lt;a href="https://github.com/simonw/simonwillisonblog/tree/b7b59e504b5c2f5e04ad59e83a1f4fb6f76c58da/jupyter-notebooks"&gt;a Juyter notebook&lt;/a&gt; and use that to generate a list of importable items as Python dictionaries. I run &lt;code&gt;open('/tmp/items.json').write(json.dumps(items, indent=2))&lt;/code&gt; to dump the items to a JSON file. Then I can run &lt;code&gt;./manage.py import_blog_json /tmp/items.json&lt;/code&gt; to import them into my local development environment - thanks to the &lt;code&gt;import_ref&lt;/code&gt; I can do this as many times as I like until I’m pleased with the result.&lt;/p&gt;
&lt;p&gt;Once it’s ready, I run &lt;code&gt;!cat /tmp/blah.json | pbcopy&lt;/code&gt; in Jupyter to copy the JSON to my clipboard, then paste the JSON into a new &lt;a href="https://gist.github.com/"&gt;GitHub Gist&lt;/a&gt;. I then copy the URL to that raw JSON and execute it against my production instance.&lt;/p&gt;
&lt;p&gt;Heroku tip: running &lt;code&gt;heroku run bash&lt;/code&gt; will start a bash prompt in a dyno hooked up to your application. You can then run &lt;code&gt;./manage.py ...&lt;/code&gt; commands against your production environment.&lt;/p&gt;
&lt;p&gt;So… I just have to run &lt;code&gt;heroku run bash&lt;/code&gt; followed by  &lt;code&gt;./manage.py import_blog_json https://gist.github.com/path-to-json --tag_with=askmetafilter&lt;/code&gt; and the new content will be live on my site.&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;tag_with&lt;/code&gt; option allows me to specify a tag to apply to all of that imported content, useful for checking that everything worked as expected.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django-admin"&gt;django-admin&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/scraping"&gt;scraping&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/jupyter"&gt;jupyter&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="django-admin"/><category term="scraping"/><category term="heroku"/><category term="jupyter"/></entry><entry><title>Squeezing every drop of performance out of a Django app on Heroku</title><link href="https://simonwillison.net/2017/Oct/31/django-app-on-heroku/#atom-tag" rel="alternate"/><published>2017-10-31T14:08:47+00:00</published><updated>2017-10-31T14:08:47+00:00</updated><id>https://simonwillison.net/2017/Oct/31/django-app-on-heroku/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="https://medium.com/@bfirsh/squeezing-every-drop-of-performance-out-of-a-django-app-on-heroku-4b5b1e5a3d44"&gt;Squeezing every drop of performance out of a Django app on Heroku&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
Ben Firshman describes some lesser known tricks for scaling Django on Heroku—in particular, using gunicorn gevent asynchronous workers and setting up PostgreSQL connection pooling using django-db-geventpool.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;&lt;/p&gt;



</summary><category term="django"/><category term="postgresql"/><category term="heroku"/></entry><entry><title>Porting my blog to Python 3</title><link href="https://simonwillison.net/2017/Oct/21/python3/#atom-tag" rel="alternate"/><published>2017-10-21T22:22:40+00:00</published><updated>2017-10-21T22:22:40+00:00</updated><id>https://simonwillison.net/2017/Oct/21/python3/#atom-tag</id><summary type="html">
    &lt;p&gt;This blog is now running on Python 3! Admittedly this is nearly nine years after &lt;a href="https://www.python.org/download/releases/3.0/"&gt;the first release of Python 3.0&lt;/a&gt;, but it’s the first Python 3 project I’ve deployed myself so I’m pretty excited about it.&lt;/p&gt;
&lt;p&gt;Library authors like to use &lt;a href="https://pypi.python.org/pypi/six"&gt;six&lt;/a&gt; to allow them to write code that supports both Python 2 and Python 3 at the same time… but my blog isn’t a library, so I used the &lt;a href="https://docs.python.org/3/library/2to3.html"&gt;2to3 conversion tool&lt;/a&gt; that ships with Python instead.&lt;/p&gt;
&lt;p&gt;And… it worked pretty well! I ran the following command from my project’s root directory:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;2to3 -w -n blog/ config/ redirects/ feedstats/
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;-w&lt;/code&gt; option causes the files to be over-written in place. Since everything is already in git, there was no reason to have 2to3 show my a diff without applying it. Likewise, the &lt;code&gt;-n&lt;/code&gt; option tells 2to3 not to bother saving backups of the files it modifies.&lt;/p&gt;
&lt;p&gt;Here’s &lt;a href="https://github.com/simonw/simonwillisonblog/commit/615efeba55c0c32a8147bda49e207a7a52ddb674"&gt;the initial commit&lt;/a&gt; containing mostly the 2to3 changes.&lt;/p&gt;
&lt;p&gt;Next step: run the tests! My test suite may be very thin, but it does at least check that the app can run its migrations, start up and serve a few basic pages without errors. One of my migrations was failing due to rogue bytestrings but that was &lt;a href="https://github.com/simonw/simonwillisonblog/commit/f00224e7375098bb500b56b35c6f40dbc4955abc#diff-15a9a47c9ee1f93f3a07a4dbe0cf4214"&gt;an easy fix&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;At this point I started to lean heavily on my &lt;a href="https://simonwillison.net/2017/Oct/17/free-continuous-deployment/"&gt;continuous integration setup built on Travis CI&lt;/a&gt;. All of my Python 3 work took place &lt;a href="https://github.com/simonw/simonwillisonblog/tree/python3"&gt;in a branch&lt;/a&gt;, and all it took was a &lt;a href="https://github.com/simonw/simonwillisonblog/commit/f00224e7375098bb500b56b35c6f40dbc4955abc#diff-354f30a63fb0907d4ad57269548329e3"&gt;one line change to my .travis.yml&lt;/a&gt; for Travis to start running the tests for that branch using Python 3.&lt;/p&gt;
&lt;p&gt;With the basic tests working, I made my first deploy to my Heroku staging instance - after first &lt;a href="https://github.com/simonw/simonwillisonblog/commit/54b31e98e35031afbbf8c18f3c2446a0af8b5c65"&gt;modifying my Heroku runtime.txt&lt;/a&gt; to tell it to use Python 3.6.2. My staging environment allowed me to sanity check that everything would work OK when deployed to Heroku.&lt;/p&gt;
&lt;p&gt;At this point I got a bit lazy. The responsible thing to do would have been extensive manual testing plus systematic unit test coverage of core functionality. My blog is hardly a critical piece of infrastructure though, so I went with the faster option: put it all live and &lt;a href="https://simonwillison.net/2017/Oct/17/free-continuous-deployment/#Step_4_Monitor_errors_with_Sentry_75"&gt;use Sentry to see if anything breaks&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This is where Heroku’s ability to deploy a specific branch came in handy: one click to deploy my python3 branch, keep an eye on Sentry (via push notifications from &lt;a href="https://simonwillison.net/2017/Oct/17/free-continuous-deployment/#Step_5_Hook_it_all_together_with_Slack_97"&gt;my private slack channel&lt;/a&gt;) and then one click to deploy my master branch again for an instant rollback in case of errors. Which I had to do instantly, because it turned out I had stored some data in Django’s cache using Python 2 pickle and was trying to read it back out again using Python 3.&lt;/p&gt;
&lt;p&gt;I fixed that by &lt;a href="https://github.com/simonw/simonwillisonblog/commit/41f7a112721ec5772ad189c4293da081291a604a"&gt;bumping my cache VERSION setting&lt;/a&gt; and deployed again. This deploy lasted a few minute longer before Sentry started to fill up with encoding errors and I rolled it back again.&lt;/p&gt;
&lt;p&gt;The single biggest difference between Python 2 and Python 3 is &lt;a href="https://docs.python.org/3.0/whatsnew/3.0.html#text-vs-data-instead-of-unicode-vs-8-bit"&gt;how strings are handled&lt;/a&gt;. Python 3 strings are unicode sequences. Learning to live in a world where strings are all unicode and byte strings are the rare, deliberate exceptions takes some getting used to.&lt;/p&gt;
&lt;p&gt;The key challenge for my blog actually came from &lt;a href="https://github.com/simonw/simonwillisonblog/blob/45d7acd56af475119d2738e736d9b4cb19a9e8eb/blog/templatetags/entry_tags.py"&gt;my custom markup handling template tags&lt;/a&gt;. 15 years ago &lt;a href="https://simonwillison.net/2002/Jun/16/myFirstXhtmlMindBomb/"&gt;I made the decision&lt;/a&gt; to &lt;a href="https://simonwillison.net/2003/Jan/6/xhtmlIsJustFine/"&gt;store all of my blog entries&lt;/a&gt; as valid XHTML fragments. This meant I could use XML processors - back then in PHP, today &lt;a href="https://docs.python.org/3/library/xml.etree.elementtree.html"&gt;Python’s ElementTree&lt;/a&gt; - to perform various transformations on my content.&lt;/p&gt;
&lt;p&gt;ElementTree in Python 2 can only consume bytestrings. In Python 3 it expects unicode strings. Cleaning this up took a while, eventually inspiring me to &lt;a href="https://github.com/simonw/simonwillisonblog/commit/7295cddd1a6ab2c7bc6fcf3da410ab6ea0954791"&gt;refactor my custom template tags completely&lt;/a&gt;. In the process I realized that my blog templates were mostly written back before Django’s template language implemented autoescape (&lt;a href="https://simonwillison.net/2008/Jul/22/alpha/"&gt;in Django 1.0&lt;/a&gt;), so my code was littered with unnecessary &lt;code&gt;|escape&lt;/code&gt; and &lt;code&gt;|safe&lt;/code&gt; filters. Those are all gone now.&lt;/p&gt;
&lt;p&gt;Sentry lets you mark an exception as “resolved” when you think you’ve fixed it - if it occurs again after that it will be re-reported to your Slack channel and added back to the Sentry list of unresolved issues. Once Sentry was clear (especially given Googlebot had crawled my older pages) I could be pretty confident there were no more critical 500-causing errors.&lt;/p&gt;
&lt;p&gt;That left logic errors, of which only one has cropped up so far: the “zero years ago” bug. Entries on my homepage include a relative date representation, e.g. “three days ago”. Python 3 &lt;a href="https://www.python.org/dev/peps/pep-0238/"&gt;changed how the divison operator works on integers&lt;/a&gt; - &lt;code&gt;3 / 2 == 1.5&lt;/code&gt; where in Python 2 it gets truncated to &lt;code&gt;1&lt;/code&gt;. As a result, every entry on my homepage showed “zero years ago”. Thankfully this was &lt;a href="https://github.com/simonw/simonwillisonblog/commit/d6e6eeb93ac02aa33c59b151f2a20e26d41f18b1"&gt;a one-line fix&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;All in all this process was much less painful than I expected. It took me longer to write this blog entry than it did to actually make the conversion (thanks to 2to3 doing most of the tedious work), and the combination of Travis CI, Sentry and Heroku allowed me to ship aggressively with the knowledge that I could promptly identify and resolve any issues that came up.&lt;/p&gt;
&lt;p&gt;Next upgrade: &lt;a href="https://www.djangoproject.com/weblog/2017/oct/16/django-20-beta-1-released/"&gt;Django 2.0&lt;/a&gt;!&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/continuous-integration"&gt;continuous-integration&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python3"&gt;python3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/travis"&gt;travis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sentry"&gt;sentry&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="continuous-integration"/><category term="python"/><category term="python3"/><category term="heroku"/><category term="travis"/><category term="sentry"/></entry><entry><title>How to set up world-class continuous deployment using free hosted tools</title><link href="https://simonwillison.net/2017/Oct/17/free-continuous-deployment/#atom-tag" rel="alternate"/><published>2017-10-17T13:32:49+00:00</published><updated>2017-10-17T13:32:49+00:00</updated><id>https://simonwillison.net/2017/Oct/17/free-continuous-deployment/#atom-tag</id><summary type="html">
    &lt;p&gt;I’m going to describe a way to put together a world-class continuous deployment infrastructure for your side-project without spending any money.&lt;/p&gt;
&lt;p&gt;With &lt;a href="https://puppet.com/blog/continuous-delivery-vs-continuous-deployment-what-s-diff"&gt;continuous deployment&lt;/a&gt; every code commit is tested against an automated test suite. If the tests pass it gets deployed directly to the production environment! How’s that for an incentive to write comprehensive tests?&lt;/p&gt;
&lt;p&gt;Each of the tools I’m using offers a free tier which is easily enough to handle most side-projects. And once you outgrow those free plans, you can solve those limitations in exchange for money!&lt;/p&gt;
&lt;p&gt;Here’s the magic combination:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/"&gt;GitHub&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://travis-ci.org/"&gt;Travis CI&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://heroku.com/"&gt;Heroku&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://sentry.io/"&gt;Sentry&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://slack.com/"&gt;Slack&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;&lt;a id="Step_one_Publish_some_code_to_GitHub_with_some_tests_16"&gt;&lt;/a&gt;Step one: Publish some code to GitHub with some tests&lt;/h2&gt;
&lt;p&gt;I’ll be using the &lt;a href="https://github.com/simonw/simonwillisonblog"&gt;code for my blog&lt;/a&gt; as an example. It’s a classic Django application, with a small (OK, tiny) suite of unit tests. The tests are run using the standard Django &lt;code&gt;./manage.py test&lt;/code&gt; command.&lt;/p&gt;
&lt;p&gt;Writing a Django application with tests is outside the scope of this article. Thankfully the official Django tutorial &lt;a href="https://docs.djangoproject.com/en/1.11/intro/tutorial05/"&gt;covers testing in some detail&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Step_two_Hook_up_Travis_CI_22"&gt;&lt;/a&gt;Step two: Hook up Travis CI&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://travis-ci.org/"&gt;Travis CI&lt;/a&gt; is an outstanding hosted platform for continuous integration. Given a small configuration file it can check out code from GitHub, set up an isolated test environment (including hefty dependencies like a PostgreSQL database server, Elasticsearch, Redis etc), run your test suite and report the resulting pass/fail grade back to GitHub.&lt;/p&gt;
&lt;p&gt;It’s free for publicly hosted GitHub projects. If you want to test code in a private repository you’ll have to pay them some money.&lt;/p&gt;
&lt;p&gt;Here’s &lt;a href="https://github.com/simonw/simonwillisonblog/blob/a5c2d2549f26dd2d75cbf863c8b36d617092c2a1/.travis.yml"&gt;my .travis.yml configuration file&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;language: python

python:
  - 2.7

services: postgresql

addons:
  postgresql: &amp;quot;9.6&amp;quot;

install:
  - pip install -r requirements.txt

before_script:
  - psql -c &amp;quot;CREATE DATABASE travisci;&amp;quot; -U postgres
  - python manage.py migrate --noinput
  - python manage.py collectstatic

script:
  - python manage.py test
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;And here’s the resulting &lt;a href="https://travis-ci.org/simonw/simonwillisonblog"&gt;Travis CI dashboard&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The integration of Travis with GitHub runs &lt;em&gt;deep&lt;/em&gt;. Once you’ve set up Travis, it will automatically test every push to every branch - driven by GitHub webhooks, so test runs are set off almost instantly. Travis will then report the test results back to GitHub, where they’ll show up in a bunch of different places -  including these pleasing green ticks on &lt;a href="https://github.com/simonw/simonwillisonblog/branches"&gt;the branches page&lt;/a&gt;:&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/github-branches-with-ci-small.png" alt="GitHub branches page showing CI results" /&gt;&lt;/p&gt;
&lt;p&gt;Travis will also run tests against any &lt;a href="https://github.com/simonw/simonwillisonblog/pull/3"&gt;open pull requests&lt;/a&gt;. This is a great incentive to build new features in a pull request even if you aren’t using them for code review:&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/github-pull-request-with-ci-small.png" alt="GitHub pull request showing CI results" /&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://circleci.com/"&gt;Circle CI&lt;/a&gt; deserves a mention as an alternative to Travis. The two are close competitors and offer very similar feature sets, and Circle CI's free plan allows up to 1,500 build minutes of private repositories per month.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Update 25th July 2020&lt;/strong&gt;: I've started using GitHub Actions for most of my projects now - see my &lt;a href="https://simonwillison.net/tags/githubactions/"&gt;githubactions&lt;/a&gt; tag.&lt;/p&gt;

&lt;h2&gt;&lt;a id="Step_3_Deploy_to_Heroku_and_turn_on_continuous_deployment_61"&gt;&lt;/a&gt;Step 3: Deploy to Heroku and turn on continuous deployment&lt;/h2&gt;
&lt;p&gt;I’m a big fan of &lt;a href="https://heroku.com/"&gt;Heroku&lt;/a&gt; for side projects, because it means not having to worry about ongoing server-maintenance. I’ve lost several side-projects to &lt;a href="https://blog.heroku.com/archives/2011/6/28/the_new_heroku_4_erosion_resistance_explicit_contracts/"&gt;entropy and software erosion&lt;/a&gt; - getting an initial VPS set up may be pretty simple, but a year later security patches need applying and the OS needs upgrading and the log files have filled up the disk and you’ve forgotten how you set everything up in the first place…&lt;/p&gt;
&lt;p&gt;It turns out Heroku has basic support for continuous deployment baked in, and it’s trivially easy to set up. You can tell Heroku to deploy on every commit to GitHub, and then if you’ve attached a CI service like Travis that reports build health back you can check the box for “Wait for CI to pass before deploy”:&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/heroku-deploy-settings-small.png" alt="Heroku deployment settings for continuous deployment" /&gt;&lt;/p&gt;
&lt;p&gt;Since small dynos on Heroku are free, you can even set up a separate Heroku app as a staging environment. I started my continuous integration adventure just deploying automatically to my staging instance, then switched over to deploying to production once I gained some confidence in how it all fitted together.&lt;/p&gt;
&lt;p&gt;If you’re using continuous deployment with Heroku and Django, it’s a good idea to set up Heroku to automatically run your migrations for every deploy - otherwise you might merge a pull request with a model change and forget to run the migrations before the deploy goes out. You can do that using Heroku’s &lt;a href="https://devcenter.heroku.com/articles/release-phase"&gt;release phase&lt;/a&gt; feature, by adding the line &lt;code&gt;release: python manage.py migrate --noinput&lt;/code&gt; to your Heroku &lt;code&gt;Procfile&lt;/code&gt; (&lt;a href="https://github.com/simonw/simonwillisonblog/blob/81f7e2ba19b84f572e8a546bcc28bbfb1e211eb6/Procfile"&gt;here’s mine&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;Once you go beyond Heroku’s free tier things get much more powerful: &lt;a href="https://www.heroku.com/flow"&gt;Heroku Flow&lt;/a&gt; combines pipelines, review apps and their own CI solution to provide a comprehensive solution for much larger teams.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Step_4_Monitor_errors_with_Sentry_75"&gt;&lt;/a&gt;Step 4: Monitor errors with Sentry&lt;/h2&gt;
&lt;p&gt;If you’re going to move fast and break things, you need to know when things have broken. &lt;a href="https://sentry.io/"&gt;Sentry&lt;/a&gt; is a fantastic tool for collecting exceptions, aggregating them and spotting when something new crops up. It’s open source so you can host it yourself, but they also offer a robust hosted version with a free plan that can track up to 10,000 errors a month.&lt;/p&gt;
&lt;p&gt;My favourite feature of Sentry is that it gives each exception it sees a “signature” based on a MD5 hash of its traceback. This means it can tell if errors are the same underlying issue or something different, and can hence de-dupe them and only alert you the first time it spots an error it has not seen before.&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/sentry-small.png" alt="Notifications from Travis CI and GitHub in Slack" /&gt;&lt;/p&gt;
&lt;p&gt;Sentry has integrations for most modern languages, but it’s particularly easy to use with Django. Just install &lt;a href="https://pypi.python.org/pypi/raven"&gt;raven&lt;/a&gt; and add few extra lines to your &lt;a href="http://settings.py"&gt;settings.py&lt;/a&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;SENTRY_DSN = os.environ.get('SENTRY_DSN')
if SENTRY_DSN:
    INSTALLED_APPS += (
        'raven.contrib.django.raven_compat',
    )
    RAVEN_CONFIG = {
        'dsn': SENTRY_DSN,
        'release': os.environ.get('HEROKU_SLUG_COMMIT', ''),
    }
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here I’m using the Heroku pattern of &lt;a href="https://devcenter.heroku.com/articles/config-vars"&gt;keeping configuration in environment variables&lt;/a&gt;. &lt;code&gt;SENTRY_DSN&lt;/code&gt; is provided by Sentry when you create your project there - you just have to add it as a Heroku config variable.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;HEROKU_SLUG_COMMIT&lt;/code&gt; line causes the currently deployed git commit hash to be fed to Sentry so that it knows what version of your code was running when it reports an error. To enable that variable, you’ll need to &lt;a href="https://devcenter.heroku.com/articles/dyno-metadata"&gt;enable Dyno Metadata&lt;/a&gt; by running &lt;code&gt;heroku labs:enable runtime-dyno-metadata&lt;/code&gt; against your application.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Step_5_Hook_it_all_together_with_Slack_97"&gt;&lt;/a&gt;Step 5: Hook it all together with Slack&lt;/h2&gt;
&lt;p&gt;Would you like a push notification to your phone every time your site gets code committed / the tests pass or fail / a deploy goes out / a new error is detected? All of the above tools can report such things to &lt;a href="https://slack.com/"&gt;Slack&lt;/a&gt;, and Slack’s free plan is easily enough to collect all of these notifications and push them to your phone via the free Slack &lt;a href="https://slack.com/downloads/ios"&gt;iOS&lt;/a&gt; or &lt;a href="https://slack.com/downloads/android"&gt;Android&lt;/a&gt; apps.&lt;/p&gt;
&lt;p&gt;&lt;img style="width: 100%" src="https://static.simonwillison.net/static/2017/slack-github-ci-small.png" alt="Notifications from Travis CI and GitHub in Slack" /&gt;&lt;/p&gt;
&lt;p&gt;Here are instructions for setting up Slack with &lt;a href="https://get.slack.help/hc/en-us/articles/232289568-Use-GitHub-with-Slack"&gt;GitHub&lt;/a&gt;, &lt;a href="https://docs.travis-ci.com/user/notifications/#Configuring-slack-notifications"&gt;Travis CI&lt;/a&gt;, &lt;a href="https://slack.com/apps/A0F7VRF7E-heroku"&gt;Heroku&lt;/a&gt; and &lt;a href="https://slack.com/apps/A0F814BEV-sentry"&gt;Sentry&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;&lt;a id="Need_more_Pay_for_it_105"&gt;&lt;/a&gt;Need more? Pay for it!&lt;/h2&gt;
&lt;p&gt;Having run much of this kind of infrastructure myself in the past I for one am delighted by the idea of outsourcing it, especially when the hosted options are of such high quality.&lt;/p&gt;
&lt;p&gt;Each of these tools offers a free tier which is generous enough to work great for small side projects. As you start scaling up, you can start paying for them - that’s why they gave you a free tier in the first place.&lt;/p&gt;

&lt;p&gt;Comments or suggestions? Join &lt;a href="https://news.ycombinator.com/item?id=15490935"&gt;this thread on Hacker News&lt;/a&gt;.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/continuous-deployment"&gt;continuous-deployment&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/continuous-integration"&gt;continuous-integration&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/github"&gt;github&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/postgresql"&gt;postgresql&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/testing"&gt;testing&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/slack"&gt;slack&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/travis"&gt;travis&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/sentry"&gt;sentry&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="continuous-deployment"/><category term="continuous-integration"/><category term="django"/><category term="github"/><category term="postgresql"/><category term="testing"/><category term="heroku"/><category term="slack"/><category term="travis"/><category term="sentry"/></entry><entry><title>Running gunicorn behind nginx on Heroku for buffering and logging</title><link href="https://simonwillison.net/2017/Oct/2/nginx-heroku/#atom-tag" rel="alternate"/><published>2017-10-02T01:57:20+00:00</published><updated>2017-10-02T01:57:20+00:00</updated><id>https://simonwillison.net/2017/Oct/2/nginx-heroku/#atom-tag</id><summary type="html">
    &lt;p&gt;Heroku's default setup for Django uses the &lt;a href="http://gunicorn.org/"&gt;gunicorn&lt;/a&gt; application server. Each
Heroku dyno can only run a limited number of gunicorn workers, which means a
limited number of requests can be served in parallel (around 4 per dyno is a
good rule of thumb).&lt;/p&gt;

&lt;p&gt;Where things get nasty is when you have devices on slow connections - like
mobile phones. Heroku's router buffers headers but it does not buffer response
bodies, so a slow device could hold up a gunicorn worker for several seconds.
Too many slow devices at once and the site will become unavailable to other
users.&lt;/p&gt;

&lt;p&gt;This issue is explained and discussed here: &lt;a href="http://blog.etianen.com/blog/2014/01/19/gunicorn-heroku-django/"&gt;Don't use Gunicorn to host your Django sites on Heroku &lt;/a&gt;&lt;/p&gt;

&lt;p&gt;That article recommends using waitress as an alternative to gunicorn, but in
the comments at the bottom of the article people suggest using a Heroku
&lt;a href="https://github.com/beanieboi/nginx-buildpack"&gt;nginx-buildpack&lt;/a&gt; as an alternative.&lt;/p&gt;

&lt;p&gt;Here is a slightly out-of-date tutorial on getting this all set up: &lt;a href="https://koed00.github.io/Heroku_setups/"&gt;https://koed00.github.io/Heroku_setups/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I used the following commands to set up the buildpacks:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;heroku stack:set cedar-14
heroku buildpacks:clear
heroku buildpacks:add https://github.com/beanieboi/nginx-buildpack.git
heroku buildpacks:add https://github.com/heroku/heroku-buildpack-python.git
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Unfortunately the nginx buildpack is not yet compatible with the new &lt;samp&gt;heroku-16&lt;/samp&gt;
stack, so until the nginx buildpack has been updated it's necessary to run the
application on the older &lt;samp&gt;cedar-14&lt;/samp&gt; stack. See this discussion for details: &lt;a href="https://github.com/ryandotsmith/nginx-buildpack/issues/68"&gt;ryandotsmith/nginx-buildpack#68&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Adding nginx in this way also gives us the opportunity to fix another
limitation of Heroku: its default logging configuration. By default, log lines produced by Heroku (visible using &lt;samp&gt;heroku logs --tail&lt;/samp&gt; or with a logging addon such as &lt;a href="https://elements.heroku.com/addons/papertrail"&gt;Papertrail&lt;/a&gt;) look like
this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    Oct 01 18:01:06 simonwillisonblog heroku/router: at=info
        method=GET path="/2017/Oct/1/ship/" host=simonwillison.net
        request_id=bb22f67e-6924-4e81-b6ad-74d1f465cda7
        fwd="2001:8003:74c5:8b00:79e4:80ed:fa85:7b37,108.162.249.198"
        dyno=web.1 connect=0ms service=338ms status=200 bytes=4523 protocol=http
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;Notably missing here is both the user-agent string and the referrer header
sent by the browser! If you're a fan of tailing log files these omissions are pretty
disappointing.&lt;/p&gt;

&lt;p&gt;The nginx buildback I'm using loads a default configuration file at
&lt;samp&gt;config/nginx.conf.erb&lt;/samp&gt;. By including &lt;a href="https://github.com/simonw/simonwillisonblog/blob/ad874a2bf9ebfeffcb0a1a7f8594ad9735fcfc01/config/nginx.conf.erb"&gt;my own copy of this file&lt;/a&gt; I can override
the original and define my own custom log format.&lt;/p&gt;

&lt;p&gt;Having applied this change, the new log lines look like this:&lt;/p&gt;

&lt;pre&gt;&lt;code&gt;    2017-10-02T01:44:38.762845+00:00 app[web.1]:
        measure#nginx.service=0.133 request="GET / HTTP/1.1" status_code=200
        request_id=8b6402de-d072-42c4-9854-0f71697b30e5 remote_addr="10.16.227.159"
        forwarded_for="199.188.193.220" forwarded_proto="http" via="1.1 vegur"
        body_bytes_sent=12666 referer="-" user_agent="Mozilla/5.0 (Macintosh;
        Intel Mac OS X 10_11_6) AppleWebKit/537.36 (KHTML, like Gecko)
        Chrome/61.0.3163.100 Safari/537.36"
&lt;/code&gt;&lt;/pre&gt;

&lt;p&gt;&lt;em&gt;This blog entry started life as &lt;a href="https://github.com/simonw/simonwillisonblog/commit/23615a4822ab463c611a3e6a1f4d6cb4dcfc5e7b"&gt;a commit message&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/logging"&gt;logging&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/nginx"&gt;nginx&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/user-agents"&gt;user-agents&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/gunicorn"&gt;gunicorn&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="django"/><category term="logging"/><category term="nginx"/><category term="user-agents"/><category term="heroku"/><category term="gunicorn"/></entry><entry><title>What's the cheapest or free stack solution to deploy and experiment with a realtime application in 2016?</title><link href="https://simonwillison.net/2016/Aug/16/whats-the-cheapest-or/#atom-tag" rel="alternate"/><published>2016-08-16T09:02:00+00:00</published><updated>2016-08-16T09:02:00+00:00</updated><id>https://simonwillison.net/2016/Aug/16/whats-the-cheapest-or/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/Whats-the-cheapest-or-free-stack-solution-to-deploy-and-experiment-with-a-realtime-application-in-2016/answer/Simon-Willison"&gt;What&amp;#39;s the cheapest or free stack solution to deploy and experiment with a realtime application in 2016?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Heroku have a good free tier, and comprehensive support for deploying both Python and Node.js. If you are mainly interested in realtime I would suggest starting out with Node.js on Heroku. Depending on the complexity of your project you might even be able to use raw Node.js without adding something like Express.&lt;/p&gt;&lt;p&gt;&lt;span&gt;&lt;a href="https://devcenter.heroku.com/articles/getting-started-with-nodejs#introduction"&gt;Getting Started on Heroku with Node.js&lt;/a&gt;&lt;/span&gt;&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/hosting"&gt;hosting&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/programming"&gt;programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/webapps"&gt;webapps&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-development"&gt;web-development&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/websockets"&gt;websockets&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="hosting"/><category term="programming"/><category term="webapps"/><category term="web-development"/><category term="websockets"/><category term="quora"/><category term="heroku"/></entry><entry><title>For a Django application, deployed on Heroku, what are my options for storing user-uploaded media files?</title><link href="https://simonwillison.net/2013/Oct/21/for-a-django-application/#atom-tag" rel="alternate"/><published>2013-10-21T18:28:00+00:00</published><updated>2013-10-21T18:28:00+00:00</updated><id>https://simonwillison.net/2013/Oct/21/for-a-django-application/#atom-tag</id><summary type="html">
    &lt;p&gt;&lt;em&gt;My answer to &lt;a href="https://www.quora.com/For-a-Django-application-deployed-on-Heroku-what-are-my-options-for-storing-user-uploaded-media-files/answer/Simon-Willison"&gt;For a Django application, deployed on Heroku, what are my options for storing user-uploaded media files?&lt;/a&gt; on Quora&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;S3 is really a no-brainer for this, it's extremely inexpensive, very easy to integrate with and unbelievably reliable. It's so cheap that it will be practically free for testing purposes (expect to spend pennies a month on it).&lt;/p&gt;

&lt;p&gt;You could store uploaded files in the Heroku database, but that will explode the size of your backups and will be much more expensive than paying for S3.&lt;/p&gt;
    
        &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/aws"&gt;aws&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/django"&gt;django&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/photos"&gt;photos&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/programming"&gt;programming&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/python"&gt;python&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/s3"&gt;s3&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/web-development"&gt;web-development&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/quora"&gt;quora&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;&lt;/p&gt;
    

</summary><category term="aws"/><category term="django"/><category term="photos"/><category term="programming"/><category term="python"/><category term="s3"/><category term="web-development"/><category term="quora"/><category term="heroku"/></entry><entry><title>The New Heroku (Part 4 of 4): Erosion-resistance &amp; Explicit Contracts</title><link href="https://simonwillison.net/2011/Jun/29/heroku/#atom-tag" rel="alternate"/><published>2011-06-29T17:26:00+00:00</published><updated>2011-06-29T17:26:00+00:00</updated><id>https://simonwillison.net/2011/Jun/29/heroku/#atom-tag</id><summary type="html">
    
&lt;p&gt;&lt;strong&gt;&lt;a href="http://blog.heroku.com/archives/2011/6/28/the_new_heroku_4_erosion_resistance_explicit_contracts/"&gt;The New Heroku (Part 4 of 4): Erosion-resistance &amp;amp; Explicit Contracts&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;
I really like Adam’s description of Software Erosion—I’ve seen that happen to my projects a bunch of times, and it really is an important problem to solve.


    &lt;p&gt;Tags: &lt;a href="https://simonwillison.net/tags/heroku"&gt;heroku&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/recovered"&gt;recovered&lt;/a&gt;, &lt;a href="https://simonwillison.net/tags/software-erosion"&gt;software-erosion&lt;/a&gt;&lt;/p&gt;



</summary><category term="heroku"/><category term="recovered"/><category term="software-erosion"/></entry></feed>