jameshunt (.us)

Don’t Just Develop FOR Docker — or — Develop WITH It

A lot of words get written about how Docker revolutionizes delivery of the output of the software development process. Without a doubt, containers-as-super-packages are phenomenally useful for wrapping up binaries with configuration files, helper programs, shared libraries, and language runtimes. I’m not here to talk about that today.

No, I want to talk about how I use Docker while writing software–and how you can too!

Pulling in Libraries

Here’s a Dockerfile that makes my life easier when I’m working in Perl:

FROM ubuntu:20.04

RUN apt-get update \
 && apt-get install -y carton \
 && rm -rf /var/lib/apt/lists

WORKDIR /app
COPY cpanfile .
RUN carton install

# start here when you figure out how to run the Perl-y thing...

Here’s what I do for Common Lisp work:

FROM ubuntu:20.04

RUN apt-get update \
 && apt-get install -y sbcl \
 && rm -rf /var/lib/apt/lists

ADD https://beta.quicklisp.org/quicklisp.lisp /tmp

# start here when you figure out how to run the Lisp-y thing...

These “partial images” contain the seeds of a real production deployment, but one that can be used to incrementally develop the software in the same environment.

It gets even more interesting when we start relying on libraries that are outside of the purview of the language libraries themselves; usually because they are written in C. For example, if I add hunchentoot, a pure-Lisp HTTP server, to my Common Lisp project, it will pull in ironclad, for TLS capabilities, which depends on OpenSSL.

To accommodate, I just have to amend the apt-get install command from this:

RUN apt-get update \
 && apt-get install -y sbcl \
 && rm -rf /var/lib/apt/lists

to this:

RUN apt-get update \
 && apt-get install -y sbcl libssl-dev \
 && rm -rf /var/lib/apt/lists

Surrounding Infrastructure

Who builds an application that doesn’t handle data? I usually need either a Redis cluster, a PostgreSQL instance, or at least a persistent file system to keep stuff around in between application server reboots. For that, I use Docker Compose and off-the-shelf images.

For example, the other day I was building a content-addressable blob store based on SHA-3 hashing. Blobs live in files under the file system, named after the checksum of their contents. I also needed the ability to group blobs arbitrarily (via tags) and store keyed metadata (file size, original file name, etc.). Metadata lives in a PostgreSQL relational database.

To spin everything up reliably, I wrote a Compose recipe. Here it is:

version: '3'
services:
  db:
    image: postgres
    environment:
      POSTGRES_PASSWORD: sekrit
    volumes:
      ./_/pg:/var/lib/postgresql/pgdata
      ./schema.sql:/docker-entrypoint-initdb.d/10-schema.sql

  api:
    build:
      context: .
    environment:
      DB_HOST: db
    volumes:
      - ./_/store:/store
      - ./api:/app:ro
    ports:
      - 7000:7000

I get two things out of this: (1) a persistent database, and (2) a place to execute code in a REPL (this is a Common Lisp project). Should I want to start over, I can drop the containers, wipe the persistent storage directory (_) and start it back up again:

$ docker-compose down
$ sudo rm -rf _
$ docker-compose up -d

Faster Iteration

A major advantage Lisps have over other languages is the utility of the REPL–an interactive top-level expression evaluator. With a spinning api container, I can access the REPL and play around with code as it evolves:

$ docker exec -it the_api_1 sbcl
This is SBCL 2.0.1.debian, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
* (+ 1 2 3)
6

This is handy, because it allows me to edit files outside of the container (The ./api:/app volume mount does that), while still relying on in situ execution, evaluation, and exploration.