<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><title>Launch by Lunch</title><link href="https://launchbylunch.com/" rel="alternate"></link><link href="https://launchbylunch.com/feeds/all.atom.xml" rel="self"></link><id>https://launchbylunch.com/</id><updated>2026-07-29T00:00:00-04:00</updated><subtitle>Databases, DevOps, and Development</subtitle><entry><title>Introducing pg-java, a new PostgreSQL driver for the JVM</title><link href="https://launchbylunch.com/posts/2026/Jul/29/introducing-pg-java/" rel="alternate"></link><published>2026-07-29T00:00:00-04:00</published><updated>2026-07-29T00:00:00-04:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2026-07-29:/posts/2026/Jul/29/introducing-pg-java/</id><summary type="html">&lt;p&gt;A brand new PostgreSQL driver for Java, built PostgreSQL-first with JDBC as a layer on top, written commit-by-commit by an AI in a loop against a very large regression suite.&lt;/p&gt;</summary><content type="html">&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-a-new-driver"&gt;Why a new driver&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#things-you-wont-find-in-other-java-drivers"&gt;Things you won't find in other Java drivers&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#batch-insert-rewritten-to-array-parameters"&gt;Batch INSERT rewritten to array parameters&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-real-pipelining-api"&gt;A real pipelining API&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#it-wont-hand-your-password-to-an-unencrypted-socket"&gt;It won't hand your password to an unencrypted socket&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#the-public-api-surface-is-a-file-that-the-build-enforces"&gt;The public API surface is a file that the build enforces&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#small-other-stuff"&gt;Small other stuff&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#performance"&gt;Performance&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#about-that-written-by-ai-part"&gt;About that "written by AI" part&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#a-long-time-coming"&gt;A long time coming&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#status-and-whats-next"&gt;Status and what's next&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#final-thoughts"&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;p&gt;I've been working on &lt;a href="https://github.com/pgjdbc/pgjdbc"&gt;pgjdbc&lt;/a&gt;, the PostgreSQL JDBC driver, for many years.
It's a great driver, gets millions of monthly downloads, and it's not going anywhere.&lt;/p&gt;
&lt;p&gt;It's also a driver whose shape was decided a very long time ago.
JDBC came first and PostgreSQL came second.
The JVM it was designed for had no virtual threads, no records, and no sealed types.
Most of what you'd want to change about that today can't be changed in place.
Doing so would break the applications that depend on the current behavior, and that's most of the Java world talking to PostgreSQL.&lt;/p&gt;
&lt;p&gt;So I started a new one: &lt;a href="https://github.com/pgjdbc/pg-java"&gt;pg-java&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It's a modern, PostgreSQL-specific driver for the JVM, and it is &lt;em&gt;pre-release&lt;/em&gt;.
But the core driver works, there's a JDBC layer on top of it, and it's been tested far more thoroughly than "pre-release" usually implies.&lt;/p&gt;
&lt;h2 id="why-a-new-driver"&gt;Why a new driver&lt;/h2&gt;
&lt;p&gt;Four things drove the design.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;PostgreSQL-first.&lt;/strong&gt;
The native API is designed around PostgreSQL's wire protocol and feature set.
It is not designed around the lowest common denominator that JDBC has to support across every database on earth.
If PostgreSQL can do it, the API should be able to say it directly.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;JDBC as a layer, not a foundation.&lt;/strong&gt;
Full JDBC compliance is a long-term goal.
There's already a &lt;code&gt;java.sql.*&lt;/code&gt; layer that registers a &lt;code&gt;Driver&lt;/code&gt; and gives you &lt;code&gt;Connection&lt;/code&gt;, &lt;code&gt;PreparedStatement&lt;/code&gt;, &lt;code&gt;ResultSet&lt;/code&gt;, &lt;code&gt;DatabaseMetaData&lt;/code&gt;, &lt;code&gt;DataSource&lt;/code&gt;, and XA.
But it's built &lt;em&gt;on top of&lt;/em&gt; the native API rather than dictating its shape.
That ordering matters. Once JDBC's assumptions get into the execution core they never come back out.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Virtual threads for I/O.&lt;/strong&gt;
For a decade, "fast driver" implied an async or reactive API, because the alternative was a thread per connection.
On Java 21 that trade is gone.
pg-java is ordinary blocking-style code that is careful never to pin a carrier thread (&lt;em&gt;which mostly means &lt;code&gt;ReentrantLock&lt;/code&gt; instead of &lt;code&gt;synchronized&lt;/code&gt; around I/O&lt;/em&gt;).
You can run thousands of connections on virtual threads without an event loop or a callback API in sight.
It's also significantly easier to reason about both how it works and how you would use it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Streaming by default.&lt;/strong&gt;
The core query primitive is a pull cursor.
It reads exactly enough off the wire to produce the next row and never buffers a whole result set.
&lt;code&gt;forEach&lt;/code&gt;, &lt;code&gt;map&lt;/code&gt;, &lt;code&gt;collect&lt;/code&gt;, and friends are adapters built on top of it, not a second read path.&lt;/p&gt;
&lt;p&gt;Here's the native API:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;PgConnectionConfig&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PgConnectionConfig&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;builder&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;localhost&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;database&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;appdb&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;app&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;build&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PgConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PgConnections&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;connect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;config&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;PgResultStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;execute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;select id, name from widget where kind = $1&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;gear&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;Row&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;currentRow&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot; &amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Note the &lt;code&gt;$1&lt;/code&gt; and the 1-based column indexes.
This is PostgreSQL's protocol, spelled the way PostgreSQL spells it, with no &lt;code&gt;java.sql&lt;/code&gt; anywhere.&lt;/p&gt;
&lt;p&gt;And here's the same thing through JDBC, which is what most people will actually use:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;jdbc:pg://localhost:5432/appdb?user=app&amp;amp;sslmode=verify-full&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Connection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;DriverManager&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getConnection&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;app&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="n"&gt;PreparedStatement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="w"&gt;                &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;prepareStatement&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;select id from t where name = ?&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;setString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;widget&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;ResultSet&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;executeQuery&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="k"&gt;while&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;next&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="n"&gt;System&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;println&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;getLong&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The driver owns the &lt;code&gt;jdbc:pg:&lt;/code&gt; scheme and also answers to &lt;code&gt;jdbc:postgresql:&lt;/code&gt; for ported applications.&lt;/p&gt;
&lt;p&gt;There's a third module too: a pgjdbc source-compatibility layer that exposes &lt;code&gt;org.postgresql.Driver&lt;/code&gt; and the usual &lt;code&gt;org.postgresql.*&lt;/code&gt; vendor types.
It's there for applications that want to try the new driver without touching their code.
Its target is a migration aid rather than a certified drop-in (and like everything else, still pre-release).&lt;/p&gt;
&lt;h2 id="things-you-wont-find-in-other-java-drivers"&gt;Things you won't find in other Java drivers&lt;/h2&gt;
&lt;p&gt;Some of what follows is just modern-rewrite table stakes.
A few pieces I haven't seen anywhere else.&lt;/p&gt;
&lt;h3 id="batch-insert-rewritten-to-array-parameters"&gt;Batch INSERT rewritten to array parameters&lt;/h3&gt;
&lt;p&gt;Every driver hits the same wall on bulk DML: a batch of N inserts is N round trips.
pgjdbc's answer is &lt;code&gt;reWriteBatchedInserts&lt;/code&gt;, which collapses them into one multi-row &lt;code&gt;VALUES&lt;/code&gt; statement.
That works, but it has costs.
The SQL text grows with the batch size.
It burns N*M scalar parameters, so it has to chunk to stay under the protocol's 65535-parameter cap.
And every distinct batch size is a new statement for the server to parse and plan.&lt;/p&gt;
&lt;p&gt;pg-java can rewrite the same batch into &lt;em&gt;array parameters&lt;/em&gt; instead:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;-- your SQL, batched N times:&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;-- rewritten, one execution, three array parameters of length N:&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;t&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;c&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;unnest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;int4&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;timestamptz&lt;/span&gt;&lt;span class="p"&gt;[])&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The SQL text is now constant regardless of the batch size.
That means one prepared statement, planned once, with perfect cache reuse.
It uses M parameters (one array per column) instead of N*M, so it never approaches the parameter cap and never needs chunking.
And it's a single round trip and a single server-side execution.
In the benchmarks it runs about 2x the non-rewritten path and lands slightly ahead of pgjdbc's best batch mode.&lt;/p&gt;
&lt;p&gt;It's opt-in via &lt;code&gt;rewriteBatchUsingArrays&lt;/code&gt; and, for now, INSERT-only.
That restriction is deliberate.
For UPDATE and DELETE you can't reconstruct per-element update counts from a single execution, which silently breaks optimistic-locking checks and anything built on them.
UPDATE has a sharper problem.
If two batch entries target the same row then &lt;code&gt;UPDATE ... FROM unnest(...)&lt;/code&gt; applies one arbitrarily chosen source row instead of applying them in order.
The final state of your data can differ, not just the counts.
Silently changing what a batch &lt;em&gt;does&lt;/em&gt; is not an acceptable price for making it fast.&lt;/p&gt;
&lt;p&gt;The pgjdbc-style multi-row &lt;code&gt;VALUES&lt;/code&gt; rewrite is implemented too, separately, so the compat layer can reproduce pgjdbc's exact behavior when an application asks for it.&lt;/p&gt;
&lt;h3 id="a-real-pipelining-api"&gt;A real pipelining API&lt;/h3&gt;
&lt;p&gt;The extended query protocol never required waiting for one statement's results before sending the next.
libpq has had pipeline mode for years.
Java drivers generally don't expose it.
pg-java does:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PgPipeline&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;connection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;pipeline&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PgPipelineStatement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;insert into t (v) values ($1)&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;List&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;of&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;PgPipelineStatement&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;queue&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;&amp;quot;select v from t&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;sync&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;                                  &lt;/span&gt;&lt;span class="c1"&gt;// explicit error boundary&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="n"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="na"&gt;summary&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;&lt;span class="w"&gt;                     &lt;/span&gt;&lt;span class="c1"&gt;// results come back in queue order&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="k"&gt;try&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PgResultStream&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;rs&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="cm"&gt;/* ... */&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;sync()&lt;/code&gt; is the public error-boundary primitive.
Each one closes a section.
If the server errors inside a section, the failing handle's read throws, later handles in that same section throw a "skipped" exception carrying the first error as the cause, and later sections are unaffected.
Handles are strict FIFO.&lt;/p&gt;
&lt;p&gt;The window is byte-budgeted with explicit flow control.
That part is not tuning.
A pipeline that writes blindly while the server streams rows back will deadlock the moment both TCP buffers fill.&lt;/p&gt;
&lt;h3 id="it-wont-hand-your-password-to-an-unencrypted-socket"&gt;It won't hand your password to an unencrypted socket&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;sslmode&lt;/code&gt; defaults to &lt;code&gt;prefer&lt;/code&gt;, matching libpq.&lt;/p&gt;
&lt;p&gt;Defaulting it to &lt;code&gt;verify-full&lt;/code&gt; sounds better in theory but works out worse in practice.
It breaks every server with a self-signed or private-CA certificate, which is most PostgreSQL installations.
So it would be a default that everybody has to override, and they'd override it with &lt;code&gt;sslmode=disable&lt;/code&gt;.
That's strictly worse.&lt;/p&gt;
&lt;p&gt;So the credential is gated on encryption instead, and the transport default is left alone.
When the server asks for &lt;code&gt;password&lt;/code&gt; or &lt;code&gt;md5&lt;/code&gt; authentication over a TCP connection with no TLS, the driver refuses before the credential is disclosed.
SCRAM is unaffected, since no password crosses the wire there.
Unix sockets are exempt.
&lt;code&gt;sslmode&lt;/code&gt; itself is untouched.&lt;/p&gt;
&lt;p&gt;The check can't live in &lt;code&gt;sslmode&lt;/code&gt;.
The &lt;em&gt;server&lt;/em&gt; chooses the authentication method, or an active interceptor answering in its place does.
The disclosure has to be judged where it happens rather than from the policy the client set at connect time.&lt;/p&gt;
&lt;p&gt;There's an opt-out, &lt;code&gt;allowUnencryptedPasswordAuth&lt;/code&gt;.
You'll need it for pre-14 servers using password authentication, since &lt;code&gt;password_encryption&lt;/code&gt; defaulted to &lt;code&gt;md5&lt;/code&gt; until then, and for PgBouncer.
The pgjdbc compat layer sets it by default.
A drop-in that changes your application's security behavior on a jar swap is the one thing a drop-in must never do.&lt;/p&gt;
&lt;p&gt;There's also &lt;code&gt;require_auth&lt;/code&gt; for accepting or refusing specific authentication methods, and &lt;code&gt;channelBinding=require&lt;/code&gt;.
Together those give a deployment real downgrade protection.&lt;/p&gt;
&lt;p&gt;These defaults are strictly for the core PostgreSQL-specific native driver.
The compatibility layer atop it applies the same defaults as pgjdbc.&lt;/p&gt;
&lt;h3 id="the-public-api-surface-is-a-file-that-the-build-enforces"&gt;The public API surface is a file that the build enforces&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;docs/api-surface.md&lt;/code&gt; enumerates every public type in the core module along with its stability tier.
A test reflects over the exported packages and fails the build if a public type isn't listed, if a listed type has vanished, or if an &lt;code&gt;@Experimental&lt;/code&gt; annotation and the table disagree.&lt;/p&gt;
&lt;p&gt;Adding a public type is therefore a deliberate act with a paper trail.
It doesn't happen just because some class needed to be visible to a sibling package.
The table is executable, not decorative.&lt;/p&gt;
&lt;h3 id="small-other-stuff"&gt;Small other stuff&lt;/h3&gt;
&lt;ul&gt;
&lt;li&gt;Zero runtime dependencies in the protocol module. It compiles against an empty classpath.&lt;/li&gt;
&lt;li&gt;The core module adds only the ongres SCRAM jars and the SLF4J API.&lt;/li&gt;
&lt;li&gt;&lt;code&gt;.pgpass&lt;/code&gt; support, including libpq's refusal to read a password file that's group or world readable. Reading it is opt-in via &lt;code&gt;passfile&lt;/code&gt; or &lt;code&gt;usepassfile&lt;/code&gt;, as a driver has no business reading files in your home directory unless you asked it to.&lt;/li&gt;
&lt;li&gt;The &lt;code&gt;PG*&lt;/code&gt; environment variables are honored natively and ignored by the compat layer, since pgjdbc never read them.&lt;/li&gt;
&lt;li&gt;GraalVM native-image metadata, with a smoke test that actually builds and runs a native binary so the metadata can't silently rot.&lt;/li&gt;
&lt;li&gt;Server errors surfaced with full fidelity: SQLSTATE, severity, detail, hint, position, schema, table, column, and constraint. Nothing gets flattened into a message string. Even the errors are PostgreSQL at its purest.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="performance"&gt;Performance&lt;/h2&gt;
&lt;p&gt;The benchmark harness is driver-agnostic and measures the driver, not the database.
The driver under test is supplied at runtime and never bundled.&lt;/p&gt;
&lt;p&gt;Against pgjdbc 42.7.13 on PostgreSQL 16, pg-java's native path runs at roughly 101-108% of pgjdbc's throughput across the whole workload sweep.
That's &lt;code&gt;select-1&lt;/code&gt;, &lt;code&gt;select-by-id&lt;/code&gt;, &lt;code&gt;select-rows&lt;/code&gt;, &lt;code&gt;prepared&lt;/code&gt;, &lt;code&gt;insert&lt;/code&gt;, &lt;code&gt;update&lt;/code&gt;, &lt;code&gt;returning&lt;/code&gt;, &lt;code&gt;types&lt;/code&gt;, &lt;code&gt;copy&lt;/code&gt;, and &lt;code&gt;mixed&lt;/code&gt;.
The array batch rewrite roughly doubles batch-insert throughput and edges past pgjdbc's own rewrite.&lt;/p&gt;
&lt;p&gt;Allocation is the axis I'd still call unfinished.
Some paths are at parity or better. On wide rows both the native and compat layers allocate 26% less than pgjdbc.
Others, the write side in particular, allocate substantially more.&lt;/p&gt;
&lt;p&gt;The numbers all live in &lt;code&gt;docs/benchmarks/&lt;/code&gt; as dated, append-only reports.
Fifteen macro runs so far, including the regressions and the negative results.
Run 011 measured a change that turned out to do nothing at all and it's written up anyway.
The point of keeping the record is that a failed idea doesn't get re-tried three weeks later.&lt;/p&gt;
&lt;h2 id="about-that-written-by-ai-part"&gt;About that "written by AI" part&lt;/h2&gt;
&lt;p&gt;Essentially the whole driver was written by Claude, in a loop, over about six weeks.
That's roughly 1,400 commits between June 19th and today: about 52,000 lines of main Java, 43,000 lines of tests, 22 architecture decision records, and 70 design documents.
The designs were manually reviewed and revised, but the code is Claude's on all but a handful of commits.&lt;/p&gt;
&lt;p&gt;I want to be precise about what that means.
"AI wrote it" is doing a lot of work in that sentence and it's not the interesting part.
The interesting part is the harness around it.&lt;/p&gt;
&lt;p&gt;None of which is to say nobody was driving.
What I spent six weeks doing was writing the contract and rejecting the wrong answers.
&lt;code&gt;AGENTS.md&lt;/code&gt; is twelve kilobytes of conventions, module rules, and definitions of done, and it is the single reason that a session with no memory of the previous one still lands code that fits the code already there.
The &lt;a href="https://martinfowler.com/bliki/ArchitectureDecisionRecord.html"&gt;Architecture Decision Records&lt;/a&gt; (ADRs) exist for the same reason.&lt;/p&gt;
&lt;p&gt;The calls that mattered most were about what &lt;em&gt;not&lt;/em&gt; to keep.
Routing the multi-row &lt;code&gt;VALUES&lt;/code&gt; batch chunks through the general pipelining API looked like obvious reuse, but it measured 61% of pgjdbc's throughput, so it lost to a batch-only primitive built on the same underlying engine (ADR-0021).
A security audit flagged the &lt;code&gt;sslmode=prefer&lt;/code&gt; default and proposed flipping the transport to &lt;code&gt;verify-full&lt;/code&gt;, which I overruled, because the finding was really about the credential and the credential is what should be gated (ADR-0009).
Both of those are in the repo with the reasoning attached, which is the part I'd defend.
The decisions are auditable rather than vibes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;There is a plan, and the plan is the unit of work.&lt;/strong&gt;
&lt;code&gt;docs/plans/overall.md&lt;/code&gt; is a phased, numbered implementation plan where each item (&lt;code&gt;C8.4&lt;/code&gt;, &lt;code&gt;N5.7&lt;/code&gt;, &lt;code&gt;P4&lt;/code&gt;) is intended to be exactly one commit.
The loop picks up the next item, implements it, and ticks the checkbox in a separate commit.
Anything deferred goes into &lt;code&gt;docs/follow-up.md&lt;/code&gt; with the files, the line hints, and the reason, so that a fresh session with no memory of the conversation can pick it up later.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Decisions get written down before they're implemented.&lt;/strong&gt;
Twenty-two ADRs cover the I/O model, module boundaries, the result model, exceptions, TLS posture, secret handling, both batch rewrites, pipelining, and API stability.
The ADR index carries two separate columns, &lt;code&gt;Status&lt;/code&gt; and &lt;code&gt;Built&lt;/code&gt;, because an accepted decision is not a claim that the code does it yet.
An agent that would otherwise re-litigate async versus blocking every third session instead reads ADR-0001 and gets on with it.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;The build is the fitness function.&lt;/strong&gt;
&lt;code&gt;mvn verify&lt;/code&gt; fails on Spotless formatting, a 7-bit-ASCII policy over all source and docs, a module-layering test, a JaCoCo floor, dependency convergence, and the API surface manifest described above.
None of those are things a model reliably remembers.
All of them are things a build can enforce every single time.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Regression testing is most of the work.&lt;/strong&gt;
There is a full suite of unit tests that run entirely in memory, and an equally wide suite of integration tests on top of that.
The unit tests need no Docker at all.
There's a &lt;code&gt;MockServer&lt;/code&gt; that lets a test drive real protocol byte sequences, so most protocol behavior is testable without a server anywhere in the picture.
The integration tests run under Testcontainers across a matrix of server versions (&lt;em&gt;9.1 through 18 are supported and 14 through 18 gate every change&lt;/em&gt;), plus dedicated harnesses for TLS, PgBouncer, authentication methods, and Unix sockets.&lt;/p&gt;
&lt;p&gt;Baking the container startup into the test suite greatly simplifies the agentic dev loop as targeting a different PostgreSQL server version is a java system property (&lt;code&gt;-Dpg.it.image=...&lt;/code&gt;).
This allows automated testing of multiple versions in a simple loop.&lt;/p&gt;
&lt;p&gt;And then the part I'd recommend to anybody doing this: &lt;strong&gt;run somebody else's test suite.&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;&lt;code&gt;compat-suites/&lt;/code&gt; runs pgjdbc's own upstream test suite, and Hibernate's, against pg-java's compat layer in Docker, and gates on committed baselines.
As of the current baseline, 6,851 of pgjdbc's 7,331 test cases pass against a driver that shares no code with it.&lt;/p&gt;
&lt;p&gt;Of the remaining tests, they're not all true failures either.
The vast majority are tests that cover purely internal aspects of pgjdbc.
The compatibility suite does cover some of them, but the primary focus is the public surface area of the driver and matching the behavior of pgjdbc.&lt;/p&gt;
&lt;p&gt;A new failure fails the gate.
An improvement prints a nudge to refresh the baseline so the win gets locked in.
Nothing an agent writes about its own code is worth as much as several thousand tests written by people who were trying to pin down a &lt;em&gt;different&lt;/em&gt; implementation's behavior.&lt;/p&gt;
&lt;p&gt;So does it work?
Mostly, and the failure modes are also interesting.&lt;/p&gt;
&lt;p&gt;The loop is very good at breadth.
Every codec, every metadata method, every negative TLS case.
It's also good at the kind of grinding consistency that humans are bad at, which is most of what a driver's test suite is.&lt;/p&gt;
&lt;p&gt;It is prone to declaring victory.
That's why the gates and the baselines exist, and why the contributor doc has a rule that says to report honestly and state plainly when a step was skipped.&lt;/p&gt;
&lt;h2 id="a-long-time-coming"&gt;A long time coming&lt;/h2&gt;
&lt;p&gt;What's surreal about this entire development process is that both the plan and the result are what we had discussed almost eight years ago:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Date: Thu, 25 Oct 2018 10:42:13 -0400
Subject: Re: Rewriting the driver
From: Sehrope Sarkuni &amp;lt;sehrope@jackdb.com&amp;gt;

