My attempt at building a production grade docker image

📅 Published: Thomas Queste

My attempt at building a production grade docker image

As I am self-hosting a couple of services, mainly for keeping my data for myself (Sorry Google, Facebook), I tried to build a “production-grade docker image”. Here’s my attempt and what I learnt along the way.

Radicale

The first service I dockerized is Radicale, a calendar/contact server (CalDav/CardDav).

Radicale is a good choice for a start due to its simplicity:

  • written in python
  • filesystem database
  • single config file
  • runnable directly with radicale
  • available in PyPI (pip install radicale)

If we wanted to stop here, this Dockerfile is sufficient:

FROM debian:jessie

ENV DEBIAN_FRONTEND noninteractive
RUN apt-get update \
    && apt-get install -y python2.7 python-pip \
    && rm -rf /var/lib/apt/lists/*

RUN pip install radicale

CMD ["radicale"]

Easy: Use a smaller base image

I started with a Debian base image, then switched to an Alpine image, then found there are even alpine+python images.

The official Python images have an Alpine version: https://hub.docker.com/_/python/

I did not set a specific image version (eg. python:3.5.2-alpine) in the hope that it could ease upgrades and that a rebuild could be automatically fired by Docker hub using a configured dependency. Forget repeatable builds!

Let’s go for python:3-alpine:

FROM python:3-alpine

RUN pip install radicale

CMD ["radicale"]

Easy: Process management

It seems a good practice to use a process manager to handle PID 1 and reaping subprocesses. As I don’t know if Radicale handles signals properly, nor if it creates new subprocesses and handles them well, let’s use a process manager (this is more cargo-cult than scientific evidence).

I started with Yelp’s Dumb Init but:

  • I got strange messages when stopping the container
  • Dumb Init is in PyPI but requires a C compiler installed, which needs to be added to the Alpine image

Alternative: use Tini, a tiny but valid 'init' for containers. Tini has the advantages of just working and installable in Alpine with apk add --update tini.

Here is our image with Tini:

FROM python:3-alpine

RUN pip install radicale

ENTRYPOINT ["/tini", "--"]
CMD ["radicale"]

Hard: Volumes and permission

Next best practices: Never Run As Root. We don’t do that for hosted services since decades, so don’t do that inside containers, especially publicly opened containers. The Docker Security team does not recommend it either (https://www.youtube.com/watch?v=LmUw2H6JgJo).

That means: use the USER instruction or switch user when the container is run. Combined with a volume, that’s where I started having permission problems.

What seems to occur is that mounting a host volume (eg. docker run ... -v /path:/data/radicale) overwrites the permission in the container. What was owned by radicale:radicale became owned by root:root in the container.

The reason is that the Docker daemon runs as root, so the mounted volume became root (UID=0) in the container, in which, UID=0 is also root. Note that, when the radicale user in the container has the UID 1000, which is my user on the host. Complete detail here: https://denibertovic.com/posts/handling-permissions-with-docker-volumes/

I first found a solution from Stack Overflow and in the book Using Docker by Adrian Mouat (excellent book btw).

The Redis docker image handles the permission problem this way:

  • First, use a custom entrypoint:
COPY docker-entrypoint.sh /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
  • Then, chown $user the mounted volume in the entrypoint script:
if [ "$1" = 'redis-server']; then
    chown -R redis .             # Fix permissions
    exec gosu redis "$0" "$@"    # Run as `redis` and not `root`
fi

I reproduced the same behavior in my Radicale image with Tini:

COPY docker-entrypoint.sh /usr/local/bin
ENTRYPOINT ["/sbin/tini", "--", "docker-entrypoint.sh"]
CMD ["radicale", "--config", "/radicale/config"]

docker-entrypoint.sh:

if [ "$1" = 'radicale' -a "$(id -u)" = '0' ]; then
    chown -R radicale .
    exec su-exec radicale "$@"
fi

I used Su-exec, a lightweight alternative to Gosu, and more importantly, su-exec is available in Alpine repositories.

S6, the alternative

S6-Overlay contains the S6 series of scripts. As overlay they mean a tgz to unpack in the image.

S6-Overlay is a complete alternative, it provides:

  • An init system; it could replace Tini.
  • A script to fix permissions (custom scripts in /etc/fix-attrs.d); replace the chown radicale
  • Dropping privileges; replace Su-Exec

I did not have the time to play with S6. The thing is quite complex and powerful, maybe more than what I need.

Next

There are still many things to do outside the image itself. I have yet to:

  • Manage/Restart the container with Systemd
  • Limit the number of automatic restarts in Systemd
  • Monitor the process in the container
  • Limit the container capabilities
  • Limit the container networking
  • Limit the container resources
  • Test automatically the process when rebuilding the image (the Python version is not enforced)
  • Put the log in their own file and rotate them

My Radicale image is available at:

Improve this post