When first setting up some containers on a new project, you might be using 2 or 3 tools, and decide to just take some "official" image from Docker Hub as a baseline for a container.

Official Docker images are pretty great when you have very little you want to configure. They're often more than sufficient for prototyping when it comes to things that are otherwise fidget-y to set up, like a database.

But a common pattern emerges when using these tools:

  • You follow along with the quickstart, and the vanilla setup works
  • You start adding your code and changes, and suddenly things don't work as well
  • You try to debug it, and discover the 10 startup scripts added to the project that can configure things
  • You look through issue trackers, hoping to see someone else with your issue

These official images feel nice because you can get things running quickly, but they are a pain to debug as a beginner because you barely know where to start looking. How are you going to debug the default nginx config added by the image when you don't even know where that is?

One coping mechanism for this is what I call "operation via subtraction". You take the entire official Dockerfile and its files, make that your own, and then remove things you believe you don't need.

At this point you are still in a tricky place. You probably have a broken container as the result of 20 operations, and chipping away at it you sill end up with a broken container, just as a result of 10 operations instead.

These images are tempting because it's, frankly, annoying to deal with stuff like apt and project-hosted repos, and other futzy details. Many of these Dockerfile are filled with stuff a step removed from line noise, and ultimately it feels like stuff that you should be able to ignore.

But really a lot of these images are going to be Alpine or Debian. And then you don't use the Alpine image for whatever reason, so they all end up being Debian at their core.

Instead of looking at how to use some piece of software via Docker, in many cases it will end up being easier in the mid-term to look up how to use some software in Debian. Even then, the biggest challenge is that your container wants one command, so if you want to run N pieces of software you'll need to mess around with something like Supervisor, on top of everything else.

And while doing this, the existing containers exist as templates for how to do things, but you will feel way less pressure to follow this line along, if you are starting from scratch and adding everything yourself.

Here is an OK workflow that should let you approach building a Dockerfile in a way closer to just trying to get a single instance working, rather than trying to be too principled about it.

First create a Dockerfile:

# this or debian or w/e you are comfortable with
FROM ubuntu:23.04
# you're probably going to want the following in here at one point
RUN apt update

Inside a script called run_container.sh, in the same directory as your Dockerfile, put something like the following:

#!/usr/bin/env bash
set -eo pipefail
docker build -t wip-container .
docker run -v $(pwd):/root/cwd -p 8080:8080 -it wip-container bash

This sets up the container so the current working directory is mounted to /root/cwd, and port 8080 is opened up. Feel free to hardcode the mount point, but the core idea is to not only encode your container setup, but how you are running it as a host.

Some might use Docker Compose for the above, I think it's great, but after you've moved beyond the constant rebuilds that you'll want to do on your first iteration.

chmod +x run_container.sh, then do the following:

  • Run ./run_container.sh to get into a shell.
  • Run some commands to try and get the container set up.
  • If you need files, create them in the same directory you ran the command in. Inside the container the files will be present in /root/cwd. You can just edit these files in the host.
  • When you feel like you have gotten somewhere, checkpoint by running the following in the container:
history | sed -E 's/^ +[0-9]+/RUN /'

This will spit out all the commands you've run, prefixed with RUN. You can then paste that into your Dockerfile. Remove any cruft (but only if you really really know it's cruft).

Once you've done that, quit from the shell, and then do ./run_container.sh again, to see if what you wanted work. Poke at it a bit more, add any ports you might need, etc etc.

At one point your thing is set up nicely. Add any EXPOSE or CMD things you want in your Dockerfile, and now you have something up and running!

This will take a bit more time, but it will be much less of a pain to debug if you have any issues, and you'll have a good idea of how things are working.