Regarding the overall idea, I&amp;#39;m all for a true &amp;quot;next gen&amp;quot; driver though I&amp;#39;d
hope that it&amp;#39;ll be something that allows for a more PostgreSQL-specific
backend.

The JDBC layer should be atop of that, not the other way around. For
example, processing rows one by one to limit memory usage shouldn&amp;#39;t require
obscure combos of opening transactions and setting fetch sizes.
Notifications and arbitrary response handling should be baked in from the
beginning (ex: I should be able to run &amp;quot;SELECT 1&amp;quot; or &amp;quot;COPY (SELECT 1) TO
STDOUT&amp;quot; without knowing anything in advance about the response type of the
SQL command) .

[ ... truncated ... ]

Two points on actual development of this. It&amp;#39;s my experience that things
like don&amp;#39;t come together from group discussion. Someone (Dave? :D) needs to
create the &amp;quot;core&amp;quot; that others can then join onto. It&amp;#39;s no small undertaking
but it&amp;#39;s how all projects of this size come about.

Secondly, and IMHO more importantly, there needs to be a decision from the
beginning of whether backwards compatibility is going to be a design goal.
I&amp;#39;d vote &amp;quot;no&amp;quot; on that. This would be a great chance to clean up the driver
properties and drop any excess baggage. As an example, there could be a
unified track for configuring SSL.

If backwards compatibility is desired, at the very least the core should be
able to stand on its own with any compatibility layer being handle in the
JDBC component.

[ ... truncated ... ]

Regards,
-- Sehrope Sarkuni
Founder &amp;amp; CEO | JackDB, Inc. | https://www.jackdb.com/
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Read against what shipped, it's very nearly a checklist.&lt;/p&gt;
&lt;p&gt;Processing rows one by one without the obscure combo of an open transaction and a fetch size is the pull cursor.
"The core should be able to stand on its own with any compatibility layer being handled in the JDBC component" is the module split, almost word for word.
The unified track for configuring SSL is &lt;code&gt;sslmode&lt;/code&gt;.
Arbitrary response handling landed too: the native API hands you a result stream without needing to know in advance what the command returns.&lt;/p&gt;
&lt;p&gt;The one line I keep coming back to is the other one.
Someone needs to create the core that others can join onto, and it's no small undertaking.
That was true in 2018, and it's why the idea sat for eight years.&lt;/p&gt;
&lt;p&gt;It turns out the answer to "&lt;em&gt;who is going to write it&lt;/em&gt;" was a model in a loop for six weeks.&lt;/p&gt;
&lt;h2 id="status-and-whats-next"&gt;Status and what's next&lt;/h2&gt;
&lt;p&gt;Where it stands today:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Pre-release.&lt;/strong&gt; Nothing is published to Maven Central yet.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;No compatibility guarantees.&lt;/strong&gt; The API is still moving and will keep moving for a while.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;The compat layer is a migration aid, not a certified drop-in.&lt;/strong&gt; Some &lt;code&gt;PGConnection&lt;/code&gt; methods are still stubs.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Two accepted ADRs are unbuilt.&lt;/strong&gt; JSON value binding and composite-to-record mapping are decided designs that nobody has written yet.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;What already works:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Connects over TCP, TLS, and Unix domain sockets.&lt;/li&gt;
&lt;li&gt;Authenticates with SCRAM-SHA-256, including channel binding.&lt;/li&gt;
&lt;li&gt;Simple and extended query protocols, with a server-side statement cache.&lt;/li&gt;
&lt;li&gt;COPY in and out, LISTEN/NOTIFY, and large objects.&lt;/li&gt;
&lt;li&gt;Pipelining, and batches with either rewrite strategy.&lt;/li&gt;
&lt;li&gt;A JDBC layer with &lt;code&gt;DataSource&lt;/code&gt; and XA sitting on top of all of it.&lt;/li&gt;
&lt;li&gt;Passes the vast majority of the pgjdbc test suite&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To build it you need Java 21, plus Docker if you want to run the integration tests:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/pgjdbc/pg-java.git
&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;pg-java
./mvnw&lt;span class="w"&gt; &lt;/span&gt;clean&lt;span class="w"&gt; &lt;/span&gt;install
./mvnw&lt;span class="w"&gt; &lt;/span&gt;verify&lt;span class="w"&gt; &lt;/span&gt;-Pintegration-tests&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="c1"&gt;# needs a Docker daemon&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;It's released under the &lt;a href="https://opensource.org/license/postgresql"&gt;PostgreSQL License&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;If you try it, I want to hear what breaks.
That goes double for anyone with an application weird enough to have found the corners of pgjdbc, because those corners are exactly what a new driver gets wrong.&lt;/p&gt;
&lt;p&gt;Open an issue on &lt;a href="https://github.com/pgjdbc/pg-java"&gt;GitHub&lt;/a&gt; or &lt;a href="mailto:sehrope@jackdb.com"&gt;email me&lt;/a&gt;.
If you think you've found a security issue, please use GitHub's private vulnerability reporting instead of a public issue.&lt;/p&gt;
&lt;p&gt;Getting the driver this far has been the most interesting open source project I've worked on in years.
The next part, where other people start using it and telling me where it's wrong, is the part that actually makes it a driver.&lt;/p&gt;</content><category term="pg-planet"></category><category term="java"></category><category term="jdbc"></category><category term="postgresql"></category><category term="databases"></category><category term="pg-java"></category><category term="performance"></category><category term="ai"></category></entry><entry><title>Postgres password encryption without leaking credentials</title><link href="https://launchbylunch.com/posts/2024/Jan/16/postgres-password-encryption/" rel="alternate"></link><published>2024-01-16T00:00:00-05:00</published><updated>2024-01-16T00:00:00-05:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2024-01-16:/posts/2024/Jan/16/postgres-password-encryption/</id><summary type="html">&lt;p&gt;How to encrypt PostgreSQL passwords for CREATE and ALTER USER commands without leaking credentials in plaintext.&lt;/p&gt;</summary><content type="html">&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#password-encryption-ie-hashing"&gt;Password Encryption (i.e. "hashing")&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#postgresql-password-encryption-md5"&gt;PostgreSQL Password Encryption - MD5&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#postgresql-password-encryption-scram-sha-256"&gt;PostgreSQL Password Encryption - SCRAM-SHA-256&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#create-user-alter-user"&gt;CREATE USER / ALTER USER&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#libraries"&gt;Libraries&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#c-libpq"&gt;C - libpq&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#java-postgresql-jdbc-driver-pgjdbc"&gt;Java - PostgreSQL JDBC Driver (PGJDBC)&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#nodejs-pg-password-util"&gt;node.js - pg-password-util&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#python-psycopgs-encrypt_password"&gt;python - psycopg's encrypt_password&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#password-generation"&gt;Password Generation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#final-thoughts"&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;"Dance like no one is watching. Encrypt like everyone is."&lt;/p&gt;
&lt;p&gt;-- Adrian Lamo&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;The simplest way to create a user to access a PostgreSQL database is something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;t0pSecret!&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;However, the text of this SQL will be sent to the remote database server for execution.
That means the &lt;em&gt;plaintext&lt;/em&gt; of the password is itself sent to the database.&lt;/p&gt;
&lt;p&gt;If statement logging is enabled the following will appear in server logs:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="mf"&gt;2024&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;01&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mf"&gt;14&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mf"&gt;19&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;09&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="mf"&gt;09.064&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;UTC&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;[&lt;/span&gt;&lt;span class="mf"&gt;75&lt;/span&gt;&lt;span class="err"&gt;]&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;LOG&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;statement&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;app&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;&amp;#39;&lt;/span&gt;&lt;span class="n"&gt;t0pSecret&lt;/span&gt;&lt;span class="err"&gt;!&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If there's a pool or proxy in between you and the database server, it could end up in &lt;em&gt;those&lt;/em&gt; logs as well.&lt;/p&gt;
&lt;p&gt;We can do better. Much better!&lt;/p&gt;
&lt;h2 id="password-encryption-ie-hashing"&gt;Password Encryption (i.e. "hashing")&lt;/h2&gt;
&lt;p&gt;The PostgreSQL docs refer this topic as "password encryption".
In reality, the operation that is being performed is form of &lt;a href="https://en.wikipedia.org/wiki/Cryptographic_hash_function"&gt;hashing&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/HMAC"&gt;HMAC&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The server does not need to know the exact text of your database user's password.
It just needs to know if your supplied credentials match the ones that are authorized to access that user.&lt;/p&gt;
&lt;p&gt;Password hashing is one way to perform such a calculation and allows the server to only store the result of that one-way hash function.
The server can then check if the result of applying the password hashing operation for the credentials supplied in your connection attempt matches the expected password hash.&lt;/p&gt;
&lt;h2 id="postgresql-password-encryption-md5"&gt;PostgreSQL Password Encryption - MD5&lt;/h2&gt;
&lt;p&gt;Up until PostgreSQL v10, there was only one encryption option built-in to PostgreSQL for dealing with passwords: &lt;code&gt;md5&lt;/code&gt;&lt;/p&gt;
&lt;p&gt;With &lt;code&gt;md5&lt;/code&gt; password encryption, the server stores &lt;code&gt;MD5(password || username)&lt;/code&gt; in its internal &lt;code&gt;pg_shadow&lt;/code&gt; catalog (think &lt;a href="https://en.wikipedia.org/wiki/Passwd#Shadow_file"&gt;&lt;code&gt;/etc/shadow&lt;/code&gt;&lt;/a&gt; but for database passwords).&lt;/p&gt;
&lt;p&gt;&lt;small&gt;&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The hashing operations described in this post use the following expressions:&lt;/p&gt;
&lt;p&gt;&lt;code&gt;MD5( ...)&lt;/code&gt; = result of &lt;code&gt;md5&lt;/code&gt; hash function of its one and only argument&lt;/p&gt;
&lt;p&gt;&lt;code&gt;||&lt;/code&gt; = binary operation to concatenate the raw bytes of its arguments&lt;/p&gt;
&lt;p&gt;So &lt;code&gt;'foo' || 'bar' = 'foobar'&lt;/code&gt; and &lt;code&gt;MD5('test') = '098f6bcd4621d373cade4e832627b4f6'&lt;/code&gt;
&lt;/small&gt;&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;During the authentication step of the PostgreSQL wire protocol, the server picks a random &lt;a href="https://en.wikipedia.org/wiki/Salt_(cryptography)"&gt;salt&lt;/a&gt; and asks the authenticating client to calculate: &lt;code&gt;MD5( MD5(password || username) || salt))&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The random salt prevents the value from a previous connection from being re-used for a later connection attempt (also known as a &lt;a href="https://en.wikipedia.org/wiki/Replay_attack"&gt;replay attack&lt;/a&gt;). Unfortunately, the size of the salt in the wire protocol is only four-bytes so it's not particularly effective. With enough connection attempts, assuming there is no transport level encryption (TLS), then an eavesdropper could collect every possible authentication response.&lt;/p&gt;
&lt;h2 id="postgresql-password-encryption-scram-sha-256"&gt;PostgreSQL Password Encryption - SCRAM-SHA-256&lt;/h2&gt;
&lt;p&gt;Starting in PostgreSQL v10, a new option was added and made the default for handling passwords: SCRAM-SHA-256&lt;/p&gt;
&lt;p&gt;SCRAM is an acronym for &lt;a href="https://en.wikipedia.org/wiki/Salted_Challenge_Response_Authentication_Mechanism"&gt;Salted Challenge Response Authentication Mechanism&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;It can be used with various hashing algorithms.
The PostgreSQL implementation supports only &lt;a href="https://en.wikipedia.org/wiki/SHA-2"&gt;SHA-256&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;SCRAM has a number of advantages over md5 encryption:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;A modern hash function (SHA-256)&lt;/li&gt;
&lt;li&gt;Larger salt sizes (defaulting to 16-bytes)&lt;/li&gt;
&lt;li&gt;&lt;a href="https://en.wikipedia.org/wiki/Key_stretching"&gt;key stretching&lt;/a&gt; (defaulting to 4096 rounds)&lt;/li&gt;
&lt;li&gt;Proving to the client that the server knows the user's credentials (the client authenticates the server too!)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;&lt;strong&gt;With the addition of SCRAM-SHA-256, there is no reason to ever use md5 authentication on a modern PostgreSQL server.&lt;/strong&gt;&lt;/p&gt;
&lt;h2 id="create-user-alter-user"&gt;CREATE USER / ALTER USER&lt;/h2&gt;
&lt;p&gt;The PostgreSQL SQL commands for creating a user, &lt;code&gt;CREATE USER ...&lt;/code&gt;, accepts a &lt;code&gt;... PASSWORD 'literal-value-goes-here'&lt;/code&gt; clause for specifying the password.
The &lt;code&gt;ALTER USER ...&lt;/code&gt; command operates similarly.&lt;/p&gt;
&lt;p&gt;The server checks that new password value and decides if it is already encrypted.
If not, the server encrypts it using the connection's default encryption method (&lt;code&gt;SHOW password_encryption&lt;/code&gt; to see the default).&lt;/p&gt;
&lt;p&gt;If the value of the new password starts with &lt;code&gt;md5&lt;/code&gt; then the server assumes it is an already encrypted password using the md5 scheme.&lt;/p&gt;
&lt;p&gt;If the value of the new password starts with &lt;code&gt;SCRAM-SHA-256$&lt;/code&gt; then the server assumes it is an already encrypted password using the SCRAM-SHA-256 scheme.&lt;/p&gt;
&lt;p&gt;So the following would instruct the PostgreSQL server to encrypt the password using the connections default password encryption:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;abcd&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Whereas this would be treated as an already encrypted md5 password:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;md5fb592cb4152e2aacaaf452714d283f7e&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;And this would be treated as an already encrypted SCRAM-SHA-256 password:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCRAM-SHA-256$4096:jHhJMplyRcr1io3v3YwabQ==$WNwkp8PFet/L8UUtFBEa7Sn8xSofpPZ4klcfuB0w6Yk=:Oqnfxd3FhyYTALxW+YeU/yVAzxKT+ho08E+hE/&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;In all three cases, the user will be able to log in with the same password, &lt;code&gt;abcd&lt;/code&gt;, however in the first case the user's password may have been compromised by being logged during the command execution.&lt;/p&gt;
&lt;p&gt;The same update could be done via an &lt;code&gt;ALTER USER ...&lt;/code&gt; command:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;-- Have the server encrypt the password ... BAD&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;abcd&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;-- Provide an md5 encrypted password ... BETTER (but not great)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;md5fb592cb4152e2aacaaf452714d283f7e&amp;#39;&lt;/span&gt;

&lt;span class="c1"&gt;-- Provide a SCRAM-SHA-256 encrypted password ... BEST (really this is only one you should be using)&lt;/span&gt;
&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCRAM-SHA-256$4096:jHhJMplyRcr1io3v3YwabQ==$WNwkp8PFet/L8UUtFBEa7Sn8xSofpPZ4klcfuB0w6Yk=:Oqnfxd3FhyYTALxW+YeU/yVAzxKT+ho08E+hE/&amp;#39;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;There's no reason to present the plaintext password to the server.
The server does not even save the plaintext of the password.
It immediately encrypts it (using the current &lt;code&gt;password_encryption&lt;/code&gt; option) and saves the encrypted value:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;USER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;alice&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PASSWORD&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;abcd&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;ALTER&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ROLE&lt;/span&gt;

