- What a container is (and what it is not)
- Docker: engine, images and registries
- docker run: how a container starts
- Podman: the same container without a daemon
- Docker vs Podman: the real differences
- When to use Docker and when to use Podman
- Best practices that work with both
- Conclusion
- Frequently asked questions
You type one command and a web server with its database comes up on your machine in a couple of seconds. That is the promise of container engines, and two tools deliver it today: Docker and Podman. Docker has been the de facto standard since 2013. Podman, pushed by Red Hat, does the same work with one structural difference underneath: it needs no central service running as root. Both read and write the same OCI images, so most of the time you can type podman exactly where you used to type docker.
A container is not a small virtual machine. It is a Linux process with limits bolted on, and once you hold that picture in your head, everything else about engines, images, layers and registries falls into place.
Here is the route. I will start with what a container really is and how it differs from a virtual machine, then move to how docker run behaves, what Podman changes, where the two engines genuinely diverge and how to choose one for the job in front of you.

What a container is (and what it is not)
A container is a normal Linux process with limits bolted on. It runs as a process, or a small group of processes, that the kernel keeps apart from the rest through two mechanisms Linux has carried for years:
- Namespaces: what a process is allowed to see. There is one namespace for processes, another for the network, another for the filesystem, another for users, and so on through about a dozen of them. A container lives in its own set of namespaces, so it sees its processes as if they were the only ones on the machine and gets its own network stack.
- cgroups (control groups): what a process is allowed to consume. They limit and account for CPU, memory, disk I/O and the number of processes. This is the piece that stops one runaway container from swallowing the whole server.
The consequence is the one that matters most. Every container on a host shares the same kernel. That makes them extremely light, because there is no operating system to boot, and it also means the isolation stops at the process level instead of reaching the hardware. The Linux kernel documentation on namespaces explains the mechanism in detail, and the NIST container security guide (SP 800-190) devotes whole chapters to how the threat model of a container differs from that of a virtual machine.
Four terms get mixed up all the time, so pin them down now and the rest of the article reads much faster:
- Image: the immutable template a container is started from. A package that carries a filesystem and the instructions needed to boot it.
- Container: a running instance of an image. Think of the class and the object in programming. One image can produce many containers.
- Registry: the store where images are published and downloaded from (Docker Hub, GitHub Container Registry, a private registry…).
- Runtime: the low-level piece that talks to the kernel to create the container (
runc,crun). Neither Docker nor Podman writes all of this from scratch. Both lean on these components.
Container versus virtual machine
People ask about the virtual machine comparison constantly, and the short version is that the two solve different problems. A virtual machine virtualises hardware and boots a complete guest operating system on top of a hypervisor. A container boots no operating system at all. It reuses the host kernel and isolates the process, and that single design choice explains almost every row in the table below.
| Container | Virtual machine | |
|---|---|---|
| What is virtualised | Processes isolated with namespaces and cgroups | Full hardware on top of a hypervisor |
| Kernel | The host’s, shared | One of its own per guest VM |
| Start-up | Milliseconds | Tens of seconds |
| Typical size | A few MB to a few hundred | Several GB per VM |
| Density | Dozens or hundreds per server | A handful per server |
| Isolation | Weaker, because the kernel is shared | Stronger, with a hardware boundary |
| Operating systems | Any that share the same kernel type | Any, including a different OS family |
| Typical use | Microservices, CI/CD, development environments | Workloads that need strong isolation or different OSes |
Another way to say it: a VM isolates systems, a container isolates applications. That is why the sensible setup is often both at once, containers running inside virtual machines, which is exactly what every Kubernetes cluster in the cloud does. Hardware virtualisation is no recent accident either. It is one more chapter in the history of operating systems.
Images, layers and the OCI standard
A container image is a stack of read-only layers, and that structure explains why images get pulled so fast. Every instruction in a Dockerfile that changes the filesystem creates a new layer, and layers are shared between images. If two images start from the same base, that base is downloaded and stored only once. When you start a container, a thin writable layer goes on top. Whatever changes lives there and disappears when you delete the container, unless you attach a volume.
That sharing is what makes container images quick to pull and cheap to store, and it is also why the order of your layers matters. Keep the parts that change least (the base image, the dependency install) near the top of the Dockerfile and let the build cache do its job for everything below. The build cache documentation spells out the mechanics.
Nobody owns that image format. It is defined by the Open Container Initiative (OCI), a project under the Linux Foundation that fixes two specifications, one for the image and one for the runtime. Docker, Podman and the registries of every cloud speak that same language. That is why you can build an image with one tool and run it with another without changing a thing.
The runtimes themselves live under the OCI umbrella too. runc is the reference implementation, written in Go, and crun is the lighter C implementation that Podman often prefers. The engine you type commands into is, in the end, a friendly front end for one of these.
Docker: engine, images and registries
Docker is the company and the toolset that made containers popular from 2013 onwards. When someone says “Docker” they may mean one of three different things, and it pays to separate them from the start.
The engine and the daemon
The Docker Engine is the engine itself: the service (dockerd) that listens for commands, manages images, networks and volumes, and launches containers. Docker’s architecture is client-server. The docker command you type in the terminal does no work of its own. It sends a request to the daemon over a Unix socket, /var/run/docker.sock by default, and shows you the reply. The official architecture overview walks through it piece by piece.
Underneath, the modern engine is more layered than the diagram suggests. The CLI talks to dockerd, which delegates to containerd, and containerd calls an OCI runtime such as runc to build the container through the kernel. Each layer does one job, which is what lets Docker swap pieces in and out over time.
That centralisation is convenient, and it carries a security implication worth understanding. The daemon runs as root, and anyone who can talk to its socket can, in practice, obtain root on the host. Adding a user to the docker group is a first-order security decision. Treat it as one. NIST calls this out explicitly among the risks of the Docker model.
From Dockerfile to image
Images are built from a text file called a Dockerfile. Its instructions are short and declarative, and reading one is enough to understand what an image contains:
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 3000
CMD ["node", "server.js"]
Each line has a purpose. FROM picks the base. WORKDIR sets the working directory. COPY brings files in. RUN executes a command at build time. USER drops privileges. EXPOSE documents the port, and CMD defines what runs when the container starts. The Dockerfile reference is the primary source for every instruction and its behaviour.
One subtlety separates a good image from a merely working one. RUN, COPY and ADD each create a layer. Installing dependencies in the same RUN that cleans up the package cache keeps the image small, and splitting them leaves the cache behind in a lower layer forever. Multi-stage builds go further. They let you compile in one stage and copy only the result into a slim final stage, so compilers never reach production.
Image, container and registry in practice
The daily workflow is three steps that repeat:
# 1. Find and pull an image from the registry
docker pull nginx:1.27-alpine
# 2. List the images already on this machine
docker images
# 3. Start a container from that image
docker run -d --name web -p 8080:80 nginx:1.27-alpine
The registry is the piece that makes all of this portable. Docker Hub is the best known and comes configured by default, yet it is not the only one. GitHub Container Registry, Amazon ECR, Google Artifact Registry and any OCI-compatible private registry work the same way. A registry stores images with a name and a tag. The latest tag is merely the one used when you specify none, and it means neither “newest” nor “most stable”. The registry concept is covered in more depth in Docker’s own documentation.
An image can also be pinned by digest instead of by tag. A digest is a cryptographic hash of the exact image content, so it never drifts. A tag can be reassigned tomorrow, while a digest guarantees you get the same bytes every time. That is why serious pipelines pin by digest and stop trusting latest.
Docker Hub applies download limits to free and anonymous accounts. In production you usually solve it in one of two ways: authenticate, or better, run your own registry as an internal cache. It is an operational detail that shows up sooner than people expect.
docker run: how a container starts
docker run does two things at once. It creates a container from an image and starts it. Its structure is always the same:
docker run [OPTIONS] IMAGE[:TAG] [COMMAND] [ARGUMENTS...]
All three parts matter. The options come before the image name and describe how you want the container (ports, variables, volumes, name…). The image can carry a tag or a digest to pin an exact version. The final command is optional. If you write one, it replaces whatever command the image had defined by default (CMD or ENTRYPOINT). The official docker run reference is the primary source for any option that is not listed here.
# Run the image as-is, with its default command
docker run alpine
# Override the default command: here, print the container's IP
docker run --rm alpine ip addr
The options you will use 90% of the time
| Option | What it does |
|---|---|
-d |
Runs in the background (detached) and frees the terminal. |
-it |
An interactive session with a terminal. Used to open a shell inside the container. |
--name |
Assigns a name. It is unique, so it cannot be reused even if the container is stopped. |
--rm |
Removes the container when it exits. Stops dead runs from piling up. |
-p 8080:80 |
Publishes port 80 of the container on port 8080 of the host. |
-P |
Publishes every port the image exposes, on random host ports. |
-e KEY=value |
Sets an environment variable inside the container. |
-v name:/path |
Mounts a (persistent) volume or a host directory (bind mount). |
--network |
Connects the container to a specific network instead of the default one. |
--restart |
Restart policy: no, on-failure, always, unless-stopped. |
--entrypoint |
Replaces the image’s entry point. |
-m, --cpus |
Cap the container’s memory and CPU. |
A real command, line by line
Here is a concrete example of a web server with its database, which is where almost every option shows up together:
docker network create shop
docker run -d
--name shop-db
--restart unless-stopped
--network shop
-e POSTGRES_PASSWORD=secret
-e POSTGRES_DB=shop
-v shop-data:/var/lib/postgresql/data
postgres:16-alpine
docker run -d
--name shop-web
--network shop
-p 8080:80
-e DB_HOST=shop-db
mysite:1.4
In the first command, -d leaves the container in the background. --name gives it a readable name, and --restart unless-stopped brings it back up if the server reboots. The two -e flags pass credentials and the database name as environment variables. The -v shop-data:/var/lib/postgresql/data mounts a named volume, so the data survives the container being deleted. That last point is the one most often forgotten in tests, and it costs people real data: the contents of a container without a volume are volatile.
In the second, -p 8080:80 publishes the service and --network shop puts it on the same network as the database, so it can resolve it by name (shop-db) without exposing anything to the internet. Containers on a user-defined network see each other by name, and containers on the default network do not. Publishing a port is nothing more than redirecting TCP or UDP traffic from one machine to another, the same logic covered in how TCP/IP works.
To check how things are going, the two commands you will always use are docker ps (running containers only) and docker ps -a (stopped ones too). Their columns (ID, image, command, status, ports and name) summarise the whole lifecycle. To get into a container that is already running, you use docker exec:
docker exec -it shop-web sh
docker logs -f shop-web
docker stop shop-web && docker rm shop-web
One nuance saves a lot of confusion here. docker exec opens a new process inside a container that is already running, whereas docker run creates a fresh one from scratch. If the container has died, exec is useless. You have to read its logs and start another.
Networking and storage: the two things that bite
Almost every container problem in production is a networking or a storage problem, so it pays to know the handful of options that exist. For networking, Docker offers a default bridge network, user-defined bridge networks, host (which removes the network namespace and shares the host’s stack), none (no network at all) and overlay for multi-host setups. The rule of thumb is simple. Create a user-defined network per application, because it gives you automatic DNS by container name and cleaner isolation.
For storage, three mechanisms cover nearly everything. Named volumes are managed by the engine and are the right choice for databases and anything that must outlive the container. Bind mounts map a host path directly, which is handy in development and ties the container to the host’s layout. tmpfs keeps data in memory and vanishes with the container, which is exactly what you want for temporary secrets. The volumes documentation details when each one applies.
Podman: the same container without a daemon
Podman (“Pod Manager”) was born at Red Hat with an idea that is simple to state and hard to pull off: do everything Docker does, with no central service. Instead of a daemon answering requests, every command you type spawns a process that creates the container and exits. Podman is today the container engine Red Hat ships and supports in RHEL, and the one OpenShift uses underneath. Its behaviour and commands are documented in the official Podman documentation.
Daemonless: why it matters
The label that sums up Podman’s philosophy is daemonless: no daemon. When you run podman run, there is no service listening on a permanent socket that waits for orders. The container is a direct child of your session (or is managed by systemd), and there is no central piece that, if it falls over, takes everything else on the machine with it. Each container gets a small supervisor process, conmon, which handles its I/O and exit status. That helper is per container, so it never becomes the shared point the daemon was.
The difference shows up hard in a security review. A central daemon such as Docker’s is a single point of failure and, more importantly, a single point of privilege. Compromising it amounts to compromising the host. Remove the daemon and that attack surface is gone.
Rootless: containers without root
The second advantage goes hand in hand with the first. Podman is designed to run without privileges. An ordinary user can create and run containers, which are stored in their own directory (~/.local/share/containers) and are not mixed with anyone else’s. A podman images run as a regular user only shows that user’s images.
Under the hood this uses user namespaces. The unprivileged user is mapped to a range of subordinate identifiers inside the container, so that inside it the process may see itself as root while staying a normal user outside. Those mapping ranges are defined in /etc/subuid and /etc/subgid, and the helpers newuidmap and newgidmap apply them. It is one more layer of isolation, and it fits the principle of least privilege that governs any security design, the same principle behind the models of role-based access control.
Spell out the practical payoff. A rootless container that escapes its boundaries does not land on the host as root. It lands as an unprivileged user, and from there the attacker still has work to do. That is a real reduction in blast radius, especially next to a root-owned daemon that anyone in the docker group can reach.
Pods: the nod to Kubernetes
Podman’s name is no accident. Besides loose containers, it can run pods, the same unit used by Kubernetes. A pod is a group of one or more containers that share a network, IP addresses and namespaces, so they talk to each other over localhost as if they were the same machine. The usual pattern is a main container accompanied by helper containers (sidecars) that support it. Pod creation is detailed in the podman pod create reference.
podman pod create --name webapp -p 8080:80
podman run -d --pod webapp --name app mysite:1.4
podman run -d --pod webapp --name cache redis:7-alpine
podman pod ps
Behind the scenes a pod has an infrastructure container, the pause container, that holds the shared namespaces, and the other containers join it. Kubernetes implements pods exactly the same way, which is why the two models line up. The practical benefit is real. What you build locally with Podman pods translates almost directly into Kubernetes manifests, without learning two different mental models.
The ecosystem around Podman
Podman does not come alone. Red Hat ships it with several tools that cover the ground Docker’s daemon covers on its own:
- Buildah: builds OCI images, with or without a
Dockerfile, and can mount a container’s filesystem to adjust it directly. - Skopeo: inspects, copies and signs images between registries without downloading them to the machine.
- Quadlet: generates systemd units from configuration files, so a container starts and restarts like any other system service.
- podman compose: lets you use
compose.yamlfiles with Podman, backed by an external provider.
Integration with systemd is one of its strong suits. Where Docker relies on the daemon’s --restart, Podman lets you declare a container as just another service, with dependencies, After=, WantedBy= and socket activation. Where Docker has Docker Compose and Swarm, Podman points straight at Kubernetes, the orchestration standard the clouds actually use.
Moving from Docker to Podman
The migration is usually less dramatic than it sounds, precisely because the interfaces are deliberately similar. The realistic steps are these:
- Install Podman alongside Docker. On RHEL-family systems it is a single package, and it does not conflict because it stores its images in its own place.
- Start with the alias. For a first pass,
alias docker=podmanis enough to see which of your daily commands work untouched. Most do. - Export and import images. If you want your existing local images,
docker savepluspodman loadmoves them across without pulling again. - Sort out the Docker API. Anything that talks to
docker.sockneeds Podman’s compatible socket, enabled withsystemctl --user enable --now podman.socket, which exposes an endpoint under$XDG_RUNTIME_DIR/podman/podman.sock. - Replace restart policies with systemd. Instead of
--restart always, generate a unit, today with Quadlet, so the container is managed like any other service. - Review Compose files. Test them through
podman composeand fix the few directives that assume the Docker daemon.
None of these steps forces you to rebuild your images, because the format is the same. What changes is the operational shell around them.
Docker vs Podman: the real differences
If both run the same images and share almost all of their syntax, where is the difference? In the architecture, the privilege model and the ecosystem:
| Docker | Podman | |
|---|---|---|
| Architecture | Client-server with a daemon (dockerd) |
Daemonless: one process per command |
| Privileges | The daemon runs as root, and an optional rootless mode exists |
Rootless by default and first-class |
| Socket | /var/run/docker.sock |
$XDG_RUNTIME_DIR/podman/podman.sock, per user |
| Images | OCI and Docker | OCI (the same ones) |
| Command line | docker … |
podman …, compatible |
| Pods | No, loose containers only | Yes, pods equivalent to Kubernetes pods |
| Orchestration | Docker Compose and Swarm | Compose (via podman compose) and Kubernetes |
| Building | Docker Build / BuildKit | podman build and Buildah |
| systemd | Its own management through restarts | Native systemd integration (Quadlet) |
| Desktop | Docker Desktop | Podman Desktop |
| Desktop licence | Paid subscription for large organisations | No cost |
The command line is compatible on purpose. Many commands are identical (run, ps, images, exec, logs, stop, rm, build, pull, push), and it is common to find guides that use an alias (alias docker=podman) so muscle memory does not have to change. What is not identical is anything that depends on the daemon. If you program against Docker’s API through its socket, you will probably need Podman’s service enabled to expose a compatible endpoint.
Is Podman the successor to Docker?
Podman has earned its place as a consolidated alternative, and Docker still holds the wider adoption. Podman has gained ground wherever isolation and privilege matter more than convenience: Linux servers managed by systemd, environments that do not want a permanent root service, and Kubernetes-based platforms. Docker keeps the advantage of being the name everyone knows, with the widest ecosystem, the smoothest path on Windows and macOS through Docker Desktop, and a solid grip on many development workflows.
Two things that are often muddled are also worth separating. The Docker Engine is free and open-source software. What carries a subscription in certain cases is Docker Desktop, which is free for personal use and small businesses and requires payment in larger organisations. On Linux the engine installs at no cost. That distinction explains why some organisations move to Podman without Docker bothering them technically at all. The licensing criteria are set out on the Docker subscription page, and the engine’s own licence lives on the Moby project.
When to use Docker and when to use Podman
The useful question is which context each engine fits, so run your own setup through this short list.
Docker is a good fit when…
- You work on Windows or macOS and want the path of least friction. Docker Desktop wraps the virtual machine and the management for you. On Windows it relies on the Linux subsystem, so knowing what WSL2 is helps you understand what sits underneath.
- Your team already has workflows built on
Dockerfile, Compose and the Docker API, and changing them would buy nothing. - You need the widest possible ecosystem of images, extensions and third-party tooling.
- You are going to use Docker Swarm as a simple orchestrator, with the structure you already know.
Podman is a good fit when…
- You run Linux servers and want containers managed as systemd services, with no permanent daemon.
- Security and least privilege are a requirement: rootless containers by default and no socket that hands
rootto whoever reaches it. - You are heading towards Kubernetes. Podman pods are the same concept and the learning curve is shorter.
- You want to avoid desktop licence costs in a large organisation, or you depend on Red Hat environments.
On plenty of machines the two coexist without trouble, because they use different paths and different stores. The practical rule is to pick one as the main engine so you do not duplicate images or juggle two local stores. Keep the other for occasional experiments.
Best practices that work with both
The decisions that actually make a difference in production do not depend on which engine you pick:
- Pin versions, and stop leaning on
latest. A concrete tag (nginx:1.27-alpine) or, better, a digest makes the same image always the same. Withlatest, two deployments of identical code can run different versions. - Keep images small and multi-stage. Start from compact bases (the alpine or slim variants, for example) and use multi-stage builds so compilers and unnecessary files never reach the final image. Less surface means fewer vulnerabilities and faster pulls.
- Never put secrets in the image. No passwords or keys in a
Dockerfile. They are baked into the layers, and anyone who pulls the image can read them. Secrets are injected at run time through environment variables, mounted files or a secret manager. - Do not run as
rootinside the container. Declare an unprivileged user with theUSERinstruction. If the engine is also rootless, the two protections stack. - Limit resources. Bounded CPU and memory stop one runaway container from dragging down the host and its neighbours.
- Do not mount the engine’s socket into a container unless you know exactly what you are doing. Whoever controls it from inside gains the same power over the host as the engine itself.
- Persist only what should persist. Databases and user data go in named volumes, and everything else stays in the container’s ephemeral layer. Have a backup plan for those volumes too, because a container does not protect you against an accidental delete.
- Write logs to standard output. A container should log to
stdout/stderrand let the engine or the platform collect it, instead of writing files inside that vanish with the container. - Scan and sign your images. Checking them for known vulnerabilities and verifying their provenance before deployment closes the gap between “it works” and “it is safe”.
- One process per container. It is what makes scaling, restarting and debugging predictable instead of a guessing game.
Conclusion
Docker and Podman solve the same problem, packaging and running applications in an isolated, portable way, with two different philosophies. Docker bets on a convenient central service and the largest ecosystem. Podman bets on doing without it, running unprivileged and speaking Kubernetes’ language. Because both use the same OCI image format, the choice is not irreversible. The content you build is worth the same on either.
What really matters is the isolation model, pinning versions, keeping secrets out of images and capping privileges and resources. With those foundations in place, switching engines is an afternoon’s work. Nobody has to spend months on it.
Frequently asked questions
Can Podman run the same images as Docker?
Yes. Podman works with OCI images, the same format Docker uses, so images from Docker Hub run without conversion. The command line is also largely compatible, which makes the transition almost immediate.
Is Podman the successor to Docker?
It is more accurate to call it a consolidated alternative than a universal replacement. Podman has gained weight on Linux servers and in Red Hat environments through its daemonless, rootless model, while Docker keeps wider adoption and a broader ecosystem. Both keep evolving.
Is Docker no longer free?
The Docker Engine remains free and open-source software. What carries a subscription is Docker Desktop, which is free for personal use and small businesses and requires payment in larger organisations. If you are worried about cost on Linux, the engine does not have one.
Is a container the same as a virtual machine?
No. A virtual machine boots a complete operating system on virtualised hardware, whereas a container is an isolated process that shares the host’s kernel. Containers are far lighter and start in milliseconds, while virtual machines offer stronger isolation.
Can I run Docker and Podman on the same machine?
Yes, although it is worth designating one as the main engine so you do not duplicate images or create parallel local stores. A common setup is Podman on servers and Docker Desktop on the development machine, or the reverse, sharing images through a registry.
What is a Podman pod?
It is a group of containers that share a network and namespaces, exactly like a Kubernetes pod. It groups containers that belong together, for example an application and its sidecar, so they can talk over localhost.
Can I keep using docker-compose with Podman?
Yes, through podman compose, which reads the same compose.yaml files and relies on an external provider. Most files work unchanged, and the occasional directive that assumes the Docker daemon needs adjusting.
Does Podman work on macOS and Windows?
Yes. Podman runs on those platforms through Podman Desktop and podman machine, which starts a lightweight Linux virtual machine to host the containers, much as Docker Desktop does. The command-line experience is the same.