I’ve never really followed the hype around Docker but to be honest also never really taken the time to look into it more. That’s until my friend Harald told me that he is using it on his Raspberry Pi to run some services. What sounded appealing is that you can reproduce builds, you are not “polluting” the host system, you can keep all configs etc. in one place, and move your services somewhere else quickly. The latter is especially interesting when you want to reinstall the host system. Furthermore, you can put the build as well as configuration in version control. Of course, you are adding another layer of complexity in the mix. I thought I’d give it a try. Here are some notes pertinent to the setup with my Raspberry Pi.

Docker images for arm32v6/7 architecture

While the Raspberry Pi 4 has a 64bit CPU, Raspbian is 32bit. The main reason is backwards compatibility to older Raspberry Pi models. This can sometimes be a problem if an image is not provided for ARM 32bit architectures. One example is Gitea which so far is the only one that I encountered for my needs. Fortunately, they provide an arm32v6 binary in their releases so it was not too difficult to create a Dockerfile that downloads the binary. There are other Docker images that compile Gitea for the specific ARM architecture but there doesn’t seem to be a big advantage in using ARMv6 vs ARMv7 (see differences and see comparison benchmark).

My Dockerfile is based on the official one. You can find it in my repository on GitHub. The main difference is that instead of compiling Gitea it downloads the binary and corresponding repository for the specific release.

Docker and ufw

As outlined previously, I use ufw as a frontend for iptables. Unfortunately, Docker directly manipulates iptables which means that published ports are accessible from the outside even if no allow rule is added with/to ufw. There is a workaround which requires disabling iptables manipulation by Docker but they generally don’t recommend turning this setting off. Someone created another solution which leaves iptables manipulation by Docker intact but I haven’t tried it out myself.

In my case since I intended to use a reverse proxy, the only ports exposed to the outside are 80 and 443 for the reverse proxy. The ports of the containers are only exposed and not published. Therefore, I just left it as it is and did not use any of the workarounds.

Creating a user-defined network

I created a bridge network which allows the containers within that network to communicate. There are a lot of options. I used the following command:

docker network create --driver bridge <networkname>

With docker network inspect <networkname> you can then find out the subnet and gateway and use the gateway as the IP address to bind MariaDB to (see above) and subsequently use it as the host to connect to it from clients.

There are advanced options you can make use of, for example, I used the following command:

docker network create \
    --driver bridge \
    --gateway 172.22.0.1 \
    --subnet 172.22.0.0/16 \
    -o "com.docker.network.bridge.enable_ip_masquerade=true" \
    -o "com.docker.network.bridge.name=docker-<networkname>" \
    -o "com.docker.network.bridge.enable_icc=true" \
    -o "com.docker.network.driver.mtu=1500" \
    <networkname>

When you create containers you need to add --network <networkname> as an argument. If you are using docker-compose, you can specify the following to have all containers be part of this network:

networks:
  default:
    external:
      name: <networkname>

Running the database on the host

As outlined previously, I have a MariaDB instance running on the host. Why is it not running in a container? I did consider it but containers are supposed to be stateless and the general recommendation is to not run production databases in Docker. There is an interesting discussion about this on Reddit as well. In the end, it seemed like a risk (which I am not willing to take).

This of course makes it a bit more tricky when you want to connect to the database from within a container. By default, this doesn’t work unless you use host networking. But this seemed less practical since the network of the container is not isolated from the host.

There are two things to consider. The IP address MariaDB binds itself to to listen for connections, and the host name users are allowed to connect from. By default, MariaDB binds itself to localhost (127.0.0.1). You could of course bind to any IP address (0.0.0.0) and then connect through the hosts IP address. I didn’t want to open it up like this so I used the user-defined bridge network I described above. MariaDB is then bound to the host IP of that network. A disadvantage is that then only Docker containers within that network can access the database. So if you have any other application on the host that needs to access the database that doesn’t work.

When creating database users, instead of specifying 'user'@'localhost' you could say 'user'@'172.22.0.%'. This assumes that your subnet is 172.22.0.0/24. This way you don’t need to know the specific IP address of a container. Also, the IP address of a container is not guaranteed to be the same if you restart your containers. To create a database with a user do the following in the SQL console (when running sudo mysql from the host):

CREATE DATABASE <dbname>;
GRANT ALL PRIVILEGES ON <dbname>.* TO '<username>'@'172.22.0.%' identified by 'my-super-long-secret-password';
FLUSH PRIVILEGES;

Using the host timezone in containers

If ever you perform a docker logs <containername> you most likely will notice that the timezone does not match your host’s timezone. By default, Docker syncs the time but not timezone, so the timezone is UTC. Some images do support the setting of an environment variable TZ specifying the timezone but it is not always the case. If you want to support this for your own image, you need to install tzdata. However, you can also simply add a volume /etc/localtime:/etc/localtime:ro. This way, irregardless of the host on which a container is run, the timezone matches the one of the host.