&lt;span class="n"&gt;postgres&lt;/span&gt;&lt;span class="o"&gt;=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;passwd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pg_shadow&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;usename&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="w"&gt;                                                                &lt;/span&gt;&lt;span class="n"&gt;passwd&lt;/span&gt;&lt;span class="w"&gt;                                                                 &lt;/span&gt;
&lt;span class="c1"&gt;---------------------------------------------------------------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;SCRAM&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="n"&gt;SHA&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;256&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="mi"&gt;4096&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;awluLTEXFg1qnYqxpmRNkQ&lt;/span&gt;&lt;span class="o"&gt;==&lt;/span&gt;&lt;span class="err"&gt;$&lt;/span&gt;&lt;span class="n"&gt;FPHOxeG4sUCa&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;vy6W83VMHaXr3rSRq2QVfkvmAMad2A&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="n"&gt;g9oXk&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="n"&gt;UT6fF&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="n"&gt;N8z5aFIQpRvLfm8Yz13ZEjdhrWhY2&lt;/span&gt;&lt;span class="o"&gt;/&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At best the password is not leaked to some log file.
But you'll never really know, right?&lt;/p&gt;
&lt;h2 id="libraries"&gt;Libraries&lt;/h2&gt;
&lt;p&gt;As creating users and updating their passwords is a common enough task, it's been added to a number of PostgreSQL libraries.
The helpers in those libraries handle encrypting the password so that it can be used in SQL commands without exposing its plaintext.&lt;/p&gt;
&lt;h3 id="c-libpq"&gt;C - libpq&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.postgresql.org/docs/current/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORDCONN"&gt;https://www.postgresql.org/docs/current/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORDCONN&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;The &lt;code&gt;libpq&lt;/code&gt; C library is part of core PostgreSQL and is the basis for a number of user tools and other drivers.
It provides a function, &lt;a href="https://www.postgresql.org/docs/current/libpq-misc.html#LIBPQ-PQENCRYPTPASSWORDCONN"&gt;&lt;code&gt;PQencryptPasswordConn(...)&lt;/code&gt;&lt;/a&gt; that will encrypt a password using the defaults for the provided connection.&lt;/p&gt;
&lt;h3 id="java-postgresql-jdbc-driver-pgjdbc"&gt;Java - PostgreSQL JDBC Driver (PGJDBC)&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://github.com/pgjdbc/pgjdbc/blob/master/pgjdbc/src/main/java/org/postgresql/util/PasswordUtil.java"&gt;https://github.com/pgjdbc/pgjdbc/blob/master/pgjdbc/src/main/java/org/postgresql/util/PasswordUtil.java&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;A helper class, &lt;a href="https://github.com/pgjdbc/pgjdbc/blob/master/pgjdbc/src/main/java/org/postgresql/util/PasswordUtil.java"&gt;&lt;code&gt;PasswordUtil&lt;/code&gt;&lt;/a&gt; was recently added to the PostgreSQL Java database driver, PGJDBC.
The methods in that class allow for fine grained control of the encryption type and internal parameters (e.g. number of SCRAM iterations or salt size).
There are also methods for generating the &lt;code&gt;ALTER USER ...&lt;/code&gt; SQL command for external execution or inclusion in a script.&lt;/p&gt;
&lt;p&gt;A new helper method was also added &lt;code&gt;PGConnection&lt;/code&gt; that performs a password change operation for a user using the default encryption setting of the database server.
This is simplest method for updating a user's password without leaking it along the way.&lt;/p&gt;
&lt;p&gt;Only &lt;code&gt;ALTER USER ... PASSWORD ...&lt;/code&gt; is supported as creating a user has many additional options and could change over time.&lt;/p&gt;
&lt;p&gt;To use it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;alice&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;String&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newPassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s"&gt;&amp;quot;my-new-secret-password&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;PGConnection&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;pgConn&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;unwrap&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;PGConnection&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;class&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;pgConn&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;alterUserPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;newPassword&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="na"&gt;toCharArray&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When creating users, the recommended approach is to issue the &lt;code&gt;CREATE USER ...&lt;/code&gt; operation first without a password and then follow up to invoke the new &lt;code&gt;PGConnection.alterUserPassword(...)&lt;/code&gt; method.&lt;/p&gt;
&lt;p&gt;These new helpers will be available in the next release of the PGJDBC driver.&lt;/p&gt;
&lt;h3 id="nodejs-pg-password-util"&gt;node.js - pg-password-util&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.npmjs.com/package/pg-password-util"&gt;https://www.npmjs.com/package/pg-password-util&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;On node.js, you can use &lt;a href="https://www.npmjs.com/package/pg-password-util"&gt;&lt;code&gt;pg-password-util&lt;/code&gt;&lt;/a&gt;.
This is an NPM module (written by me) that encrypts the password client side and has helpers for updating an existing user's password.&lt;/p&gt;
&lt;p&gt;It's designed to work with &lt;a href="https://www.npmjs.com/package/pg"&gt;&lt;code&gt;pg&lt;/code&gt;&lt;/a&gt; the most popular PostgreSQL driver for node.js.
To update a user's password:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;alterUserPassword&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;pg-password-util&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// client is a pg.Client&lt;/span&gt;
&lt;span class="k"&gt;await&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nx"&gt;alterUserPassword&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;app&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nx"&gt;password&lt;/span&gt;&lt;span class="o"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;my-new-secret-password&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Using this helper the plaintext password is never sent over the wire and will not appear in any server logs.&lt;/p&gt;
&lt;h3 id="python-psycopgs-encrypt_password"&gt;python - psycopg's encrypt_password&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://www.psycopg.org/psycopg3/docs/api/pq.html#psycopg.pq.PGconn.encrypt_password"&gt;https://www.psycopg.org/psycopg3/docs/api/pq.html#psycopg.pq.PGconn.encrypt_password&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Python's psycopg driver is a wrapper for libpq (the PostgreSQL C library). It includes a python function wrapping the  &lt;code&gt;PQencryptPasswordConn(...)&lt;/code&gt; C function mentioned above:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;enc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encoding&lt;/span&gt;
&lt;span class="o"&gt;&amp;gt;&amp;gt;&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;encrypted&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;conn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;pgconn&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encrypt_password&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;password&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;rolename&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;encode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;enc&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="sa"&gt;b&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;SCRAM-SHA-256$4096:...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The return value is a string that can be included in an &lt;code&gt;ALTER USER ... PASSWORD ...&lt;/code&gt; statement.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Thanks to &lt;a href="http://www.file-away.co.uk/"&gt;Robert Ladyman&lt;/a&gt; for suggesting this Python library.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="password-generation"&gt;Password Generation&lt;/h2&gt;
&lt;p&gt;In case it was not obvious, the &lt;code&gt;abcd&lt;/code&gt; password above is a &lt;em&gt;terrible&lt;/em&gt; password.
Real passwords, particularly for service accounts like application database users, should be long and cryptographically random.&lt;/p&gt;
&lt;p&gt;On most *nix environments you can generate one using:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;tr -d -c a-zA-Z0-9 &amp;lt;/dev/urandom | head -c 43
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;That will generate a random 43-character password. Each character has 62 choices (a-z, A-Z, or 0-9). That's about 5.954 bits of entropy per character (ln(62) / ln2). With 43-characters, that's a little more than 256-bits entropy in total.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;There are plenty of alternatives to passwords. (&lt;em&gt;I know at least one person who, upon reading this, is already mentally drafting an email to me that everybody should be using Kerberos...&lt;/em&gt;) But if you are going to use them, they should not be exposed.&lt;/p&gt;
&lt;p&gt;Using the helpers listed above, you could:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Generate a new long random password.&lt;/li&gt;
&lt;li&gt;Save the password to a secure secrets store.&lt;/li&gt;
&lt;li&gt;Update the database user with the new credentials.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;All without persisting the value in plaintext anywhere along the way.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Do you have another library that should be included in this post or want to expand on this further? &lt;a href="mailto:sehrope@jackdb.com"&gt;Let me know&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;</content><category term="pg-planet"></category><category term="sql"></category><category term="databases"></category><category term="postgresql"></category><category term="password"></category><category term="encryption"></category><category term="hashing"></category></entry><entry><title>"ANSI? Schmansi!" at PostgresConf 2018</title><link href="https://launchbylunch.com/posts/2018/Apr/23/ansi-schmansi-postgresconf-2018-slides/" rel="alternate"></link><published>2018-04-23T00:00:00-04:00</published><updated>2018-04-23T00:00:00-04:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2018-04-23:/posts/2018/Apr/23/ansi-schmansi-postgresconf-2018-slides/</id><summary type="html">&lt;p&gt;Summary and slides from my talk at PostgresConf 2018&lt;/p&gt;</summary><content type="html">&lt;p&gt;I gave a talk last week at &lt;a href="https://postgresconf.org/conferences/2018"&gt;PostgresConf US 2018&lt;/a&gt; titled "&lt;em&gt;ANSI, Schmansi! How I learned to stop worrying and love Postgres-isms&lt;/em&gt;". The slides for the talk can be viewed online here:&lt;/p&gt;
&lt;p&gt;&lt;a href="https://sehrope.github.io/postgres-conf-2018-ansi-schmansi/#/"&gt;https://sehrope.github.io/postgres-conf-2018-ansi-schmansi/#/&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;This talk is about using the full potential of your choice of database with concrete examples of Postgres-specific features. The audience was wonderful and I look forward to more feedback on the topic.&lt;/p&gt;
&lt;p&gt;The slides were built with a customized reveal.js template for writing in &lt;a href="https://pugjs.org/api/getting-started.html"&gt;pug&lt;/a&gt;. This allows for most of the example SQL to be housed in separate files (in the &lt;code&gt;sql/&lt;/code&gt; directory) and referenced from the templates. The source for the slides including the example SQL is available &lt;a href="https://github.com/sehrope/postgres-conf-2018-ansi-schmansi"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Updated 2018-04-26:&lt;/em&gt; There's no official video from the conference but a member of the audience recorded a video of the talk and posted it on YouTube. You can &lt;a href="https://www.youtube.com/watch?v=lOzUHAggFP0"&gt;watch it here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Do you think embracing database specific behavior is a great idea? &lt;a href="mailto:sehrope@jackdb.com"&gt;Let me know&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;</content><category term="pg-planet"></category><category term="sql"></category><category term="databases"></category><category term="postgresql"></category></entry><entry><title>How I Write SQL, Part 1: Naming Conventions</title><link href="https://launchbylunch.com/posts/2014/Feb/16/sql-naming-conventions/" rel="alternate"></link><published>2014-02-16T00:00:00-05:00</published><updated>2014-02-16T00:00:00-05:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2014-02-16:/posts/2014/Feb/16/sql-naming-conventions/</id><summary type="html">&lt;p&gt;Naming conventions for SQL tables, views, columns and more.&lt;/p&gt;</summary><content type="html">&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#target-audience"&gt;Target Audience&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-naming-conventions-are-important"&gt;Why Naming Conventions Are Important&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#names-are-long-lived"&gt;Names Are Long Lived&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#names-are-contracts"&gt;Names Are Contracts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#developer-context-switching"&gt;Developer Context Switching&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#naming-conventions"&gt;Naming Conventions&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#singular-relations"&gt;Singular Relations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#key-fields"&gt;Key Fields&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#primary-keys"&gt;Primary Keys&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#foreign-keys"&gt;Foreign Keys&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#prefixes-and-suffixes-are-bad"&gt;Prefixes and Suffixes (are bad)&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#relation-type-prefixes"&gt;Relation Type Prefixes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#application-name-prefixes"&gt;Application Name Prefixes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#data-type-suffixes"&gt;Data Type Suffixes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#explicit-naming"&gt;Explicit Naming&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#indexes"&gt;Indexes&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#constraints"&gt;Constraints&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#final-thoughts"&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;blockquote&gt;
&lt;p&gt;"There are only two hard problems in Computer Science: cache invalidation and naming things."&lt;/p&gt;
&lt;p&gt;-- Phil Karlton&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;In this post I'll be going into the latter. Specifically, I'll describe naming conventions for database objects, why they are so important, and what you should and shouldn't be doing.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Warning!&lt;/strong&gt; &lt;em&gt;This is a fairly opinionated post and I welcome feedback from people suggesting alternatives.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="target-audience"&gt;Target Audience&lt;/h2&gt;
&lt;p&gt;Our company, &lt;strong&gt;&lt;a href="https://www.jackdb.com/"&gt;JackDB&lt;/a&gt;&lt;/strong&gt;, uses PostgreSQL internally to store persistent state and the naming conventions in this post were written with PostgreSQL in mind. Most of the recommendations should be equally valid for other relational databases such as MySQL, Oracle, or Microsoft SQL Server.&lt;/p&gt;
&lt;p&gt;A lot of them will also apply to NoSQL databases, though not everything. For example, the suggestion below to use full english words goes against the &lt;a href="http://docs.mongodb.org/manual/faq/developers/#how-do-i-optimize-storage-use-for-small-documents"&gt;recommended approach&lt;/a&gt; for naming fields in MongoDB. When in doubt, find a guide for your specific database type.&lt;/p&gt;
&lt;h2 id="why-naming-conventions-are-important"&gt;Why Naming Conventions Are Important&lt;/h2&gt;
&lt;h3 id="names-are-long-lived"&gt;Names Are Long Lived&lt;/h3&gt;
&lt;p&gt;Data structures are meant to last much longer than application code. Anyone that has worked on a long running system can attest to that.&lt;/p&gt;
&lt;p&gt;Well defined data structures and table layouts will outlive any application code. It's not uncommon to see an application completely rewritten without any changes done to its database schema.&lt;/p&gt;
&lt;h3 id="names-are-contracts"&gt;Names Are Contracts&lt;/h3&gt;
&lt;p&gt;Database objects are referenced by their names, thus object names are part of the contract for an object. In a way you can consider your database table and column names to be the API to your data model.&lt;/p&gt;
&lt;p&gt;Once they are set, changing them may break dependent applications. This is all the more reason to name things properly before the first use.&lt;/p&gt;
&lt;h3 id="developer-context-switching"&gt;Developer Context Switching&lt;/h3&gt;
&lt;p&gt;Having consistent naming conventions across your data model means that developers will need to spend less time looking up the names of tables, views, and columns. Writing and debugging SQL is easier when you know that &lt;code&gt;person_id&lt;/code&gt; must be a foreign key to the &lt;code&gt;id&lt;/code&gt; field of the &lt;code&gt;person&lt;/code&gt; table.&lt;/p&gt;
&lt;h2 id="naming-conventions"&gt;Naming Conventions&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Avoid quotes&lt;/strong&gt;. If you have to quote an identifier then you should rename it. Quoted identifiers are a serious pain. Writing SQL by hand using quoted identifiers is frustrating and writing dynamic SQL that involves quoted identifiers is even more frustrating.&lt;/p&gt;
&lt;p&gt;This also means that you should never include whitespace in identifier names.&lt;/p&gt;
&lt;p&gt;Ex: Avoid using names like &lt;code&gt;"FirstName"&lt;/code&gt; or &lt;code&gt;"All Employees"&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Lowercase&lt;/strong&gt;. Identifiers should be written entirely in lower case. This includes tables, views, column, and everything else too. Mixed case identifier names means that every usage of the identifier will need to be quoted in double quotes (&lt;em&gt;which we already said are not allowed&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Ex: Use &lt;code&gt;first_name&lt;/code&gt;, not &lt;code&gt;"First_Name"&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Data types are not names&lt;/strong&gt;. Database object names, particularly column names, should be a noun describing the field or object. Avoid using words that are just data types such as &lt;code&gt;text&lt;/code&gt; or &lt;code&gt;timestamp&lt;/code&gt;. The latter is particularly bad as it provides zero context.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Underscores separate words&lt;/strong&gt;. Object name that are comprised of multiple words should be separated by underscores (&lt;em&gt;ie. &lt;a href="https://en.wikipedia.org/wiki/Snake_case"&gt;snake case&lt;/a&gt;&lt;/em&gt;). &lt;/p&gt;
&lt;p&gt;Ex: Use &lt;code&gt;word_count&lt;/code&gt; or &lt;code&gt;team_member_id&lt;/code&gt;, not &lt;code&gt;wordcount&lt;/code&gt; or &lt;code&gt;wordCount&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Full words, not abbreviations&lt;/strong&gt;. Object names should be full English words. In general avoid abbreviations, especially if they're just the type that removes vowels. Most SQL databases support at least 30-character names which should be more than enough for a couple English words. PostgreSQL supports up to &lt;a href="http://www.postgresql.org/docs/current/interactive/sql-syntax-lexical.html#SQL-SYNTAX-IDENTIFIERS"&gt;63-character&lt;/a&gt; for identifiers.&lt;/p&gt;
&lt;p&gt;Ex: Use &lt;code&gt;middle_name&lt;/code&gt;, not &lt;code&gt;mid_nm&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Use common abbreviations&lt;/strong&gt;. For a few long words the abbreviation is both more common than the word itself. &lt;a href="https://en.wikipedia.org/wiki/I18n"&gt;"Internationalization" and "localization"&lt;/a&gt; are the two that come up most often as &lt;code&gt;i18n&lt;/code&gt; and &lt;code&gt;l10n&lt;/code&gt; respectively. In these cases use the abbreviation.&lt;/p&gt;
&lt;p&gt;If you're in doubt, use the full English word. It should be obvious where the abbreviation makes sense.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Avoid reserved words&lt;/strong&gt;. Avoid using any word that is considered a reserved word in the database that you are using. There aren't that many of them so it's not too much effort to pick a different word. Depending on the context, reserved words may require quoting. This means sometimes you'll write &lt;code&gt;"user"&lt;/code&gt; and sometimes just &lt;code&gt;user&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Another benefit of avoiding reserved words is that less-than-intelligent editor syntax highlighting won't erroneously highlight them.&lt;/p&gt;
&lt;p&gt;Ex: Avoid using words like &lt;code&gt;user&lt;/code&gt;, &lt;code&gt;lock&lt;/code&gt;, or &lt;code&gt;table&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here are the list of reserved words for &lt;a href="http://www.postgresql.org/docs/9.3/static/sql-keywords-appendix.html"&gt;PostgreSQL&lt;/a&gt;, &lt;a href="http://dev.mysql.com/doc/refman/5.7/en/reserved-words.html"&gt;MySQL&lt;/a&gt;, &lt;a href="http://docs.oracle.com/cd/E16655_01/server.121/e17209/ap_keywd.htm#SQLRF022"&gt;Oracle&lt;/a&gt;, and &lt;a href="http://technet.microsoft.com/en-us/library/ms189822.aspx"&gt;MSSQL&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="singular-relations"&gt;Singular Relations&lt;/h2&gt;
&lt;p&gt;&lt;strong&gt;Tables, views, and other relations that hold data should have singular names, not plural.&lt;/strong&gt; This means our tables and views would be named &lt;code&gt;team&lt;/code&gt;, not &lt;code&gt;teams&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Rather than going into the &lt;a href="https://en.wikipedia.org/wiki/Relational_algebra"&gt;relational algebra&lt;/a&gt; explanation of why this is correct I'll give a few practical reasons.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They're Consistent.&lt;/strong&gt;
It's possible to have a relation that holds a single row. Is it still plural?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;They're unambiguous.&lt;/strong&gt;
Using only singular names means you don't need to determine how to pluralize nouns.&lt;/p&gt;
&lt;p&gt;Ex: Does a "Person" object go into a "Persons" relation or a "People" one? How about an "&lt;a href="https://en.wikipedia.org/wiki/Octopus#Etymology_and_pluralization"&gt;Octopus&lt;/a&gt;" object? Octopuses? Octopi? Octopodes?&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Straightforward 4GL Translation.&lt;/strong&gt;
Singular names allow you to directly translate from 4GL objects to database relations. You may need to remove some underscores and switch to &lt;a href="https://en.wikipedia.org/wiki/Camel_case"&gt;camel case&lt;/a&gt; but the name translation will always be straight forward.&lt;/p&gt;
&lt;p&gt;Ex: &lt;code&gt;team_member&lt;/code&gt; unambigously becomes the class &lt;code&gt;TeamMember&lt;/code&gt; in Java or the variable &lt;code&gt;team_member&lt;/code&gt; in Python.&lt;/p&gt;
&lt;h2 id="key-fields"&gt;Key Fields&lt;/h2&gt;
&lt;h3 id="primary-keys"&gt;Primary Keys&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Single column primary key fields should be named &lt;code&gt;id&lt;/code&gt;&lt;/strong&gt;. It's short, simple, and unambiguous. This means that when you're writing SQL you don't have to remember the names of the fields to join on.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;            &lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;full_name&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;birth_date&lt;/span&gt;&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="nb"&gt;date&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Some guides suggest prefixing the table name in the primary key field name, ie. &lt;code&gt;person_id&lt;/code&gt; vs &lt;code&gt;id&lt;/code&gt;. The extra prefix is redundant. All field names in non-trivial SQL statements (&lt;em&gt;i.e. those with more than one table&lt;/em&gt;) should be explicitly qualified and prefixing as a form of namespacing field names is a bad idea.&lt;/p&gt;
&lt;h3 id="foreign-keys"&gt;Foreign Keys&lt;/h3&gt;
&lt;p&gt;&lt;strong&gt;Foreign key fields should be a combination of the name of the referenced table and the name of the referenced fields&lt;/strong&gt;. For single column foreign keys (&lt;em&gt;by far the most common case&lt;/em&gt;) this will be something like &lt;code&gt;foo_id&lt;/code&gt;.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team_member&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team_member_pkey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="prefixes-and-suffixes-are-bad"&gt;Prefixes and Suffixes (&lt;em&gt;are bad&lt;/em&gt;)&lt;/h2&gt;
&lt;h3 id="relation-type-prefixes"&gt;Relation Type Prefixes&lt;/h3&gt;
&lt;p&gt;Some (&lt;em&gt;older&lt;/em&gt;) guidelines suggest naming tables with a &lt;code&gt;TB_&lt;/code&gt; prefix, views with a &lt;code&gt;VW_&lt;/code&gt; prefix, or stored procedures with a &lt;code&gt;SP_&lt;/code&gt; prefix. The rationale being that a programmer reading through some unknown SQL would immediately recognize this and know the object type based on the name. This is a bad idea.&lt;/p&gt;
&lt;p&gt;Object names should not include the object type in them. That way you can change it later. A view that is replaced with a table maintains the original contract of a view (&lt;em&gt;ex: you can SELECT from it&lt;/em&gt;). A dependent system would not need to be updated after such a change. &lt;/p&gt;
&lt;p&gt;I've seen many such systems where at some point a view will become a table. Then you'll end up with code issuing INSERT statements into &lt;code&gt;vw_foobar&lt;/code&gt;. There's even a really powerful feature of PostgreSQL that allows you do &lt;a href="http://www.postgresql.org/docs/9.3/static/rules-update.html"&gt;define DML rules&lt;/a&gt; on views (&lt;em&gt;ie. you can INSERT/UPDATE/DELETE from them&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Adding object type prefixes adds extra typing now and extra confusion down the road.&lt;/p&gt;
&lt;h3 id="application-name-prefixes"&gt;Application Name Prefixes&lt;/h3&gt;
&lt;p&gt;Another (&lt;em&gt;older&lt;/em&gt;) suggestion is to have a common prefix for all your database objects. For example, our app "Foobar" would have tables name &lt;code&gt;Foobar_Users&lt;/code&gt;, &lt;code&gt;Foobar_Teams&lt;/code&gt;, etc. Again, this is a bad idea.&lt;/p&gt;
&lt;p&gt;All modern databases support some form of namespacing. For example, in PostgreSQL you can create &lt;a href="http://www.postgresql.org/docs/9.3/static/ddl-schemas.html"&gt;schemas&lt;/a&gt; to group database objects. If you have multiple applications sharing the same database and want to prevent them from clobbering each other, use schemas instead. That's exactly what they are for!&lt;/p&gt;
&lt;p&gt;Most people will not even need them though. It's far more common for a database to be used with a single logical data model than multiple, separate data models. Hence schemas will not be required. If you do need them, it should be fairly obvious.&lt;/p&gt;
&lt;p&gt;The exception to this rule is if you are developing a database agnostic code base such as a framework or plugin. Supporting multiple namespacing methods is complicated so many frameworks instead rely on application name prefixing.&lt;/p&gt;
&lt;p&gt;However, most people develop applications, not plugins or frameworks, and their applications will reside by themselves in a single type of database. Thus there is no reason to add application name prefixes to all your database objects.&lt;/p&gt;
&lt;h3 id="data-type-suffixes"&gt;Data Type Suffixes&lt;/h3&gt;
&lt;p&gt;Some guides (&lt;em&gt;again generally older ones&lt;/em&gt;), suggest suffixing your column names with the data type of the field. For example, a text field for a name would be &lt;code&gt;name_tx&lt;/code&gt;. There will even be extensive lists to translate from data types to suffixes, text -&amp;gt; tx, date -&amp;gt; dt, etc.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;This is a bad idea!&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Field data types can change. A date field could become a timestamp, an int could become a bigint or numeric.&lt;/p&gt;
&lt;h2 id="explicit-naming"&gt;Explicit Naming&lt;/h2&gt;
&lt;p&gt;Some database commands that create database objects do not require you specify a name. An object name will be generated either randomly (&lt;em&gt;ex: fk239nxvknvsdvi&lt;/em&gt;) or via a formula (&lt;em&gt;ex: foobar_ix_1&lt;/em&gt;). Unless you know exactly how a name will be generated and you are happy with it, you should be explicitly specifying names.&lt;/p&gt;
&lt;p&gt;This also includes names generated by &lt;a href="https://en.wikipedia.org/wiki/Object-relational_mapping"&gt;ORMs&lt;/a&gt;. Many ORMs default to creating indexes and constraints with long gibberish generated names. The couple minutes of time savings in the short run are not worth the head ache in remembering what &lt;code&gt;fkas9dfnksdfnks&lt;/code&gt; refers to in the long run.&lt;/p&gt;
&lt;h3 id="indexes"&gt;Indexes&lt;/h3&gt;
&lt;p&gt;Indexes should be explicitly named and include both the table name and the column name(s) indexed. Including the column names make it &lt;em&gt;much&lt;/em&gt; easier to read through SQL explain plans. If an index is named &lt;code&gt;foobar_ix1&lt;/code&gt; then you would need to look up what columns that index covers to understand if it is being used correctly.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="n"&gt;bigserial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_ck_email_lower_case&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;CHECK&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;LOWER&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;)));&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INDEX&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_ix_first_name_last_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;ON&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Explain plans will now be easy to understand. We can clearly see that the index on first name and last name, ie. &lt;code&gt;person_ix_first_name_last_name&lt;/code&gt;, is being used:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;EXPLAIN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;*&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;WHERE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alice&amp;#39;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;smith&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="w"&gt;                                          &lt;/span&gt;&lt;span class="n"&gt;QUERY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;PLAN&lt;/span&gt;&lt;span class="w"&gt;                                          &lt;/span&gt;
&lt;span class="c1"&gt;----------------------------------------------------------------------------------------------&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;Index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Scan&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;using&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_ix_first_name_last_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;cost&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;15&lt;/span&gt;&lt;span class="p"&gt;..&lt;/span&gt;&lt;span class="mi"&gt;8&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="mi"&gt;17&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;width&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;72&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;Index&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Cond&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;AND&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;smith&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="constraints"&gt;Constraints&lt;/h3&gt;
&lt;p&gt;Similar to indexes, constraints should given descriptive names. This is especially true for check constraints. It's much easier to diagnose an errant insert if the check constraint that was violated lets you know the cause.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="n"&gt;bigserial&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="nb"&gt;text&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NOT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;NULL&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="k"&gt;CREATE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team_member&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;CONSTRAINT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team_member_pkey&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Notice how PostgreSQL does a good job of giving descriptive names to the foreign key constraints.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;=#&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;\&lt;/span&gt;&lt;span class="n"&gt;d&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team_member&lt;/span&gt;
&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="k"&gt;Table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;public.team_member&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;Column&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;Type&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Modifiers&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;
&lt;span class="c1"&gt;-----------+--------+-----------&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;bigint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;null&lt;/span&gt;
&lt;span class="n"&gt;Indexes&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;team_member_pkey&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;PRIMARY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;btree&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="k"&gt;Foreign&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;constraints&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;team_member_person_id_fkey&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOREIGN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;team_member_team_id_fkey&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;FOREIGN&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;KEY&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;REFERENCES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If we try inserting a row that violates one of these constraints we immediately know the cause just based on the constraint name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;team_member&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;5678&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;insert&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;or&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;update&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;on&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;team_member&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;violates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;foreign&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;constraint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;team_member_team_id_fkey&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;DETAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;Key&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;team_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1234&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;is&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;not&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;present&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;table&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;team&amp;quot;&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Similarly, if we try inserting an email address that is not lower case into the &lt;code&gt;person&lt;/code&gt; table created above, we'll get a constraint violation error that tells us exactly what is wrong:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;-- This insert will work:&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;alice@example.com&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Alice&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Anderson&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;

