Rootless Podman, systemd, and Docker Compose files

Posted on Thu 26 October 2023 in hints-and-kinks • 4 min read

This is a summary of how I run a set of Docker (actually, Podman) containers for my Home Assistant setup on a Raspberry Pi. It works reasonably well for me, so I am sharing it here in the hope that it is useful to others.

The stage

I run my Home Assistant environment on a Raspberry Pi 4B running, currently, Ubuntu 23.04 Lunar Lobster. In total, that little machine runs five containers, out of which 3 are related to Home Assistant:

  • One is for Home Assistant itself,
  • one is for running the Mosquitto MQTT broker,
  • and one is for running Grott.

For all these services, the respective developer communities do not only maintain official Docker images, but also supported or at least recommended Docker Compose configurations.

I wanted a way to make the most of those available configurations, so as not to reinvent too many wheels.

How I manage containers

I prefer my containers to run in the context of users other than root.

Per-container system users

This means that I create a dedicated user for each container. What’s important is that in order to be able to use systemd user services later, I enable lingering for each user account.

For example:

$ sudo -i
# useradd homeassistant
# adduser homeassistant bluetooth
# loginctl enable-linger homeassistant

In order to actually enable lingering for the affected users, one must apparently reboot the machine after this change.

(I’ll get back to why I add the homeassistant user to the bluetooth group in a moment.)

Podman

I also don’t very much like the daemon-driven approach from Docker proper, so I tend to prefer podman as my container manager on a small system like the Raspberry Pi.

Podman tends to not be particularly well covered in the documentation of the projects I work with, but that is not much of an issue: I can combine Podman with a compatibility layer, podman-compose, so that although I am actually using Podman, I can configure my containers with an unchanged YAML configuration originally written for Docker Compose.

Here’s how I can install the necessary packages on my Raspberry Pi:

# apt install podman podman-compose

Next, I create the necessary Docker Compose configurations in the home directory of a user created to run that container.

For example, the /home/homeassistant directory, owned by the user homeassistant, contains this docker-compose.yaml file:

# /home/homeassistant/docker-compose.yaml
---
version: '3'
services:
  homeassistant:
    container_name: homeassistant
    image: "ghcr.io/home-assistant/home-assistant:stable"
    volumes:
      - /etc/localtime:/etc/localtime:ro
      # Replace this volume mapping with wherever
      # you want to put your Home Assistant configuration
      - /home/homeassistant/.config/homeassistant:/config
      - /run/dbus:/run/dbus:ro
    ports:
      - 8123:8123
    restart: always
    environment : {}

You can of course create a more elaborate configuration as you please.

Once this is set, I can manually fire up my container as a non-root user, using Podman, like so:

$ id
uid=1003(homeassistant) gid=1003(homeassistant) groups=1003(homeassistant),124(bluetooth)

$ podman-compose up
['podman', '--version', '']
using podman version: 4.3.1
** excluding:  set()
['podman', 'network', 'exists', 'homeassistant_default']
podman create --name=homeassistant --label io.podman.compose.config-hash=123 --label io.podman.compose.project=homeassistant --label io.podman.compose.version=0.0.1 --label com.docker.compose.project=homeassistant --label com.docker.compo
se.project.working_dir=/home/homeassistant --label com.docker.compose.project.config_files=docker-compose.yaml --label com.docker.compose.container-number=1 --label com.docker.compose.service=homeassistant -v /home/homeassistant/.config/h
omeassistant:/config -v /usr/share/zoneinfo/Etc/UTC:/etc/localtime:ro -v /run/dbus:/run/dbus:ro --net homeassistant_default --network-alias homeassistant -p 8123:8123 --restart always ghcr.io/home-assistant/home-assistant:stable
[...]

Systemd

Once I am satisfied that my container comes up just fine, the next step is managing it with systemd in user mode.

To do that, I need to create a config directory for systemd:

$ mkdir -p ~/.config/systemd/user

… and create a single file in there, which I name podman-compose.service:

[Unit]
Description=Podman via podman-compose
Wants=network-online.target
After=network-online.target
RequiresMountsFor=%t/containers

[Service]
Environment=PODMAN_SYSTEMD_UNIT=%n
Environment=PODMAN_USERNS=keep-id
Restart=always
TimeoutStartSec=60
TimeoutStopSec=60
ExecStart=/usr/bin/podman-compose up --remove-orphans
ExecStop=/usr/bin/podman-compose stop
Type=simple
WorkingDirectory=%h

[Install]
WantedBy=default.target

Note that many other tutorials about running docker-compose or podman-compose from systemd recommend you set Type=oneshot instead, and add the -d option to the ExecStart command.

I think using the simple type and omitting the -d option is the better idea, because in doing so,

  • I see the latest log lines from the container in systemctl --user status podman-compose,
  • I can access the full log with journalctl --user -u podman-compose,
  • I get more reliable output overall from systemctl --user status podman-compose, because rather than only reflecting whether starting the container was successful, it tells me whether it is still running at the time I check.

For more details on what the various %-prefixed specifiers mean, see the relevant section in the systemd documentation.

The Environment=PODMAN_USERNS=keep-id entry is somewhat crucial in a Home Assistant configuration. This, in combination with adding the homeassistant user to the bluetooth group and bind-mounting the /run/dbus directory, enables me to use the Raspberry Pi’s Bluetooth controller from the rootless container.1 That comes in handy for Home Assistant integrations for sensor devices using BLE.

Then, running

$ export XDG_RUNTIME_DIR=/run/user/$UID
$ systemctl --user daemon-reload
$ systemctl --user start podman-compose
$ systemctl --user enable podman-compose

starts my container, and also brings it up (under the non-root user account) every time the system boots.

In summary

What’s nice about this whole approach is that for all of my container-based services the configuration is exactly identical, except for one thing that differs from service to service: the docker-compose.yaml file.


  1. Thanks to GitHub user “Fattire” for an immensely useful GitHub comment on this subject!