GuidesStart hereRootless Podman containers

Run containers rootless with Podman on Debian

Podman on Debian 13 with every app in its own unprivileged user, started at boot as systemd services, updated automatically, and served over HTTPS by one shared Caddy.

Tested on Podman 5.4 on Debian 13 (trixie) on a Melonslab server Step 4 of 4 for beginners Updated September 25, 2026

Recommended server for this guide

VC-S Micro · 2 vCPU · 8 GB Memory · 250 GB Storage

Month to month, no lock-in 7-day money-back guarantee

€7.99/mo

Deploy now
On this page

What you will set up

Podman runs containers much like Docker, but without a background service running as root. Here, containers run as ordinary users, and each app gets a user of its own:

  • Root inside a container is that app's user outside it, so someone who breaks out of a container gets no more rights on the server than that user has.
  • Each user has its own range of user IDs, so the containers of one app cannot read the files of another.

Only one app can own ports 80 and 443, so they go to Caddy, a web server with its own user. Caddy gets HTTPS certificates for every site on the server and passes each visitor on to the right app, which listens on a local port that cannot be reached from the internet.

The app guides build on this one: Nextcloud, Matrix, Vaultwarden, Actual Budget, Stalwart, a mail server, Forgejo, a Git server, Helium services, for the Helium browser, Headscale, a Tailscale control server, Hermes Agent, an AI agent, FreeRADIUS, for Wi-Fi logins, authentik, one login for all your apps, ERPNext, for accounting and invoicing, Zabbix, to monitor your servers, Uptime Kuma, to watch your sites, Linkwarden, for bookmarks, Immich, for photos, Home Assistant, for your smart home, AdGuard Home, to block ads and trackers, Minecraft, a game server, Portainer, a web UI for your containers, OpenClaw, an AI assistant, Plausible and Umami, for web analytics, n8n, for automation, Paperless-ngx, for documents, Jellyfin and Navidrome, for films and music, SearXNG, a private search engine, Open WebUI, an AI chat, BookStack, a wiki, Stirling-PDF, for PDF tools, ntfy, for push notifications, Garage, for S3 storage, Prometheus and Grafana, for monitoring, RustDesk, for remote desktop, and k3s, a small Kubernetes that runs without root behind the same Caddy. Every step below was run on a fresh Melonslab server with Debian 13.

Before you start

You need a server with Debian 13 and root access. If it is new, secure it first.

1. Install Podman

apt update
apt install -y podman systemd-container

Podman comes with pasta, which connects rootless containers to the network. systemd-container provides machinectl, which you will use to work as each app's user.

2. Allow ports 80 and 443

Linux reserves the ports below 1024 for root. Let ordinary users use them from port 80 upwards, so that Caddy can:

echo "net.ipv4.ip_unprivileged_port_start = 80" > /etc/sysctl.d/50-unprivileged-ports.conf
sysctl --system

The setting covers IPv6 too.

3. Create a user for Caddy

Every app starts the same way, with a user of its own. For Caddy:

useradd -m -s /bin/bash caddy
loginctl enable-linger caddy

Debian gives each new user its own range of 65,536 extra user IDs, which Podman uses for the users inside its containers; cat /etc/subuid lists every user's range. enable-linger starts the user's services at boot, without anyone logging in.

4. Work as the user

machinectl shell caddy@

machinectl shell starts a proper login session for the user, which systemctl --user and Podman need. exit takes you back to root.

5. Run Caddy as a service

Podman's Quadlet turns a short file describing a container into a systemd service. As caddy, create the directory for these files:

mkdir -p ~/.config/containers/systemd

Create ~/.config/containers/systemd/caddy.container:

[Unit]
Description=Caddy, the web server in front of every app

[Container]
ContainerName=caddy
Image=docker.io/library/caddy:2
Network=host
Volume=%h/Caddyfile:/etc/caddy/Caddyfile:ro
Volume=caddy-data:/data
AutoUpdate=registry

[Service]
Restart=always

[Install]
WantedBy=default.target

And ~/Caddyfile, Caddy's configuration:

{
    admin off
}

# One block per site. The app guides add theirs below.
:80 {
    respond "Caddy is running"
}

Start it:

systemctl --user daemon-reload
systemctl --user start caddy

Open http://203.0.113.10 (your server's address) in a browser. "Caddy is running" means it is serving port 80 as the caddy user.

What the lines do:

  • Network=host puts Caddy on the server's own network, so it sees every visitor's real IP address and can reach the apps on 127.0.0.1. It is the one container that faces the internet.
  • admin off switches off Caddy's admin interface, which would otherwise let any user on the server change its configuration. Changes to the Caddyfile then take effect when you restart Caddy.
  • Image is written in full, with the registry, so there is no doubt where the image comes from.
  • AutoUpdate=registry lets Podman update the container when a newer image is published (step 6).
  • WantedBy=default.target starts it at boot. Services made from Quadlet files cannot be switched on with systemctl enable; this line does that instead.

6. Keep containers up to date

systemctl --user enable --now podman-auto-update.timer

Once a day, Podman checks for a newer image for every container with AutoUpdate=registry and restarts the ones that changed. If a container fails to start on the new image, Podman goes back to the previous one. The timer is per user, so switch it on as every app's user. To see what it would update right now:

podman auto-update --dry-run

7. How an app fits in

Each app guide follows the same pattern:

  1. As root, create the app's user with the two lines from step 3, and switch to it with machinectl shell.
  2. As that user, run the app, in a pod or as a single container, publishing one port on 127.0.0.1 only, such as PublishPort=127.0.0.1:8081:80. The guides give each app its own port.
  3. As caddy, add a block for the app's address to ~/Caddyfile and restart Caddy with systemctl --user restart caddy. Caddy gets the certificate by itself.

Two things apply to every app behind Caddy:

  • Inside the app's pod or container, Caddy's connections appear to come from the server's own IPv4 address, 203.0.113.10 in these examples. That is how pasta passes on local connections, so it is the address the app should trust to forward the visitor's real one.
  • Inside the pod, the server's own address belongs to the pod itself, so an app that calls its own public address would reach itself instead of Caddy. AddHost=cloud.example.com:host-gateway in the pod file sends that name to the server instead.

8. Everyday commands

As the user that owns the container:

podman ps                        # running containers
systemctl --user status caddy    # the service
journalctl --user -u caddy       # its log
systemctl --user restart caddy   # restart it

Troubleshooting

systemctl --user says "Failed to connect to user scope bus". You switched user with su or sudo, which does not start a login session. Use machinectl shell caddy@ from step 4.

Caddy does not start, and its log says "bind: permission denied". Step 2 is missing or has not been loaded; sysctl net.ipv4.ip_unprivileged_port_start should print 80. After fixing it, systemctl --user reset-failed caddy lets systemd start Caddy again, as it stops retrying after a few failures.

Every visitor has the same IP address in the app. Check that the app trusts the server's own IPv4 address as its proxy (step 7). Containers attached to a Podman network (Network= in the file) are worse still: their ports go through a forwarder that replaces the visitor's address with its own, which is why the app guides use pods instead.

apt waits for a lock right after the server's first boot. The server is installing its first updates. Wait a minute and try again.

Run it on your own server

VC-S Micro

€7.99/mo

vCPU
2
Memory
8 GB
Storage
250 GB
Transfer
10 TB
Standard
HDD · RAID 10
  • Full root access
  • Native /64 IPv6
  • RAID-protected storage
  • Malmö, Sweden
  • Month to month, no lock-in
  • 7-day money-back guarantee
All guides