&lt;span class="c1"&gt;-- This insert will not work:&lt;/span&gt;
&lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INSERT&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;INTO&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;person&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;first_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;last_name&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;VALUES&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;bob@EXAMPLE.com&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Bob&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s1"&gt;&amp;#39;Barker&amp;#39;&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="n"&gt;ERROR&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;relation&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;person&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;violates&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;check&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;constraint&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="ss"&gt;&amp;quot;person_ck_email_lower_case&amp;quot;&lt;/span&gt;
&lt;span class="n"&gt;DETAIL&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="n"&gt;Failing&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;row&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;contains&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;bob&lt;/span&gt;&lt;span class="o"&gt;@&lt;/span&gt;&lt;span class="n"&gt;EXAMPLE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;com&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Bob&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="n"&gt;Barker&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;If you're starting a new project then I suggest you follow the conventions outlined here. If you're working on an existing project then you need to be a bit more careful with any new objects you create.&lt;/p&gt;
&lt;p&gt;The only thing worse than bad naming conventions is multiple naming conventions. If your existing project already has a standard approach to naming its database objects then keep using it.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Do you have something to add to this list, a way to improve some of these guidelines, or just think some of these are terrible? &lt;a href="mailto:sehrope@jackdb.com"&gt;Let me know&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;</content><category term="pg-planet"></category><category term="sql"></category><category term="databases"></category><category term="postgresql"></category></entry><entry><title>AWS Tips, Tricks, and Techniques</title><link href="https://launchbylunch.com/posts/2014/Jan/29/aws-tips/" rel="alternate"></link><published>2014-01-29T00:00:00-05:00</published><updated>2014-01-29T00:00:00-05:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2014-01-29:/posts/2014/Jan/29/aws-tips/</id><summary type="html">&lt;p&gt;Tips, tricks, and techniques for getting started using Amazon AWS.&lt;/p&gt;</summary><content type="html">&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#billing"&gt;Billing&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#security"&gt;Security&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#multi-factor-authentication"&gt;Multi-factor Authentication&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ssh"&gt;SSH&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#vpc"&gt;VPC&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#bastion-host"&gt;Bastion Host&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#firewall-whitelists"&gt;Firewall Whitelists&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#email"&gt;Email&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#verification"&gt;Verification&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#dkim-and-spf"&gt;DKIM and SPF&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#testing-via-port-25"&gt;Testing via Port 25&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#ec2-on-the-cheap"&gt;EC2 (on the cheap)&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#reserved-instances"&gt;Reserved Instances&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#reserved-instance-marketplace"&gt;Reserved Instance Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#spot-instances"&gt;Spot Instances&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#s3"&gt;S3&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#gpg"&gt;GPG&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#encryption-at-rest"&gt;Encryption At Rest&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#object-expiration"&gt;Object Expiration&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#glacier"&gt;Glacier&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#final-thoughts"&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;p&gt;AWS is one of the most popular cloud computing platforms. It provides everything from object storage (&lt;em&gt;S3&lt;/em&gt;), elastically provisioned servers (&lt;em&gt;EC2&lt;/em&gt;), databases as a service (&lt;em&gt;RDS&lt;/em&gt;), payment processing (&lt;em&gt;DevPay&lt;/em&gt;), virtualized networking (&lt;em&gt;VPC and AWS Direct Connect&lt;/em&gt;), content delivery networks (&lt;em&gt;CDN&lt;/em&gt;), monitoring (&lt;em&gt;CloudWatch&lt;/em&gt;), queueing (&lt;em&gt;SQS&lt;/em&gt;), and a whole lot more.&lt;/p&gt;
&lt;p&gt;In this post I'll be going over some tips, tricks, and general advice for getting started with Amazon Web Services (AWS). The majority of these are lessons we've learned in deploying and running our cloud SaaS product, &lt;a href="https://www.jackdb.com/"&gt;JackDB&lt;/a&gt;, which runs entirely on AWS.&lt;/p&gt;
&lt;h2 id="billing"&gt;Billing&lt;/h2&gt;
&lt;p&gt;AWS billing is invoiced at the end of the month and AWS services are generally provided on a "per use" basis. For example EC2 servers are quoted in $/hour. If you spin up a server for 6 hours then turn it off you'll only be billed for those 6 hours.&lt;/p&gt;
&lt;p&gt;Unfortunately, AWS does not provide a way to cap your monthly expenses. If you accidentally spin up too many servers and forget to turn them off, then you could get a bit shock at the end of they month. Similarly, AWS charges you for total outbound bandwidth used. If you have a spike in activity to a site hosted on AWS or just excessive usage of S3, you could end up with a sizable bill.&lt;/p&gt;
&lt;p&gt;AWS &lt;em&gt;does&lt;/em&gt; allow you to set up billing alerts. Amazon CloudWatch allows you to use your projected monthly bill as a metric for alerts. You can have a notification sent to you when it exceeds a preset dollar amount.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Do this immediately!&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;Seriously just go add this immediately. Even better, add a couple of these at a variety of dollar figures. A good starting set is: $1/mo, $10/mo, $50/mo, $100/mo, $250/mo, $500/mo, and $1,000/mo&lt;/p&gt;
&lt;p&gt;If you ever end up with a run away server or accidentally provision 10 servers instead of 1 (&lt;em&gt;which is surprisingly easy when scripting automated deployments...&lt;/em&gt;), then you'll be happy you set up these billing alerts.&lt;/p&gt;
&lt;p&gt;Our company, &lt;a href="https://www.jackdb.com/"&gt;JackDB&lt;/a&gt;, is entirely hosted on AWS and our bills are fairly consistent month to month. Once that monthly bill stabilized we setup billing alerts, as described above, at the expected value as well as 1/3 and 2/3 of it. This means that on approximately the 10th and 20th of each month we get a notification that our bill is at 1/3 or 2/3 of our expected monthly spend.&lt;/p&gt;
&lt;p&gt;If either of those alerts is triggered sooner than that, it'd be a sign that monthly is on the rise.&lt;/p&gt;
&lt;h2 id="security"&gt;Security&lt;/h2&gt;
&lt;p&gt;Security is a big deal, especially so in the cloud. Countless articles have been written about it and the advice here is just a couple quick points. At a later date I'd like to write a more detailed write up.&lt;/p&gt;
&lt;h3 id="multi-factor-authentication"&gt;Multi-factor Authentication&lt;/h3&gt;
&lt;p&gt;&lt;a href="https://en.wikipedia.org/wiki/Multi-factor_authentication"&gt;Mutli-factor authentication&lt;/a&gt;, also known as two-factor authentication or 2FA, is an enhanced approach to authentication that requires combining multiple, separate, factors of authentication. Most commonly these are &lt;strong&gt;something you know&lt;/strong&gt; (&lt;em&gt;ex: a password&lt;/em&gt;) and &lt;strong&gt;something you have&lt;/strong&gt; (&lt;em&gt;ex: a hardware token or virtual token generator on your phone&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;If one of these gets compromised (&lt;em&gt;ex: someone steals your password&lt;/em&gt;), they would still need one of the other factors to login. A single factor alone isn't enough.&lt;/p&gt;
&lt;p&gt;AWS supports &lt;a href="http://aws.amazon.com/iam/details/mfa/"&gt;multi-factor authentication&lt;/a&gt; using standard TOTP pin codes. It supports both free software pins (&lt;em&gt;ex: Google Authenticator on your smart phone&lt;/em&gt;) and hardware tokens (&lt;em&gt;$12.99 as of Jan, 2014&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Do this immediately!&lt;/em&gt;&lt;/strong&gt;&lt;/p&gt;
&lt;p&gt;There is no reason not to have this enabled and I recommend immediately enabling it. In fact, you should enable 2FA on &lt;strong&gt;every&lt;/strong&gt; service you use that supports it. If you're using Google Apps or even just regular Gmail, you should enable it there as well.&lt;/p&gt;
&lt;h3 id="ssh"&gt;SSH&lt;/h3&gt;
&lt;p&gt;It's a good idea to use unique SSH keys for unrelated projects. It makes it much easier to deprovision them later on. You should generate a new pair of SSH keys (&lt;em&gt;with a &lt;a href="https://xkcd.com/936/"&gt;long passphrase&lt;/a&gt;&lt;/em&gt;) to use with your new servers. If you have multiple people sharing access to the same servers, each person should have their own unique SSH keys.&lt;/p&gt;
&lt;p&gt;Rather than specifying your key files on the command line, you should add them to your ~/.ssh/config file so they'll be automatically used. q&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Host ec2-10-20-30-40.compute-1.amazonaws.com
  User ubuntu
  IdentityFile &amp;quot;~/.ssh/my-ec2-private-key&amp;quot;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;By default SSH will send all your available SSH keys in your keyring or listed in your config file to a remote server. If you have a lot of SSH keys then you may get an error from a remote server when you try to SSH to it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="nv"&gt;Too&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;many&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;authentication&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;failures&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;username&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;From the remote server's perspective each is considered a connection attempt. If you have too many SSH keys for it to try then it may not get to the correct one. To force it to only send the SSH key specific to the server you are connecting to as listed in your config file, add the following to the top of your &lt;code&gt;~/.ssh/config&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Host *
  IdentitiesOnly yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will force you to explicitly list the SSH key to use for &lt;strong&gt;all&lt;/strong&gt; remote servers. If you want to restrict this to just a subset of them, you can replace the "*" in the &lt;strong&gt;Host&lt;/strong&gt; section with a wildcard matching the DNS name of the servers. For example &lt;code&gt;*.example.com&lt;/code&gt;.&lt;/p&gt;
&lt;h3 id="vpc"&gt;VPC&lt;/h3&gt;
&lt;p&gt;&lt;a href="http://aws.amazon.com/vpc/"&gt;Amazon Virtual Private Coud (VPC)&lt;/a&gt; is a networking feature of EC2 that allows you to define a private network for a group of servers. Using it greatly simplifies fencing off components of your infrastructure and minimizing the externally facing pieces.&lt;/p&gt;
&lt;p&gt;The basic idea is to separate your infrastructure into two halves, a public half and a private half. The external endpoints for whatever you are creating goes in the public half. For a web application this would be your web server or load balancer.&lt;/p&gt;
&lt;p&gt;Services that are only consumed internally such as databases or caching servers belong in the private half. Components in the private half are &lt;em&gt;not&lt;/em&gt; directly accessible from the public internet.&lt;/p&gt;
&lt;p&gt;This is a form of the &lt;a href="https://en.wikipedia.org/wiki/Principle_of_least_privilege"&gt;principle of least privilege&lt;/a&gt; and it's a good idea to implement it. If your server infrastructure involves more than &lt;strong&gt;one&lt;/strong&gt; server, then you probably should be using a VPC.&lt;/p&gt;
&lt;h3 id="bastion-host"&gt;Bastion Host&lt;/h3&gt;
&lt;p&gt;To access internal components in the private half your VPC you'll need a bastion host. This is a dedicated server that will act as an SSH proxy to connect to your other internal components. It sits in the public half of your VPC.&lt;/p&gt;
&lt;p&gt;Using a bastion host with a VPC greatly simplifies network security when working on AWS by significantly minimizing the number of external firewall rules you need to manage. Here's how to set one up:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Spin up a new server in the public half of your VPC&lt;/li&gt;
&lt;li&gt;Create a security group &lt;strong&gt;Bastion SG&lt;/strong&gt; and assign it to the new server&lt;/li&gt;
&lt;li&gt;Edit the security groups for your private half servers to allow &lt;strong&gt;inbound&lt;/strong&gt; access on port 22 (SSH) from &lt;strong&gt;Bastion SG&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;Add your whitelisted IPs to the inbound ACL for &lt;strong&gt;Bastion SG&lt;/strong&gt; (&lt;em&gt;see the next section&lt;/em&gt;)&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;An SSH proxy server doesn't use that much CPU and practically zero disk. A m1.micro instance (&lt;em&gt;the cheapest one that AWS offers&lt;/em&gt;) is more than enough for this. As of Jan 2014, at on-demand rates this comes out to ~$15/mo. With reserved instances you can bring this down to about $7/mo.&lt;/p&gt;
&lt;p&gt;The added cost is nothing compared to the simplicity and security it adds to your overall environment.&lt;/p&gt;
&lt;h3 id="firewall-whitelists"&gt;Firewall Whitelists&lt;/h3&gt;
&lt;p&gt;Any server with port 22 (SSH) open to the public internet will get &lt;strong&gt;a lot&lt;/strong&gt; of hacking attempts. Within 24 hours of turning on a such a server you should see a lot of entries in your SSH logs of bots trying to brute force log in. If you only allow SSH key based authentication this will be a pointless exercise but it's still annoying to see all the entries in the log (&lt;em&gt;ie. it's extra noise&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;One way to avoid this is to whitelist the IP addresses that can connect to your server. If you have a static IP address at your office or if it's "mostly static" (&lt;em&gt;ex: most dynamic IPs for cable modems and DSL don't change very often&lt;/em&gt;), then you can set up the firewall rules for your servers to only allow inbound SSH access from those IPs. Other IP addresses will not even be able to tell there is an SSH server running. Port scanning for an SSH server will fail as the initial TCP socket will never get established.&lt;/p&gt;
&lt;p&gt;Normally this would be a pain to manage on multiple servers but by using a bastion host this only needs to be done in one place. Later on if your IP address changes or you need to connect to your server from a new location (&lt;em&gt;ex: on the road at a hotel&lt;/em&gt;), then just add your current IP address to the whitelist. When you're done, simply remove it from the list.&lt;/p&gt;
&lt;h2 id="email"&gt;Email&lt;/h2&gt;
&lt;p&gt;Amazon &lt;a href="http://aws.amazon.com/ses/"&gt;Simple Email Service (SES)&lt;/a&gt; is Amazon's send-only email service for AWS. You can use it to send out email from your application or command line scripts. It includes both an AWS specific programmatic API as well as a standard STMP interface. Both cost the same (&lt;em&gt;it's pay per use&lt;/em&gt;)  but for portability purposes I recommend using the SMTP interface. This allows you to change your email provider down the road.&lt;/p&gt;
&lt;p&gt;If you're only sending a handful emails then SES can be used free of charge. The first 2,000 emails per day are free when you send them from an EC2 server. For a lot of people this should be more than enough. After the first 2,000 per day it's $.10 per 1,000 emails plus outbound bandwidth costs.&lt;/p&gt;
&lt;h3 id="verification"&gt;Verification&lt;/h3&gt;
&lt;p&gt;When you first set it up you'll need to verify ownership for the email addresses that you'll be sending from. For example if you want to send email from &lt;strong&gt;hello@example.com&lt;/strong&gt; then you'll need to prove you actually control &lt;strong&gt;example.com&lt;/strong&gt;. This is done by either verifying that you can receive email at that address. You can also verify an entire domain by setting up TXT records to verify domain ownership.&lt;/p&gt;
&lt;p&gt;You cannot send email until this verification is complete and it can take a little while for it to propagate through the system. Additionally, to prevent spammers from using SES for nefarious purposes, Amazon restricts your initial usage of SES. Your sending limit is gradually increased. If you plan on sending email from your application you should set up SES immediately so that it's available when you need it.&lt;/p&gt;
&lt;h3 id="dkim-and-spf"&gt;DKIM and SPF&lt;/h3&gt;
&lt;p&gt;To ensure that your emails are actually received by your recipients and not rejected by their spam filters you should set up both &lt;a href="https://en.wikipedia.org/wiki/Dkim"&gt;DKIM&lt;/a&gt; and &lt;a href="https://en.wikipedia.org/wiki/Sender_Policy_Framework"&gt;SPF&lt;/a&gt; for your domain. Each is a way for recipients to verify that Amazon SES is a legitimate sender of email for your domain.&lt;/p&gt;
&lt;p&gt;You can read more about setting up SPF on SES &lt;a href="http://docs.aws.amazon.com/ses/latest/DeveloperGuide/spf.html"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;You can read more about setting up DKIM on SES &lt;a href="http://docs.aws.amazon.com/ses/latest/DeveloperGuide/easy-dkim.html"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Also, if you haven't already, you should set up DKIM and SPF for your domain's email servers as well. If you're using Google Apps for email hosting more details are available &lt;a href="https://support.google.com/a/answer/174124?hl=en"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="testing-via-port-25"&gt;Testing via Port 25&lt;/h3&gt;
&lt;p&gt;Once you have it set up, a simple way to test out both DKIM and SPF is using the email verification service provided by &lt;a href="http://www.port25.com/support/authentication-center/email-verification/"&gt;Port 25&lt;/a&gt;. Simply send them an email and a short while later they'll respond back with a report saying whether SPF and DKIM are properly configured. They'll also indicate whether their spam filters would flag your message as junk mail.&lt;/p&gt;
&lt;p&gt;Note that you can also use Port 25 to verify your personal email address as well, not just Amazon SES. Just manually send an email to Port 25 and wait for the response.&lt;/p&gt;
&lt;h2 id="ec2-on-the-cheap"&gt;EC2 (on the cheap)&lt;/h2&gt;
&lt;p&gt;Amazon EC2 allows you to spin up servers on demand and you only pay for what you usage, billed hourly. This means it's particularly catered towards usage patterns that involve scaling up to a large number of servers for a short period of time.&lt;/p&gt;
&lt;p&gt;The flip side of the fine grained billing of EC2 is that the on-demand price of the servers is more expensive than other cloud providers. Here a couple tips to lower your costs.&lt;/p&gt;
&lt;h3 id="reserved-instances"&gt;Reserved Instances&lt;/h3&gt;
&lt;p&gt;If you are running a server that is always online you should look into reserved instances. With reserved instances you pay an upfront fee to in exchange for greatly reduced hourly rates. Amazon offers three levels of reserved instances, &lt;strong&gt;Light&lt;/strong&gt; (&lt;em&gt;the cheapest&lt;/em&gt;), &lt;strong&gt;Medium&lt;/strong&gt;, and &lt;strong&gt;Heavy&lt;/strong&gt; (&lt;em&gt;most expensive&lt;/em&gt;), and each is offered in 1-year (&lt;em&gt;cheaper&lt;/em&gt;) or 3-year blocks (&lt;em&gt;more expensive&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Each costs progressively more, but also progressively reduces the hourly cost for running a server. For example a standard m1.small instance costs $.06/hour on-demand. If you buy a 3-year light reserved instance for $96 the hourly price drops to $.027/hour. Factoring in the up front cost and assuming the server runs 24-hours a day, it would take about 4 months to break even with the reserved instance vs paying on-demand rates.&lt;/p&gt;
&lt;p&gt;Billing for &lt;strong&gt;Heavy&lt;/strong&gt; instances is a bit different than &lt;strong&gt;Light&lt;/strong&gt;  or &lt;strong&gt;Medium&lt;/strong&gt; as you'll be billed for the underlying instances &lt;strong&gt;regardless&lt;/strong&gt; of whether it's actually running. This means that if you buy a 3-year heavy reserved instance you're agreeing to pay for running that instance 24-hours a day for the next three years.&lt;/p&gt;
&lt;p&gt;The savings for reserved instances is anywhere from 25% to 65% over on-demand pricing. The break even point is anywhere from 3-months to a year. Once your AWS usage has stabilized it's well worth the time to investigate the cost savings of buying reserved instances.&lt;/p&gt;
&lt;p&gt;If you're unsure of whether you'll be running the instance types a year or two from now I suggest sticking to Light or Medium instances. The savings differential is only another 10-15% but they allow you more cheaply change your infrastructure down the road.&lt;/p&gt;
&lt;h3 id="reserved-instance-marketplace"&gt;Reserved Instance Marketplace&lt;/h3&gt;
&lt;p&gt;There's also a reserved instance marketplace where you can buy reserved instances from third parties or sell those that you no longer need. Since reserved instances only impact billing there's no difference in functionality in buying them via the third party marketplace. In fact, it's all part of the same UI in the admin console.&lt;/p&gt;
&lt;p&gt;The only real difference with third party reserved instances is the duration of the reservation can be just about anything. This can be very useful if you anticipate a usage term besides 1-year or 3-years. Just make sure to check the actual price as there usually a couple listed that have wildly inflated prices compared to the Amazon offered ones.&lt;/p&gt;
&lt;h3 id="spot-instances"&gt;Spot Instances&lt;/h3&gt;
&lt;p&gt;Spot instances allow you to bid on excess EC2 capacity. As long as your bid price is above the current spot price, your instance will continue to run and you'll pay the lower of the two per hour. If the spot price increases beyond your bid, your instance may be terminated. This termination could happen at anytime.&lt;/p&gt;
&lt;p&gt;As they can be terminated at any time, spot instances work best when application state and results are stored outside the instance itself. Idempotent operations and reproducible or parallelizable work is usually a great candidate for spot instances. One of the most popular use cases for it is for running continuous integration (CI) servers. If the CI server crashes and restarts an hour later, it's not that big of a deal.  The only result we really care about is if the final application build/test was successful.&lt;/p&gt;
&lt;p&gt;More complicated setups are possible with spot instances as well. If you fully automate their provisioning (&lt;em&gt;ie. automate security groups, user data, startup scripts&lt;/em&gt;), you can even have them automatically register themselves via Route53 DNS and then join an existing load balancer. In a later article I'll be writing about how to build such a setup that cheaply scales out a stateless web service via a phalanx of spot instances, all the while being fault tolerant to their random termination.&lt;/p&gt;
&lt;h2 id="s3"&gt;S3&lt;/h2&gt;
&lt;p&gt;S3 is Amazon's object store. It allows you to store arbitrary objects (&lt;em&gt;up to 5TB in size&lt;/em&gt;) and access them over HTTP or HTTPS. It is said to be designed to provide 99.999999999% durability and 99.99% availability. For what it offers, S3 is really &lt;a href="http://aws.amazon.com/s3/pricing/"&gt;cheap&lt;/a&gt;. Amazon also periodically drops the prices for it as well. Most recently a &lt;a href="http://aws.amazon.com/s3/pricing/effective-february-2014/"&gt;week ago&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;The S3 API allows you to create signed URLs that provide fine grained access to S3 resources with custom expirations. For example you can have an object stored in S3 that is not publicly readable to be temporarily accessible via a signed URL. This is a great way to provide user's access to objects stored in private S3 buckets from webapps.&lt;/p&gt;
&lt;h3 id="gpg"&gt;GPG&lt;/h3&gt;
&lt;p&gt;If you're using S3 to store sensitive data (&lt;em&gt;ex: database backups&lt;/em&gt;) then you should encrypt the data &lt;strong&gt;before&lt;/strong&gt; you upload it to S3. That way only encrypted data is stored on Amazon's servers.&lt;/p&gt;
&lt;p&gt;The easiest way to do this is using &lt;a href="https://en.wikipedia.org/wiki/GNU_Privacy_Guard"&gt;GPG&lt;/a&gt;. Generate a GPG key pair for your backup server on your local machine and add the &lt;strong&gt;public&lt;/strong&gt; key of the key pair to the server. Then use that public key to encrypt any data before uploading to S3.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Set the bucket/path on S3 where we will put the backup:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;S3_PATH&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;s3://my-s3-bucket/path/to/backup/backup-&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;date&lt;span class="w"&gt; &lt;/span&gt;+%Y%M%d&lt;span class="k"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Temp file for encryption:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;GPG_TEMP_FILE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;mktemp&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Encrypt it with GPG:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;gpg&lt;span class="w"&gt; &lt;/span&gt;--recipient&lt;span class="w"&gt; &lt;/span&gt;aws.backup@example.com&lt;span class="w"&gt; &lt;/span&gt;--output&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GPG_TEMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;--encrypt&lt;span class="w"&gt; &lt;/span&gt;mydata.foo

&lt;span class="c1"&gt;# Upload it via s3cmd:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;s3cmd&lt;span class="w"&gt; &lt;/span&gt;put&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GPG_TEMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$S3_PATH&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="c1"&gt;# Clean up temp file:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;rm&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$GPG_TEMP_FILE&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The only downside to encrypting data prior to storage on S3 is that you will need to decrypt it to read it. You can't provide the S3 URL to someone else to download the data (&lt;em&gt;ex: a direct link to a user's content that you're storing on their behalf&lt;/em&gt;) as they will will not be able to decrypt it. For backups this is the right approach as only &lt;strong&gt;you&lt;/strong&gt; (&lt;em&gt;or your company, your team, ...&lt;/em&gt;) should be able to read your data.&lt;/p&gt;
&lt;p&gt;The S3 CLI tool &lt;a href="http://s3tools.org/s3cmd"&gt;s3cmd&lt;/a&gt; includes an option to specify a GPG key. If set, then it will automatically encrypt at objects you PUT to S3. It can be a convenient option as you only need to set it one place (&lt;em&gt;the s3cmd config file&lt;/em&gt;). I prefer explicitly adding the GPG steps to my backup scripts though.&lt;/p&gt;
&lt;h3 id="encryption-at-rest"&gt;Encryption At Rest&lt;/h3&gt;
&lt;p&gt;S3 also supports what is referred to as &lt;a href="http://aws.typepad.com/aws/2011/10/new-amazon-s3-server-side-encryption.html"&gt;&lt;strong&gt;Server Side Encryption&lt;/strong&gt;&lt;/a&gt;
. With this option enabled, Amazon will encrypt your data before persisting it to disk, and will transparently decrypt it prior to serving it back to a valid S3 request. The documentation for it describes how they keep the decryption keys separate from the S3 API servers and request them on demand.&lt;/p&gt;
&lt;p&gt;Since Amazon can still read your data I don't consider this to be that useful of a feature. If you want to ensure that nobody else can read your data then &lt;strong&gt;you&lt;/strong&gt; need to do the encryption. If you trust a third party to do it, then by definition that third party is able to read your unencrypted data.&lt;/p&gt;
&lt;p&gt;Still though, it doesn't hurt to enable it either. Apparently there is no real performance penalty for &lt;a href="http://aws.typepad.com/aws/2011/10/new-amazon-s3-server-side-encryption.html#comment-6a00d8341c534853ef014e8c19308e970d"&gt;enabling it&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="object-expiration"&gt;Object Expiration&lt;/h3&gt;
&lt;p&gt;Once you start using S3 for backups you'll notice that your S3 bill will grow fairly linearly (&lt;em&gt;or faster!&lt;/em&gt;). By default, S3 persists objects forever so you'll need to take some extra steps to clean things up.&lt;/p&gt;
&lt;p&gt;The obvious solution is to delete objects as they get old. If you're using S3 for automated backups then you can have your backup script delete older entries. With additional logic you can have your backup scripts keep various ages of backups (&lt;em&gt;ex: monthly for a year, weekly for a month, daily for a week&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;A simpler, albeit coarser, approach is to use S3 object expirations. It allows you to define a maximum age for S3 objects. Once that age is reached, the object will automatically be deleted with no user interaction. You could set a 6-month expiration on your S3 backup bucket and it will automatically delete older entries than that. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;&lt;em&gt;Warning:&lt;/em&gt;&lt;/strong&gt; &lt;em&gt;If you use S3 object expiration make sure that your backups are actually working. It will delete your old objects regardless of whether your latest backups are actually valid. Make sure to test your backups regularly!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Object expiration is also a simple way to delete all the objects in an S3 bucket. From the AWS S3 console simply set the expiration for the entire bucket to be 1-day. The objects will then be automatically deleted after a day. S3 expiration is asynchronous so you may still see the objects in S3 for a short while after 24-hours but you will not be billed for them.&lt;/p&gt;
&lt;h3 id="glacier"&gt;Glacier&lt;/h3&gt;
&lt;p&gt;&lt;a href="http://aws.amazon.com/glacier/"&gt;Glacier&lt;/a&gt; is Amazon's long term, persistent storage service. It's about an order of magnitude cheaper than S3 but with much slower access times (&lt;em&gt;on the order of multiple hours&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Rather than deleting old S3 objects you can configure S3 to automatically expire them to Glacier. The storage cost will be about 10x less and if you really need the data (&lt;em&gt;ex: you accidentally destroyed all your S3 backups&lt;/em&gt;) you can slowly restore them from Glacier.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;This post ended up longer than originally planned, but it's still a &lt;em&gt;very&lt;/em&gt; incomplete list. There are plenty of other tips, tricks, and techniques to use with AWS, this is just a (&lt;em&gt;hopefully helpful&lt;/em&gt;) start.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Do you have something to add to this list, a better way of solving some of these problems, or just want to be notified when I have a new post? &lt;a href="mailto:sehrope@jackdb.com"&gt;Let me know&lt;/a&gt;!&lt;/em&gt;&lt;/p&gt;</content><category term="posts"></category><category term="aws"></category><category term="2fa"></category><category term="ec2"></category><category term="s3"></category><category term="email"></category><category term="dkim"></category><category term="ssh"></category></entry><entry><title>My blog's tech stack: Pelican powered, Dokku deployed</title><link href="https://launchbylunch.com/posts/2014/Jan/23/blog-tech-stack/" rel="alternate"></link><published>2014-01-23T00:00:00-05:00</published><updated>2014-01-23T00:00:00-05:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2014-01-23:/posts/2014/Jan/23/blog-tech-stack/</id><summary type="html">&lt;p&gt;An overview of the tech stack for this blog, why I picked it, and how to set it all up.&lt;/p&gt;</summary><content type="html">&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#requirements"&gt;Requirements&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#cheap"&gt;Cheap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#easy"&gt;Easy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#new"&gt;New&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#fun"&gt;Fun&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#static-site"&gt;Static Site&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#generators"&gt;Generators&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#pelican"&gt;Pelican&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#setup"&gt;Setup&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#pelican-quickstart"&gt;Pelican Quickstart&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#barebones-pelican-blog"&gt;Barebones Pelican Blog&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#docker-dokku-and-digitalocean"&gt;Docker, Dokku, and DigitalOcean&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ssl"&gt;SSL&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#final-thoughts"&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;p&gt;This post goes into the technical components of setting up this blog and some of the merits of the tech stack choices.&lt;/p&gt;
&lt;p&gt;For the impatient, the final choice of stack is a &lt;a href="http://getpelican.com/"&gt;Pelican&lt;/a&gt; powered static site, styled with a modified &lt;a href="https://github.com/sehrope/pelican-octopress-theme"&gt;Octopress theme&lt;/a&gt;, deployed via &lt;a href="https://github.com/progrium/dokku"&gt;Dokku&lt;/a&gt;, running &lt;a href="http://nginx.org/"&gt;nginx&lt;/a&gt; in a &lt;a href="http://docker.io/"&gt;Docker&lt;/a&gt; container, and it's all deployed to &lt;a href="https://www.digitalocean.com/?refcode=eeda01627607"&gt;DigitalOcean&lt;/a&gt; droplet.&lt;/p&gt;
&lt;p&gt;To put it another way: &lt;em&gt;it's a riddle, wrapped in a mystery, inside an enigma&lt;/em&gt;.&lt;/p&gt;
&lt;h2 id="requirements"&gt;Requirements&lt;/h2&gt;
&lt;p&gt;When setting up a new blog one of the most important (&lt;em&gt;and fun&lt;/em&gt;) decisions you get to make is the tech stack that will run it. Besides the topic and posts themselves, this is probably the most important decision. Additionally, the more technologically inclined you are, the more time you'll spend on this decision.&lt;/p&gt;
&lt;p&gt;In my case, I'd use a number of platforms in the past and had reduced my core requirements list to the following (&lt;em&gt;in order of importance&lt;/em&gt;):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Cheap&lt;/li&gt;
&lt;li&gt;Easy&lt;/li&gt;
&lt;li&gt;New&lt;/li&gt;
&lt;li&gt;Fun&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="cheap"&gt;Cheap&lt;/h3&gt;
&lt;p&gt;In 2014 there are &lt;em&gt;many&lt;/em&gt; options for cheap hosting so cost was not much of a concern. &lt;/p&gt;
&lt;p&gt;For static content there are a number of free hosting options such as Tumblr or GitHub pages. You can also use services like Amazon S3 to host a website for next to nothing.&lt;/p&gt;
&lt;p&gt;For example our company's website, &lt;a href="https://www.jackdb.com/"&gt;https://www.jackdb.com/&lt;/a&gt;, is a static site hosted on Amazon S3. The bill for it is literally &lt;em&gt;pennies&lt;/em&gt; a month and, since it's on S3, we never worry about scaling it up to handle large influxes of traffic.&lt;/p&gt;
&lt;p&gt;Even sites with dynamic content can be hosted for free or relatively cheaply. &lt;a href="https://www.heroku.com/"&gt;Heroku&lt;/a&gt; gives you a free web dyno per application. You use it to either run a dynamic site (&lt;em&gt;ex: Wordpress or Ghost&lt;/em&gt;) or a static site (&lt;em&gt;ex: nginx serving a pregenerated site&lt;/em&gt;).&lt;/p&gt;
&lt;h3 id="easy"&gt;Easy&lt;/h3&gt;
&lt;p&gt;Easy in this context means it should be easy to preview an article while writing and the final deployment step should be automated. The ideal workflow for writing will look something like this:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Write something&lt;/li&gt;
&lt;li&gt;View what it'll look like in the browser&lt;/li&gt;
&lt;li&gt;Not done? Goto step 1&lt;/li&gt;
&lt;li&gt;Publish to the production site&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Saavy readers will recognize this as the writing equivalent of a &lt;a href="https://en.wikipedia.org/wiki/REPL"&gt;REPL&lt;/a&gt;. Since a big part of writing is minimizing distraction (&lt;em&gt;so you can actually focus on writing&lt;/em&gt;), it's important to have an efficient way to "test" your posts (&lt;em&gt;i.e. the step 2 to 3 flow&lt;/em&gt;).,&lt;/p&gt;
&lt;p&gt;Publishing should be as simple as possible, preferably from the command line. This usually means it should be &lt;a href="https://devcenter.heroku.com/articles/git"&gt;git push&lt;/a&gt; style deploy.&lt;/p&gt;
&lt;p&gt;Note that being "easy" does not refer to the initial setup of the platform. On the contrary, the newer the platform the &lt;strong&gt;more&lt;/strong&gt; likely there will be complications with the initial setup. If they're not show stoppers then that would probably increase the "fun" factor though.&lt;/p&gt;
&lt;h3 id="new"&gt;New&lt;/h3&gt;
&lt;p&gt;By "new" I'm referring to technology choices that are new to not just me, but the world as a whole. Like any good technologist I'd like to use this as an opportunity to experiement with new technologies.&lt;/p&gt;
&lt;p&gt;There's a high correlation between new and fun.&lt;/p&gt;
&lt;h3 id="fun"&gt;Fun&lt;/h3&gt;
&lt;p&gt;The "fun" factor of setting up a new website is derived from the new (&lt;em&gt;to you&lt;/em&gt;) technology you get to experiment with. The more twists, turns, and hacks to set it up, the more fun you're going to have. A decent gauge of whether something is going to be fun is whether you'd want to listen to someone else describe the set up process.&lt;/p&gt;
&lt;p&gt;On the low end of the fun scale (&lt;em&gt;arguably negative fun...&lt;/em&gt;) is using a hosted service like Tumblr. There is no real set up work involved (&lt;em&gt;just sign up and optionally pick a theme&lt;/em&gt;) and nothing new to experiment with.&lt;/p&gt;
&lt;p&gt;The opposite extreme would be writing your own blogging software. It would be a very fun exercise and would make for a good write up on its own, but you probably would not get much writing done until it's finished.&lt;/p&gt;
&lt;h2 id="static-site"&gt;Static Site&lt;/h2&gt;
&lt;p&gt;I decided early on that I wanted the site to be entirely static. A static site increases your hosting options and will generally scale very cheaply and easily. Properly deployed (&lt;em&gt;i.e. ngninx with the right cache headers&lt;/em&gt;), a static site on a single server should handle any reasonable amount of traffic. Deciding on having the site be static also helps with the &lt;a href="https://en.wikipedia.org/wiki/Paradox_of_choice"&gt;paradox of choice&lt;/a&gt; in picking a tech stack.&lt;/p&gt;
&lt;p&gt;The biggest limitation with a static site is that all the content that you serve is generated in advance. This means you can't have any dynamicly generated content and any user interaction with your site will be handled by external services.&lt;/p&gt;
&lt;p&gt;For example, if you want to add comments to a static website you could use a service like &lt;a href="http://disqus.com/"&gt;Disqus&lt;/a&gt;. If you'd like to have a sign up form to collect email addresses from your readers, you can use a service like &lt;a href="http://mailchimp.com/"&gt;Mailchimp&lt;/a&gt; or &lt;a href="http://www.wufoo.com/"&gt;Wufoo&lt;/a&gt;.&lt;/p&gt;
&lt;h3 id="generators"&gt;Generators&lt;/h3&gt;
&lt;p&gt;The end result of a static website is a collection of HTML, CSS, and other asset files that you'd deploy to a web server. Rather than generate them by hand (&lt;em&gt;i.e. copy paste each page...&lt;/em&gt;) most people use a static site generator. These are pieces of software that automate the process of generating the HTML pages of your site.&lt;/p&gt;
&lt;p&gt;Some popular features for static site generators are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Allow writing posts in a light weight syntax such as &lt;a href="http://daringfireball.net/projects/markdown/"&gt;Markdown&lt;/a&gt; rather than straight HTML&lt;/li&gt;
&lt;li&gt;Automatically appying common CSS styling and theming to all your posts&lt;/li&gt;
&lt;li&gt;Generating index and listings pages&lt;/li&gt;
&lt;li&gt;Grouping pages based on post tags or content&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;See &lt;a href="http://staticsitegenerators.net/"&gt;here&lt;/a&gt; for a great list of alternatives. There are a &lt;strong&gt;lots&lt;/strong&gt; of them.&lt;/p&gt;
&lt;h3 id="pelican"&gt;Pelican&lt;/h3&gt;
&lt;p&gt;The static site generator I decided to use is &lt;a href="http://getpelican.com/"&gt;Pelican&lt;/a&gt;. Here are some quick highlights of it:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Written in Python&lt;/li&gt;
&lt;li&gt;Under &lt;a href="https://github.com/getpelican/pelican"&gt;active development&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Lots&lt;/strong&gt; of &lt;a href="https://github.com/getpelican/pelican-themes"&gt;themes&lt;/a&gt; available&lt;/li&gt;
&lt;li&gt;Many deployment options (&lt;em&gt;the final result is a static site&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Easy to install&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;I don't write a large amount of Python code but it is a language I like a lot. The syntax is clean, it has a wealth of libraries, and it runs on just about every platform. I didn't have to modify the source for Pelican in setting up my blog but, if I did, I know it'll be pretty easy.&lt;/p&gt;
&lt;p&gt;Although I didn't particularly like Octopress, I do like the standard Octopress theme. Thankfully, there is a &lt;a href="https://github.com/duilio/pelican-octopress-theme"&gt;Pelican theme&lt;/a&gt; for it! I used it as a base and added/updated/fixed a few things. You can find my fork &lt;a href="Octopress theme"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2 id="setup"&gt;Setup&lt;/h2&gt;
&lt;p&gt;Although installing Pelican was easy enough, getting a sample site up took a bit of work. All the building blocks are there but the documentation and initial setup could use a bit of work.&lt;/p&gt;
&lt;h3 id="pelican-quickstart"&gt;Pelican Quickstart&lt;/h3&gt;
&lt;p&gt;Pelican includes an executable, &lt;a href="http://docs.getpelican.com/en/3.3.0/getting_started.html#kickstart-your-site"&gt;&lt;code&gt;pelican-quickstart&lt;/code&gt;&lt;/a&gt;, that automates the creation of a new site. It asks you a couple basic questions (&lt;em&gt;your name, site name, URL, etc&lt;/em&gt;) and creates an site directory. It even includes a development script that watches your file system for changes and automatically regenerates the static site (&lt;em&gt;REPL solved!&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;Unfortunately, the site it generates is &lt;strong&gt;really&lt;/strong&gt; empty. A lot of the standard customizations that you'd expect to do to a new site are missing:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;No sample post (&lt;em&gt;where do I write?&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;No favicon defined (&lt;em&gt;where does it go?&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;No sample images (&lt;em&gt;where am I suppoed to add them?&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;No sample extras (&lt;em&gt;where do I put misc files I want to add like a PGP key?&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;No markdown code highlighting (&lt;em&gt;how do I enable this?&lt;/em&gt;)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3 id="barebones-pelican-blog"&gt;Barebones Pelican Blog&lt;/h3&gt;
&lt;p&gt;It's perfectly understandable why the base site that is generated doesn't include any of these. It's meant to be as lean as possible and added atop.&lt;/p&gt;
&lt;p&gt;Still though, I prefer a working example I can modify. When working with a new piece of technology it's always easier to tweak a functional existing system and comment out blocks you don't need then add things you do.&lt;/p&gt;
&lt;p&gt;To simplify this for the next person who'd like to use Pelican, I created a bare bones example of a Pelican powered blog. You can find it here: &lt;a href="https://github.com/sehrope/pelican-dokku-bare"&gt;https://github.com/sehrope/pelican-dokku-bare&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Some highlights of it are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Includes a &lt;a href="https://github.com/sehrope/pelican-dokku-bare#customization-steps"&gt;customization list&lt;/a&gt; (&lt;em&gt;i.e. list of places to edit to make it &lt;strong&gt;yours&lt;/strong&gt;&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Includes a sample post&lt;/li&gt;
&lt;li&gt;Includes a sample image (&lt;em&gt;and how to link to it in a post&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Includes a sample PGP key (&lt;em&gt;to show how to include extra files&lt;/em&gt;)&lt;/li&gt;
&lt;li&gt;Enables markdown code highlighting&lt;/li&gt;
&lt;li&gt;Custom 403, 404, and 500 error page via git&lt;/li&gt;
&lt;li&gt;Defaults to my tweaked Octopress theme&lt;/li&gt;
&lt;li&gt;Deploys out of the box to a Paas (&lt;em&gt;i.e. Heroku or Dokku&lt;/em&gt;) via git&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;To use it just clone the repo and start it up:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Clone it locally:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/sehrope/pelican-dokku-bare&lt;span class="w"&gt; &lt;/span&gt;my-blog

&lt;span class="c1"&gt;# Enter the cloned directory:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;my-blog

&lt;span class="c1"&gt;# Start up the development server:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;./develop_server.sh&lt;span class="w"&gt; &lt;/span&gt;start
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now if you add or edit a file in the content/posts directory it will automatically show up. To further customize it follow the instructions in the README.&lt;/p&gt;
&lt;h3 id="docker-dokku-and-digitalocean"&gt;Docker, Dokku, and DigitalOcean&lt;/h3&gt;
&lt;p&gt;The final (&lt;em&gt;and most fun&lt;/em&gt;) part of the setup was the install for Docker and Dokku on DigitalOcean. Docker and Dokku definitely fall into both the "new" and "fun" categories of technology. It was so fun I wrote about &lt;a href="https://launchbylunch.com/posts/2014/Jan/07/personal-paas-with-dokku-on-digital-ocean/"&gt;setting it all up&lt;/a&gt; and how to &lt;a href="https://launchbylunch.com/posts/2014/Jan/13/encrypting-docker-on-digitalocean/"&gt;improve it further&lt;/a&gt;. &lt;/p&gt;
&lt;p&gt;In reality the Dokku server set up was the first thing I did &amp;mdash; the blog itself was started because I wanted to write about the setup.&lt;/p&gt;
&lt;p&gt;Once Dokku is configured, this blog is just another application deployed to my DigitalOcean droplet. There is no special setup or configuration for it. I just added it as a git remote and git push to it whenever I'd like to publish new content.&lt;/p&gt;
&lt;p&gt;All it takes is:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add the remote Dokku production site:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;remote&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;production&lt;span class="w"&gt; &lt;/span&gt;dokku@launchbylunch.com:launchbylunch.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then I can publish the blog via:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;production&lt;span class="w"&gt; &lt;/span&gt;master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Publishing takes about a minute as each time it rebuilds the entire application from scratch. The Pelican site generation takes barely a second and most of the deploy time is spent building the dependencies (&lt;em&gt;ex: the buildpack compiles nginx from scratch&lt;/em&gt;). It's all fully automated and reproducible though so you don't really think about it.&lt;/p&gt;
&lt;h3 id="ssl"&gt;SSL&lt;/h3&gt;
&lt;p&gt;The only addtional configuration I did for this site after deploying it to my DigitalOcean droplet was to add SSL. Dokku has support for it out of the box so it was really just a matter of creating the server's private key, getting it signed, and uploading it.&lt;/p&gt;
&lt;p&gt;First generate a 4096 bit key. You'll be prompted for a passphrase (&lt;em&gt;don't forget it!&lt;/em&gt;).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;genrsa&lt;span class="w"&gt; &lt;/span&gt;-des3&lt;span class="w"&gt; &lt;/span&gt;-out&lt;span class="w"&gt; &lt;/span&gt;server.key.secure&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;
Generating&lt;span class="w"&gt; &lt;/span&gt;RSA&lt;span class="w"&gt; &lt;/span&gt;private&lt;span class="w"&gt; &lt;/span&gt;key,&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bit&lt;span class="w"&gt; &lt;/span&gt;long&lt;span class="w"&gt; &lt;/span&gt;modulus
&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="w"&gt; &lt;/span&gt;truncated&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
Enter&lt;span class="w"&gt; &lt;/span&gt;pass&lt;span class="w"&gt; &lt;/span&gt;phrase&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;server.key.secure:
Verifying&lt;span class="w"&gt; &lt;/span&gt;-&lt;span class="w"&gt; &lt;/span&gt;Enter&lt;span class="w"&gt; &lt;/span&gt;pass&lt;span class="w"&gt; &lt;/span&gt;phrase&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;server.key.secure:
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then create a password-less version of the private key. This will be the file we upload to our server. It doesn't have a password so that our web server can be restarted without manual intervention.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;rsa&lt;span class="w"&gt; &lt;/span&gt;-in&lt;span class="w"&gt; &lt;/span&gt;server.key.secure&lt;span class="w"&gt; &lt;/span&gt;-out&lt;span class="w"&gt; &lt;/span&gt;server.key
Enter&lt;span class="w"&gt; &lt;/span&gt;pass&lt;span class="w"&gt; &lt;/span&gt;phrase&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;server.key.secure:
writing&lt;span class="w"&gt; &lt;/span&gt;RSA&lt;span class="w"&gt; &lt;/span&gt;key
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally, create the certificate signing request. Most of the fields are arbitrary. The important one is the "Common Name" which should match the hostname of your server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;openssl&lt;span class="w"&gt; &lt;/span&gt;req&lt;span class="w"&gt; &lt;/span&gt;-new&lt;span class="w"&gt; &lt;/span&gt;-key&lt;span class="w"&gt; &lt;/span&gt;server.key.secure&lt;span class="w"&gt; &lt;/span&gt;-out&lt;span class="w"&gt; &lt;/span&gt;server.csr
Enter&lt;span class="w"&gt; &lt;/span&gt;pass&lt;span class="w"&gt; &lt;/span&gt;phrase&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;server.key.secure:
You&lt;span class="w"&gt; &lt;/span&gt;are&lt;span class="w"&gt; &lt;/span&gt;about&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;be&lt;span class="w"&gt; &lt;/span&gt;asked&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;enter&lt;span class="w"&gt; &lt;/span&gt;information&lt;span class="w"&gt; &lt;/span&gt;that&lt;span class="w"&gt; &lt;/span&gt;will&lt;span class="w"&gt; &lt;/span&gt;be&lt;span class="w"&gt; &lt;/span&gt;incorporated
into&lt;span class="w"&gt; &lt;/span&gt;your&lt;span class="w"&gt; &lt;/span&gt;certificate&lt;span class="w"&gt; &lt;/span&gt;request.
&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="w"&gt; &lt;/span&gt;truncated&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
Common&lt;span class="w"&gt; &lt;/span&gt;Name&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;e.g.&lt;span class="w"&gt; &lt;/span&gt;server&lt;span class="w"&gt; &lt;/span&gt;FQDN&lt;span class="w"&gt; &lt;/span&gt;or&lt;span class="w"&gt; &lt;/span&gt;YOUR&lt;span class="w"&gt; &lt;/span&gt;name&lt;span class="o"&gt;)&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;[]&lt;/span&gt;:launchbylunch.com
&lt;span class="o"&gt;[&lt;/span&gt;...&lt;span class="w"&gt; &lt;/span&gt;truncated&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now you should have three files:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;__server.key.secure __ - the private key&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;server.key&lt;/strong&gt; - the private key with the password removed&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;server.csr&lt;/strong&gt; - the certificate signing request for you key&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Next, you'll need to have your &lt;strong&gt;server.csr&lt;/strong&gt; signed by a certificate authority. I use &lt;a href="http://www.namecheap.com/?aff=60443"&gt;Namecheap&lt;/a&gt; as my registrar and they offer $1.99 SSL certifcates (&lt;em&gt;for one year&lt;/em&gt;) when you buy a domain. I've got a bunch of them accumulated so I used one of those.&lt;/p&gt;
&lt;p&gt;In 10-15 minutes you'll receive a confirmation email to prove domain ownership. Once that is complete you'll receive a reply from the certificate authority with the signed certificate. If they include an intermediate certificate (&lt;em&gt;ex: the Namecheap Positive SSL I describe above includes two&lt;/em&gt;) then concatenate them into a single &lt;strong&gt;server.crt&lt;/strong&gt;. &lt;/p&gt;
&lt;p&gt;Finaly upload &lt;strong&gt;server.key&lt;/strong&gt; and &lt;strong&gt;server.crt&lt;/strong&gt; to your server and place the two files in &lt;code&gt;~dokku/&amp;lt;app&amp;gt;/ssl&lt;/code&gt; and redeploy the app. In my case that's &lt;code&gt;~dokku/launchbylunch.com/ssl&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Upon startup Dokku will detect the SSL certificate and configure it's nginx router to redirect traffic from port 80 (HTTP) to 443 (HTTPS). It also does a good job of setting up nginx with &lt;a href="https://en.wikipedia.org/wiki/Perfect_forward_secrecy"&gt;PFS&lt;/a&gt;. The only thing it doesn't include out of the box is &lt;a href="https://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security"&gt;HSTS&lt;/a&gt; but we'll add that later.&lt;/p&gt;
&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;The end result of all this is that I have an easy to deploy to blog running for $10/mo. In reality I could host it for free (it's static content after all) but I like the control of running the server myself (&lt;em&gt;and it's way more fun&lt;/em&gt;). Plus, as it's just one of the many Dokku deployed apps on my server, the cost itself is shared, and I have an excuse to keep the server running.&lt;/p&gt;
&lt;p&gt;Pelican itself is really pleasant to use. Besides a couple hiccups in the initial setup I haven't had any issues with it and would recommend it to anyone else setting up a blog.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Did you try this out or do you have a more cool/fun/interesting setup for your site? &lt;a href="mailto:sehrope@jackdb.com"&gt;Tell me about it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content><category term="posts"></category><category term="meta"></category><category term="blog"></category><category term="pelican"></category><category term="dokku"></category><category term="git"></category><category term="digitalocean"></category></entry><entry><title>Encrypting Docker containers on a Virtual Server</title><link href="https://launchbylunch.com/posts/2014/Jan/13/encrypting-docker-on-digitalocean/" rel="alternate"></link><published>2014-01-13T00:00:00-05:00</published><updated>2014-01-13T00:00:00-05:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2014-01-13:/posts/2014/Jan/13/encrypting-docker-on-digitalocean/</id><summary type="html">&lt;p&gt;How to set up transparent encryption for all your Docker containers and Dokku apps with dm-crypt.&lt;/p&gt;</summary><content type="html">&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#why-encrypt"&gt;Why Encrypt?&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#limitations"&gt;Limitations&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#setup"&gt;Setup&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#install-packages"&gt;Install Packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#create-block-device-files"&gt;Create Block Device Files&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#setup-swap"&gt;Setup Swap&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#encrypted-block-device-setup"&gt;Encrypted Block Device Setup&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#docker-and-dokku-setup"&gt;Docker and Dokku Setup&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#bind-mounts"&gt;Bind Mounts&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#installation"&gt;Installation&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#destroying-everything"&gt;Destroying Everything&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#final-thoughts"&gt;Final Thoughts&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;p&gt;In my &lt;a href="https://launchbylunch.com/posts/2014/Jan/07/personal-paas-with-dokku-on-digital-ocean/"&gt;last post&lt;/a&gt; I went over setting up &lt;a href="https://github.com/progrium/dokku"&gt;Dokku&lt;/a&gt; on a &lt;a href="https://www.digitalocean.com/?refcode=eeda01627607"&gt;DigitalOcean&lt;/a&gt; droplet. In this post we'll improve that setup a bit by add some encrypted swap space. We'll also be encrypting all Dokku files, Docker containers, and the application log files for our deployed Dokku apps.&lt;/p&gt;
&lt;h2 id="why-encrypt"&gt;Why Encrypt?&lt;/h2&gt;
&lt;p&gt;Encryption is used to prevent unauthorized people from reading your data. When done at the file system level, encryption has the additional benefit that it makes it &lt;em&gt;much&lt;/em&gt; easier to dispose of the data on a disk. &lt;/p&gt;
&lt;p&gt;An import part of disposing of any server (&lt;em&gt;or more generally any hard drive&lt;/em&gt;) is &lt;a href="http://en.wikipedia.org/wiki/Disk_wiping#Importance"&gt;wiping the disks&lt;/a&gt;. With a cloud server, you are not in custody of the physical disks and securely wiping them becomes even more important. If you're not careful about it then &lt;a href="https://news.ycombinator.com/item?id=6983097"&gt;bad things&lt;/a&gt; can happen.&lt;/p&gt;
&lt;p&gt;To wipe an unencrypted disk you would need to over write the entire disk with zeroes (&lt;em&gt;or random gibberish from &lt;code&gt;/dev/urandom&lt;/code&gt;&lt;/em&gt;). Otherwise any data that was previously there could be &lt;a href="http://en.wikipedia.org/wiki/Undelete"&gt;retrieved&lt;/a&gt;. The larger the disk, the longer this will take. For example a 1 TB disk with a sequential write speed of 75 MB/s would take almost 4 hours to zero out.&lt;/p&gt;
&lt;p&gt;On the other hand, with an encrypted disk you can simply destroy the encryption keys and &lt;em&gt;nobody&lt;/em&gt; will be able to read the original data again. Since the encryption keys are relatively small (&lt;em&gt;ex: the header and key slots for &lt;a href="http://en.wikipedia.org/wiki/LUKS"&gt;LUKS&lt;/a&gt; total only a couple of MB&lt;/em&gt;), zeroing them out is very quick.&lt;/p&gt;
&lt;h2 id="limitations"&gt;Limitations&lt;/h2&gt;
&lt;p&gt;The steps in this guide will setup an encrypted block device. The encryption key for the block device will be stored in plaintext along side it. This is being done for convenience and will allow the virtual server to reboot without manual intervention (&lt;em&gt;i.e. we don't have to enter the pass phrase at boot time&lt;/em&gt;). &lt;/p&gt;
&lt;p&gt;&lt;em&gt;&lt;strong&gt;This is not secure!&lt;/strong&gt;&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Just to clarify further ... This is not really secure! Having the key stored in plain text alongside the encrypted block device is kind of like having a lock on your door and the key on the door step. If someone has access to your virtual server then they will be able to get the decryption keys. This includes any backup snapshots that archive the virtual server's entire disk.&lt;/p&gt;
&lt;p&gt;Still though, this is better than nothing and will allow you to more easily destroy your virtual server's disk when you're done using it. At the end of this post I'll go over some additional ideas to improve this configuration further.&lt;/p&gt;
&lt;h2 id="setup"&gt;Setup&lt;/h2&gt;
&lt;p&gt;A standard DigitalOcean droplet comes with a single disk allocated with the full space of the droplet. In our case it's approximately 30 GB:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;root@launchbylunch:~#&lt;span class="w"&gt; &lt;/span&gt;df&lt;span class="w"&gt; &lt;/span&gt;-h
Filesystem&lt;span class="w"&gt;           &lt;/span&gt;Size&lt;span class="w"&gt;  &lt;/span&gt;Used&lt;span class="w"&gt; &lt;/span&gt;Avail&lt;span class="w"&gt; &lt;/span&gt;Use%&lt;span class="w"&gt; &lt;/span&gt;Mounted&lt;span class="w"&gt; &lt;/span&gt;on
/dev/vda&lt;span class="w"&gt;              &lt;/span&gt;30G&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;6&lt;/span&gt;.1G&lt;span class="w"&gt;   &lt;/span&gt;22G&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;22&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;/
none&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;.0K&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;.0K&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;/sys/fs/cgroup
udev&lt;span class="w"&gt;                 &lt;/span&gt;494M&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;4&lt;/span&gt;.0K&lt;span class="w"&gt;  &lt;/span&gt;494M&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;/dev
tmpfs&lt;span class="w"&gt;                &lt;/span&gt;100M&lt;span class="w"&gt;  &lt;/span&gt;248K&lt;span class="w"&gt;  &lt;/span&gt;100M&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;/run
none&lt;span class="w"&gt;                 &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;.0M&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;5&lt;/span&gt;.0M&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;/run/lock
none&lt;span class="w"&gt;                 &lt;/span&gt;498M&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;7&lt;/span&gt;.0M&lt;span class="w"&gt;  &lt;/span&gt;491M&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;2&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;/run/shm
none&lt;span class="w"&gt;                 &lt;/span&gt;100M&lt;span class="w"&gt;     &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;  &lt;/span&gt;100M&lt;span class="w"&gt;   &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;%&lt;span class="w"&gt; &lt;/span&gt;/run/user
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Since we don't have a separate hard drive (&lt;em&gt;it's all one disk&lt;/em&gt;),  we'll be creating a file on the existing non-encrypted filesystem and using that as a block device (&lt;em&gt;via a &lt;a href="http://en.wikipedia.org/wiki/Loop_device"&gt;loop device&lt;/a&gt;&lt;/em&gt;).&lt;/p&gt;
&lt;h3 id="install-packages"&gt;Install Packages&lt;/h3&gt;
&lt;p&gt;There's only one package to install, &lt;code&gt;cryptsetup&lt;/code&gt;. Install it via &lt;code&gt;apt-get&lt;/code&gt;:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;install&lt;span class="w"&gt; &lt;/span&gt;cryptsetup
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="create-block-device-files"&gt;Create Block Device Files&lt;/h3&gt;
&lt;p&gt;Throughout this post I'll be using &lt;code&gt;$CRYPTFS_ROOT&lt;/code&gt; as the path on the unecrypted filesystem. If you'd like to have the encrypted block device and key stored elsewhere, simply modify the line that sets it's value.&lt;/p&gt;
&lt;p&gt;Create a directory at the root of the filesystem to hold our encrypted block device.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Location on the root filesystem where we&amp;#39;ll store everything:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;CRYPTFS_ROOT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/cryptfs

&lt;span class="c1"&gt;# Make the directory to store our encrypted block device:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, we'll create the encryption key for our block device. The key will be randomly generated from &lt;code&gt;/dev/urandom&lt;/code&gt;. For most purposes this should be fine but for a more secure setup this key should generated on a non-virtual server (&lt;em&gt;ex: your desktop&lt;/em&gt;) and uploaded to the virtual server. &lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a random encryption key:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;dd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/urandom&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/key&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;4K&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;1&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, we'll create a 2 GB swap file. We'll be initializing it with zeroes so that it gets fully allocated. This will take up to 10-20 seconds as it will need to write everything to disk.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create and allocate a 2 GB swap file:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;dd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/swap&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;2048&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Next, we'll create our encrypted block device. Since we don't really care about the existing data on the disk we'll create a sparse file using the &lt;code&gt;truncate&lt;/code&gt; command. Alternatively, if we wanted to overwrite any existing data on disk we could allocate it similar to how the swap file was allocated (&lt;em&gt;though this would take substantially longer&lt;/em&gt;).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a sparse 20 GB file:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;truncate&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;20G&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/disk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally we'll change the permissions on all these files so only &lt;code&gt;root&lt;/code&gt; can access them:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;chmod&lt;span class="w"&gt; &lt;/span&gt;-R&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;700&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="setup-swap"&gt;Setup Swap&lt;/h3&gt;
&lt;p&gt;Now we'll add an entry to your &lt;code&gt;/etc/crypttab&lt;/code&gt; for a swap file. We'll also add an entry to &lt;code&gt;/etc/fstab&lt;/code&gt; so that the swap is automatically enabled at each boot.&lt;/p&gt;
&lt;p&gt;The encryption key for this will be randomly chosen at each boot from &lt;code&gt;/dev/urandom&lt;/code&gt; and never saved. This means that if the virtual machine is powered off no application data that was written to swap will be recoverable (&lt;em&gt;that's a good thing!&lt;/em&gt;).&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Add a line to /etc/crypttab for our swap file:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;swap &lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;&lt;span class="s2"&gt;/swap /dev/urandom swap&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/crypttab

&lt;span class="c1"&gt;# Add swap entry to /etc/fstab:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/mapper/swap none swap defaults 0 0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/fstab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="encrypted-block-device-setup"&gt;Encrypted Block Device Setup&lt;/h3&gt;
&lt;p&gt;Next up we'll create a LUKS volume in our 20 GB file. To do so we'll first need to mount it using a loop device.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Pick an unused loop back device and save it to a variable:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;LOOP_DEVICE&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="k"&gt;$(&lt;/span&gt;losetup&lt;span class="w"&gt; &lt;/span&gt;-f&lt;span class="k"&gt;)&lt;/span&gt;

&lt;span class="c1"&gt;# Mount our block device file to it:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;losetup&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOOP_DEVICE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/disk
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we'll use &lt;code&gt;cryptsetup&lt;/code&gt; to format it as a LUKS volume. We'll be using the default settings for LUKS.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a LUKS volume with the default options using our key file&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;cryptsetup&lt;span class="w"&gt; &lt;/span&gt;--batch-mode&lt;span class="w"&gt; &lt;/span&gt;--key-file&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/key&lt;span class="w"&gt; &lt;/span&gt;luksFormat&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOOP_DEVICE&lt;/span&gt;

&lt;span class="c1"&gt;# Open it up so it appeares in /dev/mapper&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;cryptsetup&lt;span class="w"&gt; &lt;/span&gt;luksOpen&lt;span class="w"&gt; &lt;/span&gt;--key-file&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/key&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$LOOP_DEVICE&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;cryptfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The encrypted block device should now show up as &lt;code&gt;/dev/mapper/cryptfs&lt;/code&gt;. We can check the cipher details for it by running &lt;code&gt;cryptsetup status&lt;/code&gt;. It should look like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;cryptsetup&lt;span class="w"&gt; &lt;/span&gt;status&lt;span class="w"&gt; &lt;/span&gt;cryptfs
/dev/mapper/cryptfs&lt;span class="w"&gt; &lt;/span&gt;is&lt;span class="w"&gt; &lt;/span&gt;active&lt;span class="w"&gt; &lt;/span&gt;and&lt;span class="w"&gt; &lt;/span&gt;is&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;use.
&lt;span class="w"&gt;  &lt;/span&gt;type:&lt;span class="w"&gt;    &lt;/span&gt;LUKS1
&lt;span class="w"&gt;  &lt;/span&gt;cipher:&lt;span class="w"&gt;  &lt;/span&gt;aes-cbc-essiv:sha256
&lt;span class="w"&gt;  &lt;/span&gt;keysize:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;256&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;bits
&lt;span class="w"&gt;  &lt;/span&gt;device:&lt;span class="w"&gt;  &lt;/span&gt;/dev/loop0
&lt;span class="w"&gt;  &lt;/span&gt;loop:&lt;span class="w"&gt;    &lt;/span&gt;/cryptfs/disk
&lt;span class="w"&gt;  &lt;/span&gt;offset:&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="m"&gt;4096&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sectors
&lt;span class="w"&gt;  &lt;/span&gt;size:&lt;span class="w"&gt;    &lt;/span&gt;&lt;span class="m"&gt;41938944&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;sectors
&lt;span class="w"&gt;  &lt;/span&gt;mode:&lt;span class="w"&gt;    &lt;/span&gt;read/write
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now we'll create a filesystem on the encrypted block device. We're creating an ext4 filesystem:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;mkfs.ext4&lt;span class="w"&gt; &lt;/span&gt;/dev/mapper/cryptfs
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Finally lets create a mount point for it and add it to &lt;code&gt;/etc/crypttab&lt;/code&gt; and &lt;code&gt;/etc/fstab&lt;/code&gt; so that it gets automatically mounted at boot time.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Create a mount point for the encrypted filesystem:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/mnt/cryptfs

&lt;span class="c1"&gt;# Add to /etc/crypttab using the fixed key at $CRYPTFS_ROOT/key&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;cryptfs  &lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;&lt;span class="s2"&gt;/disk  &lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;&lt;span class="s2"&gt;/key  luks&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/crypttab

&lt;span class="c1"&gt;# Add it to /etc/fstab:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/dev/mapper/cryptfs  /mnt/cryptfs  ext4  defaults  0  1&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/fstab
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now that the setup is complete let's reboot the server once and test it out. If everything was properly set up, then after the reboot the encrypted filesystem should be mounted to &lt;code&gt;/mnt/cryptfs&lt;/code&gt; and we should have 2 GB of swap:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Verify that the encrypted block device was mounted:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;mount&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;grep&lt;span class="w"&gt; &lt;/span&gt;cryptfs
/dev/mapper/cryptfs&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/mnt/cryptfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ext4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw&lt;span class="o"&gt;)&lt;/span&gt;

$&lt;span class="w"&gt; &lt;/span&gt;free&lt;span class="w"&gt; &lt;/span&gt;-m
&lt;span class="w"&gt;             &lt;/span&gt;total&lt;span class="w"&gt;       &lt;/span&gt;used&lt;span class="w"&gt;       &lt;/span&gt;free&lt;span class="w"&gt;     &lt;/span&gt;shared&lt;span class="w"&gt;    &lt;/span&gt;buffers&lt;span class="w"&gt;     &lt;/span&gt;cached
Mem:&lt;span class="w"&gt;           &lt;/span&gt;&lt;span class="m"&gt;995&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="m"&gt;514&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="m"&gt;480&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="m"&gt;86&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="m"&gt;182&lt;/span&gt;
-/+&lt;span class="w"&gt; &lt;/span&gt;buffers/cache:&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="m"&gt;245&lt;/span&gt;&lt;span class="w"&gt;        &lt;/span&gt;&lt;span class="m"&gt;749&lt;/span&gt;
Swap:&lt;span class="w"&gt;         &lt;/span&gt;&lt;span class="m"&gt;2047&lt;/span&gt;&lt;span class="w"&gt;          &lt;/span&gt;&lt;span class="m"&gt;0&lt;/span&gt;&lt;span class="w"&gt;       &lt;/span&gt;&lt;span class="m"&gt;2047&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="docker-and-dokku-setup"&gt;Docker and Dokku Setup&lt;/h3&gt;
&lt;p&gt;At this point we have a mount point at &lt;code&gt;/mnt/cryptfs&lt;/code&gt; that will transparently encrypt and decrypt any data stored there. You could symlink other parts of your filesystem to subdirectories there to have them encrypted as well.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Make a directory in the encrypted filesystem&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;/mnt/cryptfs/foo

&lt;span class="c1"&gt;# Create a symlink pointing to it for /var/lib/foo:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;ln&lt;span class="w"&gt; &lt;/span&gt;-s&lt;span class="w"&gt; &lt;/span&gt;/mnt/cryptfs/foo&lt;span class="w"&gt; &lt;/span&gt;/var/lib/foo
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Unfortunately, this does not currently work with volume mounts for Docker containers(&lt;em&gt;see &lt;a href="https://github.com/dotcloud/docker/issues/2152"&gt;this issue&lt;/a&gt;&lt;/em&gt;). A work around though is to use &lt;a href="http://docs.1h.com/Bind_mounts"&gt;bind mounts&lt;/a&gt;. Bind mounts allow you to mount one part of a file system to another location. Conceptually they're very similar to symlinks.&lt;/p&gt;
&lt;h4 id="bind-mounts"&gt;Bind Mounts&lt;/h4&gt;
&lt;p&gt;We'll now add bind mounts to our &lt;code&gt;/etc/fstab&lt;/code&gt; for all the directories that we'd like to be stored on our encrypted filesystem. This will include our all our Docker and Dokku related files, all &lt;code&gt;/home&lt;/code&gt; directories, and the eventual location of nginx and our Dokku application log files. &lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; &lt;em&gt;This guide assumes that you've started with an empty server. Any existing data in &lt;code&gt;/home&lt;/code&gt; will be inaccessible after adding these bind mounts. A new empty home directory will take it's place. Also, this must be done &lt;strong&gt;before&lt;/strong&gt; you install Dokku or Docker as both of their installation directories will also be emptied.&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Loop through the paths that we&amp;#39;d like to be encrypted:&lt;/span&gt;
&lt;span class="k"&gt;for&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;DIR_NAME&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;home&lt;span class="w"&gt; &lt;/span&gt;var/lib/docker&lt;span class="w"&gt; &lt;/span&gt;var/lib/dokku&lt;span class="w"&gt; &lt;/span&gt;var/log/dokku&lt;span class="w"&gt; &lt;/span&gt;var/log/nginx
&lt;span class="k"&gt;do&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;Adding bind mount for DIR_NAME=&lt;/span&gt;&lt;span class="nv"&gt;$DIR_NAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Create the path on the encrypted filesystem (if it doesn&amp;#39;t exist)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/mnt/cryptfs/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DIR_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Create the path on the actual filesystem (if it doesn&amp;#39;t exist)&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;mkdir&lt;span class="w"&gt; &lt;/span&gt;-p&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/&lt;/span&gt;&lt;span class="nv"&gt;$DIR_NAME&lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;&lt;/span&gt;

&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="c1"&gt;# Add a bind mount entry to /etc/fstab for it:&lt;/span&gt;
&lt;span class="w"&gt;  &lt;/span&gt;&lt;span class="nb"&gt;echo&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;/mnt/cryptfs/&lt;/span&gt;&lt;span class="si"&gt;${&lt;/span&gt;&lt;span class="nv"&gt;DIR_NAME&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; /&lt;/span&gt;&lt;span class="nv"&gt;$DIR_NAME&lt;/span&gt;&lt;span class="s2"&gt; none bind 0 0&amp;quot;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;/etc/fstab
&lt;span class="k"&gt;done&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;&lt;strong&gt;Updated @ 2014/Mar/07:&lt;/strong&gt; &lt;em&gt;A previous version of this post incorrectly had the directories for the bind mount backwards. Thanks to John (&lt;a href="https://github.com/ubergarm"&gt;https://github.com/ubergarm&lt;/a&gt;) for pointing this out!&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;Now let's reboot one more time. After rebooting, verify that everything was mounted properly.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;mount
/dev/vda&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ext4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw,errors&lt;span class="o"&gt;=&lt;/span&gt;remount-ro&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="w"&gt; &lt;/span&gt;truncated&lt;span class="w"&gt; &lt;/span&gt;...&lt;span class="o"&gt;]&lt;/span&gt;
/dev/mapper/cryptfs&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/mnt/cryptfs&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ext4&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw&lt;span class="o"&gt;)&lt;/span&gt;
/mnt/cryptfs/home&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/home&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw,bind&lt;span class="o"&gt;)&lt;/span&gt;
/mnt/cryptfs/var/lib/docker&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/var/lib/docker&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw,bind&lt;span class="o"&gt;)&lt;/span&gt;
/mnt/cryptfs/var/lib/dokku&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/var/lib/dokku&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw,bind&lt;span class="o"&gt;)&lt;/span&gt;
/mnt/cryptfs/var/log/dokku&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/var/log/dokku&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw,bind&lt;span class="o"&gt;)&lt;/span&gt;
/mnt/cryptfs/var/log/nginx&lt;span class="w"&gt; &lt;/span&gt;on&lt;span class="w"&gt; &lt;/span&gt;/var/log/nginx&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;type&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;none&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;rw,bind&lt;span class="o"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="installation"&gt;Installation&lt;/h4&gt;
&lt;p&gt;Now we can install Dokku and Docker. Follow the instructions from my &lt;a href="https://launchbylunch.com/posts/2014/Jan/07/personal-paas-with-dokku-on-digital-ocean/"&gt;previous post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Once the installation is complete, our Docker containers, deployed applications, and all their config environment variables will be persisted to a transparently encrypted filesystem. Additionally, all our applications log files in &lt;code&gt;/var/log/dokku&lt;/code&gt; will also be encrypted (&lt;em&gt;I'll be going over how to get Dokku to send application logs there in a later post&lt;/em&gt;).&lt;/p&gt;
&lt;h2 id="destroying-everything"&gt;Destroying Everything&lt;/h2&gt;
&lt;p&gt;When we want to deprovision the server we can prevent the encryted data from ever being read again by destroying the encryption key and the LUKS header on the block device.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WARNING:&lt;/strong&gt; &lt;em&gt;Unless you've created a backup of your encryption key and the LUKS header there is no way to undo this operation!&lt;/em&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Shred the encryption key:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;shred&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/key

&lt;span class="c1"&gt;# Overwrite the beginning of the encrypted block device where the LUKS header is:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;dd&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/dev/zero&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;of&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$CRYPTFS_ROOT&lt;/span&gt;/disk&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;bs&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1M&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;count&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="m"&gt;10&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="final-thoughts"&gt;Final Thoughts&lt;/h2&gt;
&lt;p&gt;The biggest hole in all of this is the fact that the encryption key is stored in plaintext alongside the block device file. Unfortunately that's the price to pay for unattended restarts. For a local physical server (&lt;em&gt;ex: your desktop&lt;/em&gt;) that is rarely restarted, or one that you have direct console access to, entering the passphrase at each boot is not much of an issue. For a virtual server though, it's an extra step that either needs to be done from the virtual console or via a multi-step boot process (&lt;em&gt;boot, SSH, unlock the encrypted filesystem, resume boot ...&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;One solution to this that I've been considering is a hybrid approach. The server would be setup similar to this post, however rather than having the key file stored locally it would be requested at boot time from an external service.&lt;/p&gt;
&lt;p&gt;The external service could notify you of the restart (&lt;em&gt;ex: on the service's website or via a push notification to your smart phone&lt;/em&gt;). If you approve the request then it would respond with the encryption key, the server would mount the encrypted filesystem, and then resume the boot process. Any request not explicitly approved in X minutes would be automatically rejected. Additionaly once the encrypted filesystem is mounted the server would discard the encryption key (&lt;em&gt;so it would only be in memory while the server remains on&lt;/em&gt;).&lt;/p&gt;
&lt;p&gt;I think that would strike a good balance between security and usability.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Have an idea of how to improve this setup or have a working version of what I'm describing? &lt;a href="mailto:sehrope@jackdb.com"&gt;Tell me about it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content><category term="posts"></category><category term="docker"></category><category term="dokku"></category><category term="digitalocean"></category><category term="crypto"></category><category term="security"></category></entry><entry><title>How to set up a private PaaS with Dokku on DigitalOcean</title><link href="https://launchbylunch.com/posts/2014/Jan/07/personal-paas-with-dokku-on-digital-ocean/" rel="alternate"></link><published>2014-01-07T00:00:00-05:00</published><updated>2014-01-07T00:00:00-05:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2014-01-07:/posts/2014/Jan/07/personal-paas-with-dokku-on-digital-ocean/</id><summary type="html">&lt;p&gt;How to setup your own personal PaaS for $10/mo with Dokku on DigitalOcean&lt;/p&gt;</summary><content type="html">&lt;div class="toc"&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="#background"&gt;Background&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#domain-and-server-provisioning"&gt;Domain and Server Provisioning&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#register-a-domain-name"&gt;Register a Domain Name&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#dns"&gt;DNS&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ssh"&gt;SSH&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#ssh-key-creation"&gt;SSH Key Creation&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#ssh-config"&gt;SSH Config&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#digitalocean"&gt;DigitalOcean&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#create-a-new-droplet"&gt;Create a new droplet&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#digitalocean-dns"&gt;DigitalOcean DNS&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#connect-to-the-droplet"&gt;Connect to the droplet&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#server-setup"&gt;Server Setup&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#update-system-packages"&gt;Update System Packages&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#install-dokku-docker"&gt;Install Dokku &amp;amp; Docker&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#sample-application"&gt;Sample Application&lt;/a&gt;&lt;ul&gt;
&lt;li&gt;&lt;a href="#create"&gt;Create&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#deploy"&gt;Deploy&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#run"&gt;Run&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/li&gt;
&lt;li&gt;&lt;a href="#logs"&gt;Logs&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="#more-information"&gt;More information&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;/div&gt;
&lt;h2 id="background"&gt;Background&lt;/h2&gt;
&lt;p&gt;&lt;a href="https://github.com/progrium/dokku"&gt;Dokku&lt;/a&gt; is a &lt;a href="https://www.docker.io/"&gt;Docker&lt;/a&gt; powered mini-Heroku. Once it's installed on a server you can perform &lt;em&gt;git push&lt;/em&gt; style deployments to your own private PaaS (Platform as a Service).&lt;/p&gt;
&lt;p&gt;In this post we'll go over how to set it up on a $10/mo 1GB &lt;a href="https://www.digitalocean.com/?refcode=eeda01627607"&gt;DigitalOcean&lt;/a&gt; droplet.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;At the time of this writing, both Dokku and Docker are still in beta and not meant for production usage.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id="domain-and-server-provisioning"&gt;Domain and Server Provisioning&lt;/h2&gt;
&lt;h3 id="register-a-domain-name"&gt;Register a Domain Name&lt;/h3&gt;
&lt;p&gt;If you already have a domain name you'll be using, then you can skip this step. If not, then go register one (I use &lt;a href="http://www.namecheap.com/?aff=60443"&gt;Namecheap&lt;/a&gt;).&lt;/p&gt;
&lt;p&gt;For the purposes of this post I'll be using the domain for this site "launchbylunch.com".&lt;/p&gt;
&lt;h3 id="dns"&gt;DNS&lt;/h3&gt;
&lt;p&gt;Once you have your domain name, edit your name servers to use DigitalOcean's. On Namecheap, once you've clicked on the management page for your domain, you can find the nameserver menu under &lt;strong&gt;Domain Name Server Setup&lt;/strong&gt; on the left.&lt;/p&gt;
&lt;p&gt;&lt;img src="/images/namecheap-digitalocean-dns.png" alt="Namecheap DigitalOcean nameserver setup" title="Namecheap DigitalOcean nameserver setup" /&gt;&lt;/p&gt;
&lt;p&gt;Alternatively, you can stick to using your registrar's nameservers and setup the wild card CNAME handle in the next section there. Doing it through DigitalOcean is just a bit easier as they'll already have your server details.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note:&lt;/strong&gt; &lt;em&gt;Either way it may take up to an hour for your DNS changes to propagate.&lt;/em&gt;&lt;/p&gt;
&lt;h3 id="ssh"&gt;SSH&lt;/h3&gt;
&lt;h4 id="ssh-key-creation"&gt;SSH Key Creation&lt;/h4&gt;
&lt;p&gt;We'll be using SSH to connect to our server and it's a best practice to use SSH keys for authentication rather than passwords. Generate a new pair of SSH keys for use with your DigitalOcean account by running &lt;code&gt;ssh-keygen&lt;/code&gt; on your local machine.&lt;/p&gt;
&lt;p&gt;It'll prompt you for a name for the SSH key files (specify &lt;code&gt;id_rsa_digital_ocean&lt;/code&gt;) and a passphrase (&lt;a href="https://xkcd.com/936/"&gt;&lt;em&gt;pick something long you can remember&lt;/em&gt;&lt;/a&gt;) to secure the private key file.&lt;/p&gt;
&lt;p&gt;It should look something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;ssh-keygen
Generating&lt;span class="w"&gt; &lt;/span&gt;public/private&lt;span class="w"&gt; &lt;/span&gt;rsa&lt;span class="w"&gt; &lt;/span&gt;key&lt;span class="w"&gt; &lt;/span&gt;pair.
Enter&lt;span class="w"&gt; &lt;/span&gt;file&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;which&lt;span class="w"&gt; &lt;/span&gt;to&lt;span class="w"&gt; &lt;/span&gt;save&lt;span class="w"&gt; &lt;/span&gt;the&lt;span class="w"&gt; &lt;/span&gt;key&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;(&lt;/span&gt;/home/sehrope/.ssh/id_rsa&lt;span class="o"&gt;)&lt;/span&gt;:&lt;span class="w"&gt; &lt;/span&gt;id_rsa_digital_ocean
Your&lt;span class="w"&gt; &lt;/span&gt;identification&lt;span class="w"&gt; &lt;/span&gt;has&lt;span class="w"&gt; &lt;/span&gt;been&lt;span class="w"&gt; &lt;/span&gt;saved&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;id_rsa_digital_ocean.
Your&lt;span class="w"&gt; &lt;/span&gt;public&lt;span class="w"&gt; &lt;/span&gt;key&lt;span class="w"&gt; &lt;/span&gt;has&lt;span class="w"&gt; &lt;/span&gt;been&lt;span class="w"&gt; &lt;/span&gt;saved&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;id_rsa_digital_ocean.pub.
The&lt;span class="w"&gt; &lt;/span&gt;key&lt;span class="w"&gt; &lt;/span&gt;fingerprint&lt;span class="w"&gt; &lt;/span&gt;is:
e2:44:e3:0b:52:86:40:2a:bd:5d:63:70:a3:88:01:ef&lt;span class="w"&gt; &lt;/span&gt;sehrope@ls01
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;I like to keep all my SSH keys in the &lt;code&gt;~/.ssh&lt;/code&gt; directory so to lets move them there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;mv&lt;span class="w"&gt; &lt;/span&gt;id_rsa_digital_ocean*&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/.
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h4 id="ssh-config"&gt;SSH Config&lt;/h4&gt;
&lt;p&gt;Normally, to log in to a server using an SSH key you need to specify it on the command line via &lt;code&gt;-i&lt;/code&gt;. Rather than specifying it everytime, you can also add default SSH settings to your SSH config file, &lt;code&gt;~/.ssh/config&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Edit the file (or create it if it doesn't exist) and add an entry for your domain name:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;Host launchbylunch.com
  User root
  IdentityFile ~/.ssh/id_rsa_digital_ocean
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Now, whenever I SSH to launchbylunch.com it'll default to using that specific SSH key file for authentication. I've also specified the default user to connect to on the remote server.&lt;/p&gt;
&lt;p&gt;As an added bonus, if you have shell completions setup properly then you can just type &lt;code&gt;ssh l&lt;/code&gt; and then hit tab and it'll autocomplete server names for you.&lt;/p&gt;
&lt;h3 id="digitalocean"&gt;DigitalOcean&lt;/h3&gt;
&lt;p&gt;If you haven't done so already, create an account with &lt;a href="https://www.digitalocean.com/?refcode=eeda01627607"&gt;DigitalOcean&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Next, from the account menu click &lt;strong&gt;SSH Keys&lt;/strong&gt;. Then pick and name for your key and copy/paste the &lt;em&gt;public&lt;/em&gt; key file (&lt;code&gt;~/.ssh/id_rsa_digital_ocean.pub&lt;/code&gt;) into the text box and save it.&lt;/p&gt;
&lt;h4 id="create-a-new-droplet"&gt;Create a new droplet&lt;/h4&gt;
&lt;p&gt;Log in to &lt;a href="https://www.digitalocean.com/?refcode=eeda01627607"&gt;DigitalOcean&lt;/a&gt; and the &lt;strong&gt;Create&lt;/strong&gt; buton to create a new droplet.&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;The default size is 512mb, change it to 1GB&lt;/li&gt;
&lt;li&gt;If you'd like pick a region that is closer to you.&lt;/li&gt;
&lt;li&gt;Select the Ubuntu 13.04 x64 image&lt;/li&gt;
&lt;li&gt;Click to select the SSH key you added.&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;Create Droplet&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;In about a minute the droplet should be created. Make a note of its IP address.&lt;/p&gt;
&lt;h4 id="digitalocean-dns"&gt;DigitalOcean DNS&lt;/h4&gt;
&lt;p&gt;The final part of our setup will be to setup a wildcard DNS entry for our domain.&lt;/p&gt;
&lt;p&gt;From the main DigitalOcean menu click &lt;strong&gt;DNS&lt;/strong&gt; then:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Enter your domain (ex: launchbylunch.com)&lt;/li&gt;
&lt;li&gt;Click to select the droplet you just created&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;CREATE DOMAIN&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;This will create a DNS A record for your root domain pointing to your newly created droplet. To allow apps to be hosted on the sub domain, we'll need to add a wildcard CNAME entry. From the DNS menu of your domain:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Click &lt;strong&gt;Add Record&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;For Record Type select &lt;strong&gt;CNAME&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;For the &lt;strong&gt;Name&lt;/strong&gt; enter &lt;code&gt;*&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;For the &lt;strong&gt;Hostname&lt;/strong&gt; enter &lt;code&gt;@&lt;/code&gt;&lt;/li&gt;
&lt;li&gt;Click &lt;strong&gt;CREATE&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Once you've completed this step it should look something like this:&lt;/p&gt;
&lt;p&gt;&lt;img src="/images/digitalocean-dns.png" alt="DigitalOcean DNS" title="DigitalOcean DNS" /&gt;&lt;/p&gt;
&lt;h3 id="connect-to-the-droplet"&gt;Connect to the droplet&lt;/h3&gt;
&lt;p&gt;Now let's see if everything is setup properly. From the command line try to connect to it via SSH:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;launchbylunch.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If everything is setup properly then you should be connected. If not, then it's possible your DNS has not propagated yet. Use either &lt;code&gt;nslookup&lt;/code&gt; or &lt;code&gt;dig&lt;/code&gt; to verify if your DNS has propagated:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;nslookup&lt;span class="w"&gt; &lt;/span&gt;launchbylunch.com
Non-authoritative&lt;span class="w"&gt; &lt;/span&gt;answer:
Name:&lt;span class="w"&gt; &lt;/span&gt;launchbylunch.com
Address:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;162&lt;/span&gt;.243.228.51
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To check the wildcard CNAME, prefix anything in front of your domain:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;nslookup&lt;span class="w"&gt; &lt;/span&gt;foobar.launchbylunch.com
Non-authoritative&lt;span class="w"&gt; &lt;/span&gt;answer:
foobar.launchbylunch.com&lt;span class="w"&gt;  &lt;/span&gt;canonical&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nv"&gt;name&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;launchbylunch.com.
Name:&lt;span class="w"&gt; &lt;/span&gt;launchbylunch.com
Address:&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="m"&gt;162&lt;/span&gt;.243.228.51
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="server-setup"&gt;Server Setup&lt;/h2&gt;
&lt;h3 id="update-system-packages"&gt;Update System Packages&lt;/h3&gt;
&lt;p&gt;Now that you're connected to your server first update and upgrade your system packages. If you followed the instructions thus far then you'll be logged in as &lt;code&gt;root&lt;/code&gt; and will not need to prefix your commands with &lt;code&gt;sudo&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To update your droplet run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;update&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;apt-get&lt;span class="w"&gt; &lt;/span&gt;-y&lt;span class="w"&gt; &lt;/span&gt;upgrade
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;After the first update I like to reboot the server once to make sure everything is working fine. From the command line run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;reboot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="install-dokku-docker"&gt;Install Dokku &amp;amp; Docker&lt;/h3&gt;
&lt;p&gt;Once your server reboots connect again via SSH. We'll now be installing the latest development version of Dokku directly from GitHub:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;&lt;span class="c1"&gt;# Copy the installation script locally:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;wget&lt;span class="w"&gt; &lt;/span&gt;-qO-&lt;span class="w"&gt; &lt;/span&gt;https://raw.github.com/progrium/dokku/master/bootstrap.sh&lt;span class="w"&gt; &lt;/span&gt;&amp;gt;&lt;span class="w"&gt; &lt;/span&gt;dokku-setup.sh&lt;span class="w"&gt;    &lt;/span&gt;
&lt;span class="c1"&gt;# Run it:&lt;/span&gt;
$&lt;span class="w"&gt; &lt;/span&gt;bash&lt;span class="w"&gt; &lt;/span&gt;dokku-setup.sh
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will take a few minutes. Once it completes, Docker and Dokku will be installed on your droplet and a &lt;code&gt;dokku&lt;/code&gt; user will have been created. This &lt;code&gt;dokku&lt;/code&gt; user will handle your app deployments.&lt;/p&gt;
&lt;p&gt;Next, from your local machine (&lt;em&gt;not the droplet terminal&lt;/em&gt;) add your SSH key to the remote &lt;code&gt;dokku&lt;/code&gt; user. The last parameter &lt;code&gt;me@mycomp&lt;/code&gt; is a descriptive name so put whatever you'd like there.&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;cat&lt;span class="w"&gt; &lt;/span&gt;~/.ssh/id_rsa_digital_ocean.pub&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;|&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;ssh&lt;span class="w"&gt; &lt;/span&gt;launchbylunch.com&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;&amp;quot;sudo sshcommand acl-add dokku me@mycomp&amp;quot;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="sample-application"&gt;Sample Application&lt;/h2&gt;
&lt;h3 id="create"&gt;Create&lt;/h3&gt;
&lt;p&gt;To test out your new private PaaS let's create a project. We'll use the Heroku Node.js sample application. Run the following on your local machine to clone it:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;clone&lt;span class="w"&gt; &lt;/span&gt;https://github.com/heroku/node-js-sample
$&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nb"&gt;cd&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;node-js-sample
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Then, add your new Dokku powered PaaS as a git remote:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;remote&lt;span class="w"&gt; &lt;/span&gt;add&lt;span class="w"&gt; &lt;/span&gt;production&lt;span class="w"&gt; &lt;/span&gt;dokku@launchbylunch.com:my-new-app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="deploy"&gt;Deploy&lt;/h3&gt;
&lt;p&gt;Finally, to push your application to your private PaaS:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;git&lt;span class="w"&gt; &lt;/span&gt;push&lt;span class="w"&gt; &lt;/span&gt;production&lt;span class="w"&gt; &lt;/span&gt;master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;If everything is setup properly, then Dokku should receive the git push and start building your app. Once it's completed it will print something like this:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;-----&amp;gt; Deploying my-new-app ...
=====&amp;gt; Application deployed:
       http://my-new-app.launchbylunch.com

To dokku@launchbylunch.com:my-new-app
 * [new branch]      master -&amp;gt; master
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h3 id="run"&gt;Run&lt;/h3&gt;
&lt;p&gt;If everything deployed properly, then you should be able to test out your newly deployed app.&lt;/p&gt;
&lt;p&gt;Open the &lt;a href="http://my-new-app.launchbylunch.com"&gt;link&lt;/a&gt; for your app an you should see &lt;code&gt;Hello World!&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;To deploy a new version, simply commit it locally and push the changes again via git.&lt;/p&gt;
&lt;h2 id="logs"&gt;Logs&lt;/h2&gt;
&lt;p&gt;By default Dokku apps send all logging output to stdout. This gets saved by Docker to a single file per container. To see your application logs, SSH to your server and run:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre&gt;&lt;span&gt;&lt;/span&gt;&lt;code&gt;$&lt;span class="w"&gt; &lt;/span&gt;dokku&lt;span class="w"&gt; &lt;/span&gt;logs&lt;span class="w"&gt; &lt;/span&gt;my-new-app
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;h2 id="more-information"&gt;More information&lt;/h2&gt;
&lt;p&gt;In a later post I'll go into a Dokku plugin that allows you to run multiple process types as well as a better way to deal with logging.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;Are you building something cool with Docker or Dokku? &lt;a href="mailto:sehrope@jackdb.com"&gt;Tell me about it&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;</content><category term="posts"></category><category term="docker"></category><category term="dokku"></category><category term="digitalocean"></category><category term="paas"></category></entry><entry><title>New Year, New Blog</title><link href="https://launchbylunch.com/posts/2014/Jan/01/new-year-new-blog/" rel="alternate"></link><published>2014-01-01T00:00:00-05:00</published><updated>2014-01-01T00:00:00-05:00</updated><author><name>Sehrope Sarkuni</name></author><id>tag:launchbylunch.com,2014-01-01:/posts/2014/Jan/01/new-year-new-blog/</id><summary type="html">&lt;p&gt;A new blog to start off 2014.&lt;/p&gt;</summary><content type="html">&lt;p&gt;This site will serve as my new blog.&lt;/p&gt;
&lt;p&gt;I'll be writing about databases, app development, devops, and everything in between.&lt;/p&gt;
&lt;p&gt;The blog itself is static site generated via &lt;a href="http://getpelican.com/"&gt;Pelican&lt;/a&gt;, deployed via &lt;a href="https://github.com/progrium/dokku"&gt;Dokku&lt;/a&gt; (into a &lt;a href="https://www.docker.io/"&gt;Docker&lt;/a&gt; container), served via &lt;a href="http://nginx.org/"&gt;nginx&lt;/a&gt;, and it all runs on a &lt;a href="https://www.digitalocean.com/?refcode=eeda01627607"&gt;DigitalOcean&lt;/a&gt; droplet.&lt;/p&gt;</content><category term="posts"></category></entry></feed>