Docker -> Podman + Quadlet
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 servicekate
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
!