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
macvlanoripvlanfor 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
- Taming the systemd Boot Process: My Journey to Fixing Slow Boot Times on My Linux Laptop
- Troubleshooting Slow DNS Lookups with systemd-resolved on My Linux Homelab Server
- Taming the Journalctl Noise: How I Stopped Wasting Time on Useless systemd Logs
- Taming Disk Usage with btrfs Snapshots and Automatic Pruning on My Homelab Server
- Taming tmux: How I Replaced My Desktop's Taskbar with a Custom Terminal Layout