Tadas Sasnauskas Tech/Engineering Blog

Developer friendly multi-mode Docker image entrypoint

Context

Most reusable Docker images from Docker Hub or similar repositories are designed to run a single process (e.g. database, web server, cache storage). Expectation is that you can run it with reasonable defaults using docker run <image>:latest. And most of customisation is done through environment variables. That default mode command most of the time will be configured through ENTRYPOINT.

In case of Rails apps we often find ourselves using the same Docker image to launch multiple process types: web request serving process, delayed job or sidekiq job queue and console process to do some investigation/debugging. Some call it a multi-mode image.

Because of that we do not have a single default command, but rather these:

bundle exec rails server -b 0.0.0.0
bundle exec sidekiq

Occasionally we also use these one-off commands as well:

bundle exec rails db:migrate
bundle exec rails console

And sometimes, we may occasionally run some arbitrary commands to start a shell or inspect docker image itself:

/bin/bash
find /app

In these cases developers often do away with ENTRYPOINT and CMD leaving responsibility of providing correct command to whoever is running the docker image.

Challenge

Can we pack this mode “knowledge” into the image whilst allowing easy arbitrary commands?

Solution

Use entry point script with selectable mode and fallback to regular commands:

#!/usr/bin/env bash

set -e

case $1 in
  "migrate")
    exec bundle exec rails db:migrate
    ;;
  "console")
    exec bundle exec rails console
    ;;
  "web")
    exec bundle exec rails server -b 0.0.0.0
    ;;
  "sidekiq")
    exec bundle exec sidekiq
    ;;
  *)
    # Otherwise just shell exec the whole thing
    exec bash -c "${*}"
    ;;
esac

With Dockerfile:

# ... skipped ..
ENTRYPOINT ["/app/bin/docker-entrypoint.sh"]
CMD ["console"]

With this our development environment docker-compose.yml becomes more like:

services:
  web:
    command: web
  sidekiq:
    command: sidekiq

And production k8s deployment:

apiVersion: apps/v1
kind: Deployment
spec:
  template:
    spec:
      containers:
        - name: web
          args: ["web"]
        - name: sidekiq
          args: ["sidekiq"]

As you can see mode commands are now contained in a single place and do not spill over into infrastructure code/configuration.

Meanwhile developers are able to easily run mode or arbitrary commands without overriding entrypoint or similar.

Mode command example:

docker-compose run web console

Arbitrary command examples:

docker-compose run web /bin/bash
docker-compose run web bundle exec rails routes

All in all a seemingly simple change that brings noticeable day to day convenience!