Running Podman as a Non-Root User on a Headless Server

Install Podman and Enable User Namespaces

sudo apt update
sudo apt install -y podman

On Debian 12, Podman ships with userns-remap turned off.
I always enable it right away by editing /etc/containers/containers.conf:

[engine]
userns = "keep-id"

keep-id keeps the container’s UID/GID the same as the host user’s, which is the simplest approach for a single‑user homelab. If you have multiple users, you might want auto or a custom mapping.

After that tweak, restart the daemon:

sudo systemctl restart podman

A quick sanity check:

podman run --rm alpine whoami
# → <your-username>

You should see your own username, not root.

Networking Without Root

Rootless containers use the user‑namespace network stack. By default Podman creates a slirp4netns interface that forwards traffic through the host. On a headless server I usually prefer a more robust bridge or a dedicated VPN interface.

Option 1 – Stick with slirp4netns

podman run -d --name web -p 8080:80 nginx

-p works, but the traffic is forwarded through the user‑namespace, which can add a bit of overhead for high‑throughput workloads.

Option 2 – Build a User‑Namespace Bridge

Create a bridge and give it to the podman user:

sudo ip link add br0 type bridge
sudo ip addr add 10.10.0.1/24 dev br0
sudo ip link set br0 up
sudo iptables -I FORWARD -i br0 -j ACCEPT
sudo iptables -I FORWARD -o br0 -j ACCEPT

Then run the container with a static IP:

podman run -d --name web --net=host --ip=10.10.0.2 nginx

--net=host bypasses the user‑namespace network stack entirely. It works fine on a single‑user server, but if you expose services to the internet, be careful – the container shares the host’s network namespace.

Persisting Data with Rootless Volumes

Rootless containers can mount host paths, but the target directory must be owned by the user and, if SELinux is enabled, have the right context. For example:

mkdir -p ~/data/web
podman run -d --name web \
  -v ~/data/web:/usr/share/nginx/html:Z \
  nginx

:Z tells SELinux to generate a new context for the volume. If SELinux is off, just drop the option.

Systemd User Services

Running containers as background services is a common pattern on servers. Podman can generate systemd unit files that run under the user’s account:

podman generate systemd --name web --files

It writes a file like ~/.config/systemd/user/web.service. Enable and start it:

systemctl --user enable --now web.service

Make sure the user’s systemd socket is active:

systemctl --user enable --now podman.socket

If you want the service to start on boot, add --user to the systemctl command and tweak /etc/systemd/logind.conf:

[Login]
NAutoVTs=0
ReserveVT=none

Security Considerations

Risk Mitigation
Capability Escalation Use --security-opt=no-new-privileges on containers that run untrusted code.
SELinux Apply the :Z or :z volume options to keep contexts correct.
Network Exposure Prefer the bridge approach for services that must be reachable from the internet; avoid --net=host unless necessary.
User Namespace Mapping Keep the mapping simple (keep-id) to avoid accidental UID clashes.

Podman inherits the host’s kernel security features, so the usual hardening steps (e.g., disabling CAP_SYS_ADMIN, enabling seccomp) still apply. For a headless server I usually add a custom seccomp profile to drop unused syscalls:

podman run --security-opt seccomp=unconfined -d nginx

Use unconfined only when you’re sure the image is trusted; otherwise reference a profile from the official containers/seccomp repository.

Troubleshooting Common Issues

Symptom Likely Cause Fix
podman: error: failed to create user namespace: Operation not permitted Kernel lacks CONFIG_USER_NS or the user namespace is disabled in /etc/sysctl.conf. Enable userns in /etc/sysctl.conf and reload: sudo sysctl -p.
podman: error: failed to create network namespace: Operation not permitted The user lacks permission to create network namespaces. Add the user to the netdev group: sudo usermod -aG netdev $USER.
Container cannot bind to port 80 Port already in use or the user lacks binding rights. Use a higher port or run the container with --net=host if you’re sure it’s safe.
SELinux context errors Missing :Z or :z on volume mounts. Add the appropriate context option.

A Real‑World Example: Running a Headless GitLab Runner

I often run GitLab runners in containers on my home‑lab server. Here’s a quick rootless setup:

podman run -d --name gitlab-runner \
  -v ~/.gitlab-runner:/etc/gitlab-runner:Z \
  -v /var/run/docker.sock:/var/run/docker.sock \
  gitlab/gitlab-runner:latest

Because the runner needs to spawn Docker containers, I mount the Docker socket. In a rootless environment the socket must be owned by the user, so I create it with:

sudo mkdir -p /var/run/docker.sock
sudo chown $USER:$USER /var/run/docker.sock

Now the runner can execute jobs without elevating privileges.

When to Stick With Rootful Podman

Rootless mode is great for day‑to‑day use, but there are scenarios where rootful Podman remains the better choice:

  • High‑performance networking: Rootful containers can use macvlan or ipvlan for direct NIC access.
  • Legacy images: Some images expect root privileges to write to /var/lib. Running them rootless may require workarounds.
  • System‑wide services: If you need a container

See also