Docker -> Podman + Quadlet

Updated on

Containers are light-weight virtual machines, often used to run isolated services on a server. They (typically) use the host kernel and don’t virtualise resources, leading to higher performance with minimal overhead compared to traditional virtual machines (like qemu).

Docker is the most widely used container engine, and has been at the head of the industry for a while. It uses a daemon run by the root user to start its containers. This has long been a security concern, as a malicious entity that breaks out of the container will have root access to the system!

Podman, a tool developed by Redhat, is quite similar to docker but crucially doesn’t require a daemon to run. This means unprivileged users can still get the benefits of containerization! Docker has recently been experimenting with a similar “rootless mode”, but it’s not nearly at feature parity. Further, since Podman is developed by Redhat, it integrates with other Redhat projects very nicely, like systemd, nftables, and cockpit.

Summary

In this blog, we’ll look at migrating an existing Docker Compose service to Podman! We’ll cover setting up our new Podman service with systemd’s Quadlet integration with a new unprivileged user.

Prerequisites:

  • A computer with linux (root access required, if you want to make a new user)

Optional:

  • Root access, if you’d like to make a new user
  • An existing Docker Compose service. You can use the one in this tutorial too

I’m using archlinux for this example, but any systemd-based linux will work (Ubuntu, Debian, Fedora, Redhat…). You might need to change a few of the user-creating commands on other systems.

Creating a New User

We will assume 2 users:

  • emily is an existing sudoer who’s running a Docker Compose service
  • kate will be a new unprivileged user who will run our Podman Quadlet

Start by logging into root and setting up kate (optional if you want to use emily to host your Quadlet):

sudo su -
useradd --create-home --shell /bin/bash kate
passwd kate  # Set some sort of password

For Podman, we’ll need to give kate a range of sub-ids, which the container can use to differentiate users, while still all being kate:

cat /etc/subuid
cat /etc/subgid
# Based on output of above, find a range of 65536 ids which aren't overlapping
# with another user. For example, here we use 30000
usermod --add-subuids 70000-135536 --add-subgids 70000-135536 kate

Assuming we don’t typically use the kate user, we also want to make sure our containers aren’t killed once we logout of kate:

loginctl enable-linger kate

Podman is also quite sensitive to XDG environment variables. Make sure you have them setup properly. For example:

cat <<FILE >> /home/kate/.bashrc
export XDG_CONFIG_HOME=~/.config
export XDG_CACHE_HOME=~/.cache
export XDG_DATA_HOME=~/.local/share
export XDG_STATE_HOME=~/.local/state
export XDG_DATA_DIRS='/usr/local/share:/usr/share'
export XDG_CONFIG_DIRS='/etc/xdg'
export XDG_RUNTIME_DIR="/run/user/$(id -u kate)"
FILE
chown kate:kate /home/kate/.bashrc

Now to make sure that worked:

su -l kate
podman ps  # This shouldn't give any warnings, just a blank table
podman info | grep rootless  # This should give a line like "rootless: true"

Migrating Docker Compose to Podman + Quadlet

Consider this docker-compose.yml:

services:
  open-webui:
    image: ghcr.io/open-webui/open-webui:v0.3.35
    restart: unless-stopped
    environment:
      OPENAI_API_BASE_URLS: https://api.mistral.ai/v1
      OPENAI_API_KEYS: <key>
    ports:
      - "9130:8080"
    volumes:
      - type: bind
        source: ./data
        target: /app/backend/data

We could covert this to a simple Podman bash script. It’s a good idea to try this step as kate before proceeding. Notice that kate will need her own ./data directory. Try running this as kate:

#!/usr/bin/env bash
podman run \
  --rm \
  --name open-webui \
  -e OPENAI_API_BASE_URLS="https://api.mistral.ai/v1" \
  -e OPENAI_API_KEYS="<key>" \
  -p 9130:8080 \
  -v ./data:/app/backend/data \
  ghcr.io/open-webui/open-webui:v0.3.35

A Quadlet file is similar to a systemd unit file, but describes the same things as a docker-compose.yml:

[Unit]
Description=Open WebUI container
Wants=network-online.target
After=network-online.target
After=local-fs.target
 
[Container]
ContainerName=open-webui
Image=ghcr.io/open-webui/open-webui:v0.3.35
 
Environment=OPENAI_API_BASE_URLS="https://api.mistral.ai/v1"
Environment=OPENAI_API_KEYS="<key>"
 
PublishPort=9130:8080/tcp
 
Volume=/home/kate/Documents/servers/openwebui/data:/app/backend/data
 
[Service]
Restart=on-failure
TimeoutStartSec=900
 
[Install]
WantedBy=default.target

In this case, I put my Quadlet file at /home/kate/Documents/servers/openwebui/openwebui.container. I put a corresponding /home/kate/Documents/servers/openwebui/data directory to mount in the container.

See Erick Patrick’s repository for a great Quadlet template with all the important options!

Quadlet files for a user should be at ~/.config/containers/systemd/. To be a bit more organized, we’ll simply symlink our container out of ~/Documents/servers to here:

ln -s /home/kate/Documents/servers/openwebui/openwebui.container /home/kate/.config/containers/systemd/

Running Quadlets

Use kate to test if our Quadlet is working:

/usr/lib/podman/quadlet -dryrun -user

This should print out a file it calls openwebui.service. It looks similar to our openwebui.container, but added more to the [Service] section and added a [X-Container] section. Quadlet essentially converts our .container files to systemd .service files, so that systemd can run them normally. Let’s try it!

Refresh your daemon:

systemctl --user daemon-reload

Now check the status:

systemctl --user status openwebui.service

We can start it just like any other systemd service:

systemctl --user enable --now openwebui.service

Then, the analog to docker log is journalctl --user -fu openwebui.service!