Why I Use Podman over Docker
I loved Docker Compose. I now love Quadlets.
If you ever start self-hosting, there is a very good chance you will be encouraged to use containers. That’s for good reason. In general, it is better to containerize something if you can. Containers are kind of like a miniature VM: It makes sure dependencies run smoothly without having to worry about different Containers messing each other’s files.
Docker is usually the go-to tool for this but I recommend people to just go to Podman. I have started with Docker and even appreciated Docker Compose but there are several reasons I decided to move.
Reason One: It doesn’t mess with your firewall by default
This is the single reason I moved away from Docker. Let’s take a compose.yaml file from a popular project: Forgejo:
networks:
forgejo:
external: false
services:
server:
image: codeberg.org/forgejo/forgejo:15
container_name: forgejo
environment:
- USER_UID=1000
- USER_GID=1000
restart: always
networks:
- forgejo
volumes:
- ./forgejo:/data
- /etc/localtime:/etc/localtime:ro
ports:
- '3000:3000'
- '222:22'
What bothers me is the ports section. For some reason, the design of Docker will allow ANYONE to access your instance of Forgejo over Port 3000. Or the SSH over Port 222. And it doesn’t matter if you set your ufw or firewall-cmd firewall correctly - Docker bypasses it. It is even in the limitations section of Docker.
## Firewall limitations
Warning
Before you install Docker, make sure you consider the following security implications and firewall incompatibilities.
- If you use ufw or firewalld to manage firewall settings, be aware that when you expose container ports using Docker, these ports bypass your firewall rules. For more information, refer to Docker and ufw.
- Docker is only compatible with iptables-nft and iptables-legacy. Firewall rules created with nft are not supported on a system with Docker installed. Make sure that any firewall rulesets you use are created with iptables or ip6tables, and that you add them to the DOCKER-USER chain, see Packet filtering and firewalls.
Docker writes to iptables or nftables directly which is why your firewall rules are bypassed. There are many ways to handle this correctly and one of the most common suggestions is to prefix with 127.0.0.1. That means 127.0.0.1:3000:3000 instead of 3000:3000. To do remote access, you use a reverse proxy.
There are many naysayers behind this:
- Just use
127.0.0.1:3000:3000. That’s great but I wager most users do not. And I assume most users do not know about this “feature” until it bites them. - You should manage your external firewall. Not everyone is running on AWS or Azure. Not all hosting providers have an external firewall. And even if they do, it is simply bad design to bypass the firewall. This is a nasty side effect that bites people. No one should be surprised of a software exposing ports.
- You should run secure apps. I very much trust Forgejo with its security and all the apps I run. But a simple vulnerability or a zero-day can ruin my day. Not to mention a bunch of DDOS attacks or minor security issues that pop up. Simply put - this isn’t a good enough reason to say Docker’s handling of networks is okay.
Now, can Podman do this? Yes - but you have to run it in rootful mode. That is, you have to go out of your way to actually do this. That’s the correct approach. If you want to expose it, you can expose it in the firewall or use a reverse proxy.
Reason Two: Rootless by Design
Docker has a daemon that runs as root or sudo. The risk of this is more theoretical in practice but in general, root or sudo is scary because it gives the highest priveleges so it should be as limited as possible.
Docker does not do this. Everything is root or sudo by default. Thus, a compromise on the app, and then a compromise in Docker can lead to very nasty things in the host.
Now, is compromising an app AND then getting out of the container pretty far-fetched? Yes - which is why this is more theoretical than practical. But it is also not THAT difficult to think of given that Docker is pretty popular and with it bypassing the firewall, someone might be leaving their app out in the open without knowing.
Imagine if an app was unknowingly exposed without auth and an attacker used a vulnerability to remote execute. Since the app is root, depending on the sophistication, it could either be files being messed up or the whole system being locked down.
Podman goes a different route: It has root in an app BUT it is not root in the system (unless you EXPLICITLY tell it to). What happens is that if you run the app as user a, it will act like a process run by user a with the priveleges of user a. Assuming the above attack happens, even if the attacker was able to bypass the system, it cannot write or execute certain commands.
Now, Docker does offer a rootless mode. But you have to enable it and most do not. I think it’s because it’s not really built into the design and was bolted on. Not to say that the implementation is bad (it actually is decent) - but this could have been corrected in earlier versions.
Reason Three: Systemd Native
I have some apps that need to have access to a media drive. With Docker, I have to assume it is mounted. With Podman, I can just do:
Requires=mnt-media.mount
After=mnt-media.mount
Since Podman does generate systemd files, it also means I can use other features such as systemd timers. In Docker, it is pretty janky since while I can use systemd, it feels like I am doing a workaround rather than something native.
Let’s put a simple example: I currently run a backup service I custom built. These have two scripts: db-backup.container and backup.container. I need db-backup.container to finish BEFORE running backup.container. Oh - and make sure to run it daily.
Podman makes this very easy. My backup.container is a container do I create a Quadlet file and then have it rely on db-backup.container. Then, add a backup.timer. Everyday, it now runs smoothly. Docker doesn’t make this easy. depends_on doesn’t have a “after it finishes running”.
I make use of systemd features a LOT. I cannot imagine how much harder it would be in Docker to have something monitor a path and run a script if something in it changes. Or even just “run this weekly”.
Now, I recognize that moving to Podman isn’t easy. And honestly, that’s cool.
Counterpoint 1: Docker Compose
This was the biggest reason I stayed for a while in Docker. I now use Quadlets because it is very much the favored way of doing Podman but if you really want Compose, you can still use docker-compose or podman-compose. podman compose up can still work though there are much more edge cases which is why I think that if you do end up going to Podman, you should just use Quadlets.
It may be quite a different philosophy since you will have like five files instead of just a compose.yaml but once you get the hang of it, it feels good.
Counterpoint 2: Relies on Repos which can be outdated
This is valid. In Ubuntu 24.04 for example, it has 4.9 - which doesn’t have .pod. You have to use backports to get 5.3 which is actually okay. However, Ubuntu 26.04 has Podman 5.7.0 which has all the features you would want from Podman. It still will also receive security updates so what you get is actually a very stable version of Podman.
For RHEL-based distros, I generally find 5.3.0 to be the minimum version which still has all the features you would want. The most notable one missing would likely be podman quadlet install so you would have to run systemctl --user daemon-reload && systemctl --user start app-pod.
Now, do I think you should dive first into Podman? I am still iffy. The ecosystem around Docker is pretty big BUT I would say it is generally worth the move. The benefits are mostly invisible and it’s generally hard to justify especially if there are workarounds for Docker’s flaws. But for me, the design decisions around Docker’s defaults make me question the whole project. These concerns aren’t really minor and with a little forethought, they could have seen it as wrong from the start.
I know the era of when Docker started was VERY different but in general, I respect teams who “get it right and then make it faster eventually” rather than the opposite. It is much easier when the fundamentals are correct than to correct the design of the software.