My attempt at building a production grade docker image
📅 Published: • Thomas Queste
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"]
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 thechown 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: