LabHub

Blog

The Complete Guide to Docker & Podman Commands: Container Operations, All on One Page

한국어English日本語中文


1. Introduction: The Two Great Ranges of the Container Ecosystem

1.1 Docker's History and Where It Stands Today

Docker, which Solomon Hykes unveiled to the world in 2013 in a five-minute PyCon lightning talk called "The future of Linux Containers", drove the popularization of container technology and transformed how software is shipped. Container technologies existed before Docker — LXC (Linux Containers), FreeBSD Jail, Solaris Zones — but Docker combined an image layer system, declarative Dockerfile-based builds, and a central registry in Docker Hub to make the vision of "build once, run anywhere" real.

Docker went on to become the basis of the OCI (Open Container Initiative) standard and settled in as the core runtime of the Kubernetes ecosystem. But when Kubernetes deprecated dockershim in 2020 and Docker Desktop's commercial license policy changed (paid for companies with more than 250 employees), the industry started looking for alternatives.

Today Docker is still the de facto standard in development environments, and Docker Hub reigns as the world's largest container image registry. In production, however, containerd, CRI-O, and Podman are expanding their territory quickly.

1.2 The Background Behind Podman

Podman (Pod Manager) is an OCI (Open Container Initiative) compatible container engine developed under Red Hat's leadership. Red Hat judged that Docker's single-daemon architecture had fundamental limits in security, stability, and system integration, and built three tools to address them: Podman, Buildah, and Skopeo.

These three tools work independently while complementing each other. The design follows the Unix philosophy of "a tool that does one thing well" faithfully.

1.3 Why Podman?

The core reasons to consider moving from Docker to Podman are as follows.

Daemonless architecture: Docker always needs the dockerd daemon running in the background. If that daemon dies, all container management becomes impossible (a single point of failure). Podman forks and execs each container directly, without a daemon, so it does not have that problem.

Rootless containers: Docker supports a rootless mode too, but Podman took rootless as a basic design principle from the start. Ordinary users can run containers without root privileges, which improves security considerably.

A fork-exec model: because it follows the traditional Unix process model, integration with systemd, audit, and cgroups is natural. Each container exists as a child process of the Podman process.

Native Pod support: you can use the Kubernetes Pod concept locally as-is, and the podman generate kube command generates Kubernetes YAML automatically.

CLI compatibility: set alias docker=podman and most Docker commands keep working unchanged.


2. Architecture Compared: Docker vs Podman

2.1 Docker's Architecture: The Client-Daemon Model

Docker follows a classic client-server architecture. When a user runs docker run, the Docker CLI (the client) sends a request over a REST API to the Docker daemon (dockerd). The daemon delegates to containerd, which in turn calls runc to create the actual container.

┌─────────────────────────────────────────────────────────────────┐
Docker Architecture├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐    REST API     ┌──────────────────┐             │
│  │  Docker   │───────────────▶│   Docker Daemon   │             │
│  │   CLI/var/run/         (dockerd)      │             │
│  │          │  docker.sock    │                    │             │
│  └──────────┘                 │  ┌──────────────┐ │             │
│                               │  │  containerd   │ │             │
│                               │  │              │ │             │
│                               │  │  ┌────────┐ │ │             │
│                               │  │  │  runc   │ │ │             │
│                               │  │   (OCI)   │ │ │             │
│                               │  │  └────┬───┘ │ │             │
│                               │  └───────┼─────┘ │             │
│                               └──────────┼───────┘             │
│                                          │                      │
│                    ┌─────────────────────┼──────────────────┐   │
│                    │    Container 1Container 2      │   │
    (process)          (process)        │   │
│                    └─────────────────────┴──────────────────┘   │
│                                                                 │
│  ⚠ If dockerd dies, no container can be managed (SPOF)│  ⚠ Access to docker.sock = you can obtain root                 │
└─────────────────────────────────────────────────────────────────┘

The core problem with this structure is that access to docker.sock is effectively the same as root access. A user in the Docker group can mount the host filesystem or run a container with the --privileged flag and take over the host completely.

2.2 Podman's Architecture: The Daemonless Fork-Exec Model

Podman has no daemon. When you run podman run, the Podman process forks conmon (the container monitor) directly, and conmon executes an OCI runtime (crun or runc) to create the container.

┌─────────────────────────────────────────────────────────────────┐
Podman Architecture├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌──────────┐  direct fork-exec   ┌──────────┐                │
│  │  Podman   │────────────────────▶│  conmon   │                │
│  │   CLI        (no daemon!)      (container │                │
│  │          │                      │ monitor)  │                │
│  └──────────┘                      └─────┬────┘                │
│                                          │                      │
│                                    ┌─────▼────┐                │
│                                    │   crun    │                │
  (OCI     │                │
│                                    │ runtime)  │                │
│                                    └─────┬────┘                │
│                                          │                      │
│                    ┌─────────────────────┼──────────────────┐   │
│                    │    Container 1Container 2      │   │
    (child proc)       (child proc)     │   │
│                    └─────────────────────┴──────────────────┘   │
│                                                                 │
│  ✅ No daemon → no SPOF│  ✅ Each container is its own process → easy systemd integration │
│  ✅ Rootless supported by default└─────────────────────────────────────────────────────────────────┘

conmon is a lightweight monitor process that captures the container's stdout/stderr, records its exit code, and keeps watching the container even after the Podman CLI has exited.

2.3 The Core Comparison Table

Comparison pointDockerPodman
ArchitectureClient-daemon (dockerd)Daemonless (fork-exec)
Default privilegesroot (daemon)rootless (an ordinary user)
OCI runtimerunccrun (default), runc supported
Image buildsBuilt in (BuildKit)Through Buildah
Daemon processRequired (dockerd + containerd)None
systemd integrationLimitedNative (Quadlet)
Pod supportNone (Compose instead)Native
Generates K8s YAMLNopodman generate kube
Runs K8s YAMLNopodman play kube
Socket-based APIdocker.sock (always active)podman.sock (active when needed)
Container monitorcontainerd-shimconmon
Default registrydocker.io onlySearches multiple registries
Compose supportDocker Compose (official)podman-compose / podman compose
LicenseApache 2.0 + commercial (Desktop)Apache 2.0 (fully open source)
Security frameworkAppArmor/SeccompSELinux/AppArmor/Seccomp

3. Installation and Initial Setup

3.1 Installing Docker

Ubuntu / Debian

# Remove any existing Docker packages
sudo apt-get remove docker docker-engine docker.io containerd runc

# Install the required packages
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg lsb-release

# Add Docker's official GPG key
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
  sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

# Add the Docker repository
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
  https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

# Install Docker Engine
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# Add the current user to the docker group (you have to log back in)
sudo usermod -aG docker $USER

# Start the service and enable it at boot
sudo systemctl start docker
sudo systemctl enable docker

# Confirm the installation
docker version
docker run hello-world

CentOS / RHEL / Rocky Linux

# Remove any existing packages
sudo yum remove -y docker docker-client docker-client-latest \
  docker-common docker-latest docker-latest-logrotate \
  docker-logrotate docker-engine

# Add the Docker repository
sudo yum install -y yum-utils
sudo yum-config-manager --add-repo \
  https://download.docker.com/linux/centos/docker-ce.repo

# Install Docker Engine
sudo yum install -y docker-ce docker-ce-cli containerd.io \
  docker-buildx-plugin docker-compose-plugin

# Start the service
sudo systemctl start docker
sudo systemctl enable docker

# Add the user to the group
sudo usermod -aG docker $USER

macOS (Docker Desktop)

# Install with Homebrew
brew install --cask docker

# Or download the .dmg from the official Docker Desktop site
# https://www.docker.com/products/docker-desktop/

# After installing, launch Docker Desktop → the Docker Engine starts automatically
docker version

3.2 Installing Podman

Ubuntu / Debian

# On Ubuntu 22.04+ it is in the default repositories
sudo apt-get update
sudo apt-get install -y podman

# When you need a newer version (Ubuntu)
sudo mkdir -p /etc/apt/keyrings
curl -fsSL "https://download.opensuse.org/repositories/devel:kubic:libcontainers:unstable/xUbuntu_$(lsb_release -rs)/Release.key" \
  | gpg --dearmor \
  | sudo tee /etc/apt/keyrings/devel_kubic_libcontainers_unstable.gpg > /dev/null
echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/devel_kubic_libcontainers_unstable.gpg] \
  https://download.opensuse.org/repositories/devel:kubic:libcontainers:unstable/xUbuntu_$(lsb_release -rs)/ /" \
  | sudo tee /etc/apt/sources.list.d/devel:kubic:libcontainers:unstable.list > /dev/null
sudo apt-get update
sudo apt-get install -y podman

# Confirm the installation
podman version
podman info

CentOS / RHEL / Rocky Linux

# Included by default on RHEL 8+ / CentOS Stream 8+
sudo dnf install -y podman

# Install the additional tools
sudo dnf install -y buildah skopeo podman-compose

# Podman has no daemon, so systemctl start is unnecessary!
podman version

macOS (Podman Machine)

# Install with Homebrew
brew install podman

# Initialize the Podman machine (creates a Linux VM)
podman machine init

# Start the machine
podman machine start

# Confirm the installation
podman version
podman machine list

# Stop the machine
podman machine stop

# Delete the machine
podman machine rm

Note: on macOS and Windows, Podman creates a lightweight Linux VM (based on QEMU or the Apple Hypervisor Framework) and runs containers inside it. The approach resembles Docker Desktop, but there is also a free GUI tool called Podman Desktop.

3.3 Compatibility Setup: alias docker=podman

To keep using your existing Docker-based scripts and workflows unchanged, a simple alias is enough.

# Add it to your shell configuration file (~/.bashrc or ~/.zshrc)
alias docker=podman
alias docker-compose=podman-compose

# Apply it right away
source ~/.bashrc  # or source ~/.zshrc

# RHEL/CentOS ship a podman-docker package
sudo dnf install -y podman-docker
# That package creates a symlink from /usr/bin/docker to /usr/bin/podman
# It also includes docker.sock emulation

3.4 Docker Desktop vs Podman Desktop

Comparison pointDocker DesktopPodman Desktop
LicensePaid for companies over 250 people ($5+/user/month)Completely free (Apache 2.0)
GUIA rich UIA basic UI (improving quickly)
ExtensionsThe Docker Extensions marketplaceExtension support
KubernetesA built-in K8s clusterWorks with Kind/Minikube
VM backendLinux Kit (macOS), WSL2 (Windows)QEMU / Apple Hypervisor
Resource managementCPU, memory, and disk settingspodman machine init --cpus --memory

4. Image Management Commands

Containers are created from images. Managing images is the most basic part of running containers.

4.1 Searching for Images

# Search Docker Hub for an image
docker search nginx
podman search nginx

# Limit the number of results
docker search --limit 5 nginx
podman search --limit 5 nginx

# Search official images only (Docker)
docker search --filter is-official=true nginx

# Podman searches several registries at once (depending on registries.conf)
# /etc/containers/registries.conf
# unqualified-search-registries = ["docker.io", "quay.io", "ghcr.io"]
podman search --list-tags docker.io/library/nginx

4.2 Pulling Images

# Download the default image
docker pull nginx
podman pull nginx

# Specify a tag
docker pull nginx:1.25-alpine
podman pull nginx:1.25-alpine

# Pull from a specific registry
docker pull ghcr.io/myorg/myapp:latest
podman pull quay.io/prometheus/prometheus:latest

# Specify a platform (architecture)
docker pull --platform linux/arm64 nginx:latest
podman pull --platform linux/arm64 nginx:latest

# Download every tag
docker pull --all-tags nginx
podman pull --all-tags nginx

# Pin a specific build by digest (an immutable reference)
docker pull nginx@sha256:abc123...
podman pull nginx@sha256:abc123...

4.3 Listing Images

# List the local images
docker images
podman images

# The verbose form (the same command)
docker image ls
podman image ls

# Filter to a specific image
docker images nginx
podman images nginx

# Show dangling images only (untagged images)
docker images -f dangling=true
podman images -f dangling=true

# Print the image IDs only
docker images -q
podman images -q

# Custom-formatted output
docker images --format "{{.Repository}}:{{.Tag}} - {{.Size}}"
podman images --format "{{.Repository}}:{{.Tag}} - {{.Size}}"

# JSON output (Podman)
podman images --format json

4.4 Image Details (Inspect)

# Show the image details
docker inspect nginx:latest
podman inspect nginx:latest

# Extract a single field (Go template)
docker inspect --format '{{.Config.ExposedPorts}}' nginx
podman inspect --format '{{.Config.ExposedPorts}}' nginx

# Check the image size
docker inspect --format '{{.Size}}' nginx
podman inspect --format '{{.Size}}' nginx

# Check the environment variables
docker inspect --format '{{.Config.Env}}' nginx
podman inspect --format '{{.Config.Env}}' nginx

# Check the entrypoint and cmd
docker inspect --format '{{.Config.Entrypoint}} {{.Config.Cmd}}' nginx
podman inspect --format '{{.Config.Entrypoint}} {{.Config.Cmd}}' nginx

4.5 Image History

# Show the image layer history
docker history nginx
podman history nginx

# Show the full command (untruncated)
docker history --no-trunc nginx
podman history --no-trunc nginx

# JSON form (Podman)
podman history --format json nginx

4.6 Tagging Images

# Add a new tag to an image
docker tag nginx:latest myregistry.com/nginx:v1.0
podman tag nginx:latest myregistry.com/nginx:v1.0

# Add several tags
docker tag myapp:latest myapp:v2.1.0
docker tag myapp:latest myapp:stable
podman tag myapp:latest myapp:v2.1.0
podman tag myapp:latest myapp:stable

4.7 Removing Images

# Remove an image
docker rmi nginx:latest
podman rmi nginx:latest

# The same command (recommended)
docker image rm nginx:latest
podman image rm nginx:latest

# Force removal (even when a container is running)
docker rmi -f nginx:latest
podman rmi -f nginx:latest

# Remove every image
docker rmi $(docker images -q)
podman rmi -a

# Remove dangling images only
docker image prune -f
podman image prune -f

# Remove every unused image
docker image prune -a -f
podman image prune -a -f

# Filter on a condition when removing (images older than 24 hours)
docker image prune -a --filter "until=24h"
podman image prune -a --filter "until=24h"

4.8 Saving and Loading Images (Save/Load)

Useful in air-gapped environments or when transferring offline.

# Save an image to a tar file
docker save -o nginx.tar nginx:latest
podman save -o nginx.tar nginx:latest

# Save several images into one tar
docker save -o images.tar nginx:latest redis:latest postgres:15
podman save -o images.tar nginx:latest redis:latest postgres:15

# Save with gzip compression
docker save nginx:latest | gzip > nginx.tar.gz
podman save nginx:latest | gzip > nginx.tar.gz

# Load an image from a tar file
docker load -i nginx.tar
podman load -i nginx.tar

# Load from a gzip-compressed file
docker load -i nginx.tar.gz
podman load -i nginx.tar.gz

4.9 Image Import/Export

Where save/load includes the image metadata (layers, tags, history), export/import handles only the container's filesystem.

# Export a container's filesystem as a tar
docker export my-container -o container-fs.tar
podman export my-container -o container-fs.tar

# Create an image from a tar file
docker import container-fs.tar myimage:imported
podman import container-fs.tar myimage:imported

# Import directly from a URL
docker import https://example.com/rootfs.tar.gz myimage:latest
podman import https://example.com/rootfs.tar.gz myimage:latest

4.10 Registry Login and Push

# Log in to Docker Hub
docker login
podman login docker.io

# Log in to a specific registry
docker login ghcr.io
podman login quay.io

# Supply the username and password directly
docker login -u username -p password registry.example.com
podman login -u username -p password registry.example.com

# Log out
docker logout
podman logout docker.io

# Push an image
docker push myregistry.com/myapp:v1.0
podman push myregistry.com/myapp:v1.0

# Push every tag
docker push --all-tags myregistry.com/myapp
podman push --all-tags myregistry.com/myapp

5. Building Images

5.1 Dockerfile vs Containerfile

Docker uses the name Dockerfile, and Podman/Buildah recognize Containerfile by default. But Podman recognizes a Dockerfile automatically as well, so you can use it without renaming anything. The contents and syntax are exactly the same.

# Dockerfile or Containerfile — the syntax is identical
FROM node:20-alpine AS builder

WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build

FROM node:20-alpine
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
EXPOSE 3000
USER node
CMD ["node", "dist/main.js"]
ItemDockerfileContainerfile
Tool that uses itDockerPodman, Buildah
SyntaxIdenticalIdentical
Default filenameDockerfileContainerfile
InteroperabilityPodman recognizes a DockerfileDocker does not recognize a Containerfile
Specifying itdocker build -f Containerfile .podman build -f Dockerfile .

5.2 docker build vs podman build

# A basic build
docker build -t myapp:latest .
podman build -t myapp:latest .

# Specify a particular Dockerfile
docker build -f Dockerfile.prod -t myapp:prod .
podman build -f Containerfile.prod -t myapp:prod .

# Pass a build argument
docker build --build-arg NODE_ENV=production -t myapp:prod .
podman build --build-arg NODE_ENV=production -t myapp:prod .

# Build without the cache
docker build --no-cache -t myapp:latest .
podman build --no-cache -t myapp:latest .

# Specify the target stage (multi-stage)
docker build --target builder -t myapp:builder .
podman build --target builder -t myapp:builder .

# Multi-platform build (Docker BuildKit)
docker buildx build --platform linux/amd64,linux/arm64 -t myapp:latest .

# Multi-platform build with Podman
podman build --platform linux/amd64,linux/arm64 --manifest myapp:latest .

# Memory limit during the build
docker build --memory 2g -t myapp:latest .

# Minimize the build context size — using .dockerignore is essential
# .dockerignore (or .containerignore)
# node_modules
# .git
# *.md
# dist
# .env

5.3 A Practical Multi-Stage Build Example

A multi-stage build separates the build environment from the runtime environment, which cuts the final image size dramatically.

A Go Application Example

# ============================================
# Stage 1: the build environment
# ============================================
FROM golang:1.22-alpine AS builder

# Install the tools needed for the build
RUN apk add --no-cache git ca-certificates

WORKDIR /app

# Copy the dependencies first (to use the cache)
COPY go.mod go.sum ./
RUN go mod download

# Copy the source and build
COPY . .
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
    go build -ldflags="-w -s" -o /app/server ./cmd/server

# ============================================
# Stage 2: the runtime environment (scratch = an empty image)
# ============================================
FROM scratch

# Copy the CA certificates (for HTTPS)
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/

# Copy only the binary
COPY --from=builder /app/server /server

# Run as a non-root user
USER 65534:65534

EXPOSE 8080
ENTRYPOINT ["/server"]

A Java (Spring Boot) Example

# ============================================
# Stage 1: build
# ============================================
FROM eclipse-temurin:21-jdk-alpine AS builder

WORKDIR /app
COPY gradle/ gradle/
COPY gradlew build.gradle.kts settings.gradle.kts ./
RUN ./gradlew dependencies --no-daemon

COPY src/ src/
RUN ./gradlew bootJar --no-daemon -x test

# Create a custom JRE runtime (jlink)
RUN jlink \
    --add-modules java.base,java.logging,java.sql,java.naming,java.management,java.instrument,java.security.jgss,java.desktop \
    --strip-debug \
    --no-man-pages \
    --no-header-files \
    --compress=zip-6 \
    --output /custom-jre

# ============================================
# Stage 2: run (with the custom JRE)
# ============================================
FROM alpine:3.19

COPY --from=builder /custom-jre /opt/java
COPY --from=builder /app/build/libs/*.jar /app/app.jar

ENV JAVA_HOME=/opt/java
ENV PATH="${JAVA_HOME}/bin:${PATH}"

RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 8080
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

A Python Example

# ============================================
# Stage 1: build the dependencies
# ============================================
FROM python:3.12-slim AS builder

RUN pip install --no-cache-dir poetry
WORKDIR /app
COPY pyproject.toml poetry.lock ./
RUN poetry export -f requirements.txt --output requirements.txt --without-hashes
RUN pip install --no-cache-dir --prefix=/install -r requirements.txt

# ============================================
# Stage 2: the runtime environment
# ============================================
FROM python:3.12-slim

# Install only the system libraries you need
RUN apt-get update && apt-get install -y --no-install-recommends \
    libpq5 \
    && rm -rf /var/lib/apt/lists/*

COPY --from=builder /install /usr/local
WORKDIR /app
COPY . .

RUN useradd --create-home appuser
USER appuser

EXPOSE 8000
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

5.4 Build Cache Strategy

# ❌ Inefficient: npm install reruns every time the source changes
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build

# ✅ Efficient: npm install reruns only when package.json changes
FROM node:20-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

The principle for ordering cache layers: put layers that change rarely near the top and layers that change often near the bottom.

Changes rarely ─── FROM (base image)
RUN apt-get install (system packages)
COPY package.json (dependency definitions)
RUN npm ci (install dependencies)
COPY . . (source code)
Changes often ──── RUN npm run build (build)

5.5 Script-Based Builds with Buildah

Buildah is a powerful tool that builds images from a shell script, without a Dockerfile.

#!/bin/bash
# buildah-build.sh — build an image without a Dockerfile

# Create a new container (starting from an empty image)
container=$(buildah from scratch)

# Or start from a base image
container=$(buildah from alpine:3.19)

# Install packages
buildah run $container -- apk add --no-cache nginx

# Copy files
buildah copy $container ./nginx.conf /etc/nginx/nginx.conf
buildah copy $container ./html /var/www/html

# Configuration
buildah config --port 80 $container
buildah config --entrypoint '["/usr/sbin/nginx", "-g", "daemon off;"]' $container
buildah config --author "DevOps Team" $container
buildah config --label maintainer="devops@example.com" $container

# Commit it as an image
buildah commit $container myapp:latest

# Clean up
buildah rm $container

# Check the build result
buildah images

5.6 Copying and Inspecting Images with Skopeo

Skopeo can copy images directly between registries or inspect them without downloading them.

# Copy an image directly between registries (no local download!)
skopeo copy docker://docker.io/nginx:latest docker://quay.io/myorg/nginx:latest

# Inspect the image metadata (without downloading)
skopeo inspect docker://docker.io/library/nginx:latest

# List the image tags
skopeo list-tags docker://docker.io/library/nginx

# Save the image into a local directory (OCI format)
skopeo copy docker://nginx:latest oci:./nginx-oci:latest

# Save the image as a tar file
skopeo copy docker://nginx:latest docker-archive:./nginx.tar:nginx:latest

# Copy with authentication to a private registry
skopeo copy --src-creds user:pass --dest-creds user:pass \
  docker://source-registry.com/app:v1 \
  docker://dest-registry.com/app:v1

# Delete an image (from the registry)
skopeo delete docker://myregistry.com/myapp:old-tag

6. Container Lifecycle Commands

6.1 Creating a Container (Create)

create only creates the container; it does not start it. You start it afterwards with start.

# Create the container (without starting it)
docker create --name my-nginx nginx:latest
podman create --name my-nginx nginx:latest

# Check the created container
docker ps -a
podman ps -a

# Start the created container
docker start my-nginx
podman start my-nginx

6.2 Running a Container (Run) — All the Main Options

run performs create + start in one step. It is the command you use most often when running containers.

# ============================================
# A basic run
# ============================================
docker run nginx
podman run nginx

# ============================================
# Run in the background (-d: detach)
# ============================================
docker run -d --name web nginx
podman run -d --name web nginx

# ============================================
# Interactive mode (-it: interactive + tty)
# ============================================
docker run -it ubuntu:22.04 bash
podman run -it ubuntu:22.04 bash

# ============================================
# Delete automatically on exit (--rm)
# ============================================
docker run --rm -it alpine sh
podman run --rm -it alpine sh

# ============================================
# Port mapping (-p host:container)
# ============================================
docker run -d -p 8080:80 nginx                    # A specific port
docker run -d -p 80:80 -p 443:443 nginx           # Several ports
docker run -d -p 127.0.0.1:8080:80 nginx          # A specific interface
docker run -d -P nginx                             # Map random ports automatically
podman run -d -p 8080:80 nginx

# ============================================
# Volume mounts (-v host:container[:options])
# ============================================
docker run -d -v /host/data:/container/data nginx             # Bind mount
docker run -d -v myvolume:/container/data nginx               # Named volume
docker run -d -v /host/data:/container/data:ro nginx          # Read-only
docker run -d --mount type=tmpfs,destination=/tmp nginx       # tmpfs
podman run -d -v /host/data:/container/data:Z nginx           # SELinux label (:Z)

# ============================================
# Environment variables (-e, --env-file)
# ============================================
docker run -d -e MYSQL_ROOT_PASSWORD=secret mysql:8
docker run -d -e DB_HOST=db -e DB_PORT=5432 myapp
docker run -d --env-file .env myapp
podman run -d -e MYSQL_ROOT_PASSWORD=secret mysql:8

# ============================================
# Resource limits (--memory, --cpus)
# ============================================
docker run -d --memory 512m --memory-swap 1g nginx
docker run -d --cpus 1.5 nginx
docker run -d --cpus 2 --memory 1g --memory-reservation 512m myapp
podman run -d --memory 512m --cpus 1.5 nginx

# ============================================
# Restart policy (--restart)
# ============================================
docker run -d --restart always nginx          # Always restart
docker run -d --restart unless-stopped nginx  # Restart except after a manual stop
docker run -d --restart on-failure:5 nginx    # Restart up to 5 times on failure
docker run -d --restart no nginx              # Never restart (the default)
podman run -d --restart always nginx

# ============================================
# Network settings (--network)
# ============================================
docker run -d --network my-network nginx
docker run -d --network host nginx            # Use the host network directly
docker run -d --network none nginx            # No network
podman run -d --network my-network nginx

# ============================================
# Sharing the PID / IPC namespace
# ============================================
docker run -d --pid host nginx                # The host PID namespace
docker run -d --pid container:other nginx     # Share another container's PID namespace
docker run -d --ipc host nginx                # Share the host IPC namespace

# ============================================
# Other useful options
# ============================================
docker run -d --hostname myhost nginx               # Set the hostname
docker run -d --dns 8.8.8.8 nginx                   # Specify the DNS server
docker run -d --add-host mydb:10.0.0.5 nginx        # Add an /etc/hosts entry
docker run -d --workdir /app myapp                   # Working directory
docker run -d --user 1000:1000 myapp                 # The user to run as
docker run -d --read-only myapp                      # A read-only filesystem
docker run -d --log-driver json-file \
  --log-opt max-size=10m --log-opt max-file=3 nginx  # Log driver settings

6.3 Listing Containers (ps)

# List the running containers
docker ps
podman ps

# Every container (including stopped ones)
docker ps -a
podman ps -a

# The n most recently created containers
docker ps -n 5
podman ps -n 5

# Print the container IDs only
docker ps -q
podman ps -q

# Custom format
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"
podman ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}"

# Filter on a specific status
docker ps -f status=exited
docker ps -f status=running
docker ps -f name=web
podman ps -f status=exited

6.4 Starting, Stopping, and Restarting Containers

# Start a container
docker start my-nginx
podman start my-nginx

# Start several containers at once
docker start web db redis
podman start web db redis

# Stop a container (SIGTERM → SIGKILL after 10 seconds)
docker stop my-nginx
podman stop my-nginx

# Specify the timeout (force kill after 30 seconds)
docker stop -t 30 my-nginx
podman stop -t 30 my-nginx

# Stop every running container
docker stop $(docker ps -q)
podman stop -a

# Kill a container (SIGKILL)
docker kill my-nginx
podman kill my-nginx

# Send a specific signal
docker kill -s SIGHUP my-nginx
podman kill -s SIGHUP my-nginx

# Restart a container
docker restart my-nginx
podman restart my-nginx

# Restart every container
docker restart $(docker ps -q)
podman restart -a

6.5 Pausing and Resuming Containers

# Pause a container (SIGSTOP — freezes the processes)
docker pause my-nginx
podman pause my-nginx

# Unpause (SIGCONT)
docker unpause my-nginx
podman unpause my-nginx

6.6 Removing Containers

# Remove a stopped container
docker rm my-nginx
podman rm my-nginx

# Force-remove a running container
docker rm -f my-nginx
podman rm -f my-nginx

# Remove its volumes too
docker rm -v my-nginx
podman rm -v my-nginx

# Remove every stopped container
docker container prune -f
podman container prune -f

# Remove every container (including running ones)
docker rm -f $(docker ps -aq)
podman rm -f -a

6.7 Renaming, Committing, and Waiting on Containers

# Rename a container
docker rename old-name new-name
podman rename old-name new-name

# Save a container as an image (snapshot its current state)
docker commit my-container myimage:snapshot
podman commit my-container myimage:snapshot

# Add metadata during the commit
docker commit -m "Added config files" -a "Author" my-container myimage:v2
podman commit -m "Added config files" -a "Author" my-container myimage:v2

# Wait for the container to exit (returns the exit code)
docker wait my-container
podman wait my-container

7. Container Monitoring and Debugging

7.1 Checking the Logs

# Print the whole log
docker logs my-container
podman logs my-container

# Stream the log live (follow)
docker logs -f my-container
podman logs -f my-container

# Show only the last N lines
docker logs --tail 100 my-container
podman logs --tail 100 my-container

# Include timestamps
docker logs -t my-container
podman logs -t my-container

# Logs after a given time
docker logs --since 2024-01-01T00:00:00 my-container
docker logs --since 30m my-container    # The last 30 minutes
docker logs --since 2h my-container     # The last 2 hours
podman logs --since 30m my-container

# Logs before a given time
docker logs --until 2024-01-01T12:00:00 my-container
podman logs --until 2024-01-01T12:00:00 my-container

# Combined: live + the last 50 lines + timestamps
docker logs -f --tail 50 -t my-container
podman logs -f --tail 50 -t my-container

7.2 Live Resource Monitoring (Stats)

# Show live resource usage for every running container
docker stats
podman stats

# Monitor a specific container only
docker stats my-container
podman stats my-container

# Print once and exit (for scripts)
docker stats --no-stream
podman stats --no-stream

# Custom format
docker stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"
podman stats --format "table {{.Name}}\t{{.CPUPerc}}\t{{.MemUsage}}\t{{.NetIO}}"

Example output:

NAME        CPU %     MEM USAGE / LIMIT     NET I/O           BLOCK I/O
web         0.50%     45.2MiB / 512MiB      12.5kB / 8.3kB    4.1MB / 0B
db          2.30%     256MiB / 1GiB         45.2kB / 12.1kB   50MB / 120MB
redis       0.10%     12.5MiB / 256MiB      3.2kB / 1.1kB     0B / 0B

7.3 Checking the Processes (Top)

# List the processes inside the container
docker top my-container
podman top my-container

# Pass ps options
docker top my-container -aux
podman top my-container -aux

# Podman only: supports additional fields
podman top my-container user pid ppid args %cpu %mem
podman top my-container huser hpid

7.4 Inspecting Details

# Full container information (JSON)
docker inspect my-container
podman inspect my-container

# Check the IP address
docker inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-container
podman inspect -f '{{range.NetworkSettings.Networks}}{{.IPAddress}}{{end}}' my-container

# Check the mount information
docker inspect -f '{{json .Mounts}}' my-container | jq .
podman inspect -f '{{json .Mounts}}' my-container | jq .

# Check the status
docker inspect -f '{{.State.Status}}' my-container
podman inspect -f '{{.State.Status}}' my-container

# Check the restart count
docker inspect -f '{{.RestartCount}}' my-container

# Check the environment variables
docker inspect -f '{{range .Config.Env}}{{println .}}{{end}}' my-container
podman inspect -f '{{range .Config.Env}}{{println .}}{{end}}' my-container

# Check the log path (Docker)
docker inspect -f '{{.LogPath}}' my-container

7.5 Copying Files (cp)

# Host → container
docker cp ./config.yml my-container:/app/config.yml
podman cp ./config.yml my-container:/app/config.yml

# Container → host
docker cp my-container:/app/logs/error.log ./error.log
podman cp my-container:/app/logs/error.log ./error.log

# Copy a directory
docker cp ./configs/ my-container:/app/configs/
podman cp ./configs/ my-container:/app/configs/

# Archive mode (preserves symbolic links)
docker cp -a my-container:/app/data ./backup/
podman cp -a my-container:/app/data ./backup/

7.6 Entering a Container (Exec)

This is the core debugging tool for running commands inside a running container.

# Open an interactive shell
docker exec -it my-container bash
docker exec -it my-container sh      # When bash is absent (Alpine, etc.)
podman exec -it my-container bash

# Run a single command
docker exec my-container ls -la /app
podman exec my-container ls -la /app

# Set an environment variable, then run
docker exec -e DEBUG=true my-container node script.js
podman exec -e DEBUG=true my-container node script.js

# Run as a specific user
docker exec -u root my-container apt-get update
podman exec -u root my-container dnf update

# Specify the working directory
docker exec -w /app my-container npm test
podman exec -w /app my-container npm test

# Run the command in the background (detach)
docker exec -d my-container touch /tmp/healthcheck
podman exec -d my-container touch /tmp/healthcheck

7.7 Container Changes (Diff)

# Check the filesystem changes in a container
docker diff my-container
podman diff my-container

# Example output:
# A /tmp/new-file          (Added)
# C /etc/nginx/nginx.conf  (Changed)
# D /var/log/old.log       (Deleted)

7.8 Checking Port Mappings (Port)

# Check a container's port mappings
docker port my-container
podman port my-container

# Check a specific port
docker port my-container 80
podman port my-container 80

# Example output:
# 80/tcp -> 0.0.0.0:8080
# 443/tcp -> 0.0.0.0:8443

7.9 Monitoring Events

# Stream the events live
docker events
podman events

# Filter by event type
docker events --filter event=start
docker events --filter event=stop
docker events --filter event=die
podman events --filter event=start

# Filter to a specific container's events
docker events --filter container=my-container
podman events --filter container=my-container

# Specify a time range
docker events --since 1h --until 30m
podman events --since 1h

# JSON output
docker events --format '{{json .}}'
podman events --format json

8. Network Management

8.1 Network Types

Network driverDescriptionDockerPodman
bridgeThe default network; container-to-container over a virtual bridgedocker0 (default bridge)Netavark (v4+) / CNI
hostUses the host network directly; no port mapping neededSupportedSupported
noneNo network; fully isolatedSupportedSupported
macvlanAssigns a MAC address to the container, attached to the physical networkSupportedSupported
overlayMulti-host networking (Swarm)SupportedNot supported
ipvlanSimilar to macvlan but shares the same MACSupportedSupported

8.2 Network CRUD Commands

# ============================================
# Creating a network
# ============================================
# Create a default bridge network
docker network create my-network
podman network create my-network

# Specify the subnet and gateway
docker network create \
  --subnet 172.20.0.0/16 \
  --gateway 172.20.0.1 \
  my-network
podman network create \
  --subnet 172.20.0.0/16 \
  --gateway 172.20.0.1 \
  my-network

# Specify an IP range
docker network create \
  --subnet 172.20.0.0/16 \
  --ip-range 172.20.240.0/20 \
  --gateway 172.20.0.1 \
  my-network

# An internal network (no external access)
docker network create --internal internal-net
podman network create --internal internal-net

# Specify a particular driver
docker network create --driver macvlan \
  --subnet 192.168.1.0/24 \
  --gateway 192.168.1.1 \
  -o parent=eth0 \
  macvlan-net

# ============================================
# Listing networks
# ============================================
docker network ls
podman network ls

# ============================================
# Network details (Inspect)
# ============================================
docker network inspect my-network
podman network inspect my-network

# Check the connected containers
docker network inspect -f '{{range .Containers}}{{.Name}} {{end}}' my-network

# ============================================
# Removing a network
# ============================================
docker network rm my-network
podman network rm my-network

# Prune unused networks
docker network prune -f
podman network prune -f

8.3 Connecting and Disconnecting Networks

# Connect a running container to a network
docker network connect my-network my-container
podman network connect my-network my-container

# Connect with a fixed IP
docker network connect --ip 172.20.0.10 my-network my-container
podman network connect --ip 172.20.0.10 my-network my-container

# Disconnect from a network
docker network disconnect my-network my-container
podman network disconnect my-network my-container

8.4 An Example of Container-to-Container Communication

On a user-defined network, DNS resolution by container name happens automatically.

# Create a user-defined network
docker network create app-net
podman network create app-net

# The database container
docker run -d --name db --network app-net \
  -e POSTGRES_PASSWORD=secret \
  postgres:16-alpine

# The application container (it can reach the DB by name)
docker run -d --name app --network app-net \
  -e DATABASE_URL="postgresql://postgres:secret@db:5432/mydb" \
  -p 3000:3000 \
  myapp:latest

# Test: ping db from the app container
docker exec app ping -c 3 db
# PING db (172.20.0.2): 56 data bytes
# 64 bytes from 172.20.0.2: icmp_seq=0 ttl=64 time=0.123 ms

Caution: on Docker/Podman's default bridge network, container names are not resolved by DNS. You have to create a user-defined network for name-based communication to work.

8.5 Docker: docker0 bridge vs Podman: Netavark/CNI

Docker creates a Linux bridge interface called docker0 by default and manages networking with iptables rules.

From v4.0 onward, Podman switched its default network stack from CNI (Container Network Interface) to Netavark. Netavark is a container network stack written in Rust that provides DNS resolution together with Aardvark-dns.

# Check the Podman network backend
podman info --format '{{.Host.NetworkBackend}}'
# Output: netavark

# The Netavark + Aardvark-dns structure:
# ┌──────────────┐     ┌──────────────┐
# │  Container A │     │  Container B │
# │  172.20.0.2  │     │  172.20.0.3  │
# └──────┬───────┘     └──────┬───────┘
#        │                     │
# ┌──────▼─────────────────────▼──────┐
# │        Netavark Bridge            │
# │   (nftables-based networking)      │
# ├───────────────────────────────────┤
# │        Aardvark-dns               │
# │   (container name → IP resolution) │
# └───────────────────────────────────┘

9. Volume and Storage Management

9.1 Managing Named Volumes

# ============================================
# Create a volume
# ============================================
docker volume create my-data
podman volume create my-data

# Specify the driver and options
docker volume create --driver local \
  --opt type=nfs \
  --opt o=addr=192.168.1.100,rw \
  --opt device=:/path/to/share \
  nfs-volume

# Add labels
docker volume create --label project=myapp --label env=prod my-data
podman volume create --label project=myapp --label env=prod my-data

# ============================================
# List the volumes
# ============================================
docker volume ls
podman volume ls

# Filtering
docker volume ls -f label=project=myapp
podman volume ls -f label=project=myapp

# Dangling volumes (not attached to any container)
docker volume ls -f dangling=true
podman volume ls -f dangling=true

# ============================================
# Volume details
# ============================================
docker volume inspect my-data
podman volume inspect my-data

# Check the mount point
docker volume inspect -f '{{.Mountpoint}}' my-data
podman volume inspect -f '{{.Mountpoint}}' my-data

# ============================================
# Remove a volume
# ============================================
docker volume rm my-data
podman volume rm my-data

# Prune unused volumes
docker volume prune -f
podman volume prune -f

9.2 Bind Mount vs Named Volume vs tmpfs

Comparison pointBind mountNamed volumetmpfs
Host pathYou specify it directlyManaged by Docker/PodmanNone (memory)
Data persistenceStored permanently on the hostStored permanently in the volumeDeleted when the container exits
PerformanceDepends on the host FSCan be optimizedBest (memory-based)
PortabilityLow (tied to the host path)HighHigh
BackupYou manage it yourselfThe docker volume commandsNot possible
Typical useMounting source code, config filesDB data, uploaded filesTemporary files, secrets
# Bind Mount
docker run -d -v /home/user/data:/app/data nginx
docker run -d --mount type=bind,source=/home/user/data,target=/app/data nginx

# Named Volume
docker run -d -v app-data:/app/data nginx
docker run -d --mount type=volume,source=app-data,target=/app/data nginx

# tmpfs (memory-based, deleted when the container exits)
docker run -d --tmpfs /tmp:rw,size=100m nginx
docker run -d --mount type=tmpfs,destination=/tmp,tmpfs-size=100m nginx

# A read-only mount
docker run -d -v /host/config:/app/config:ro nginx
docker run -d --mount type=bind,source=/host/config,target=/app/config,readonly nginx

# Setting SELinux labels in Podman
podman run -d -v /host/data:/app/data:Z nginx    # Z: private label
podman run -d -v /host/data:/app/data:z nginx    # z: shared label

9.3 Backup and Restore Patterns

# ============================================
# Back up the volume data
# ============================================
# Approach 1: back up using a temporary container
docker run --rm \
  -v my-data:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/my-data-backup.tar.gz -C /source .

podman run --rm \
  -v my-data:/source:ro \
  -v $(pwd):/backup \
  alpine tar czf /backup/my-data-backup.tar.gz -C /source .

# Approach 2: dated backups
docker run --rm \
  -v postgres-data:/source:ro \
  -v $(pwd)/backups:/backup \
  alpine tar czf /backup/postgres-$(date +%Y%m%d).tar.gz -C /source .

# ============================================
# Restore the volume data
# ============================================
# Create a new volume, then restore into it
docker volume create restored-data

docker run --rm \
  -v restored-data:/target \
  -v $(pwd):/backup:ro \
  alpine tar xzf /backup/my-data-backup.tar.gz -C /target

podman run --rm \
  -v restored-data:/target \
  -v $(pwd):/backup:ro \
  alpine tar xzf /backup/my-data-backup.tar.gz -C /target

# ============================================
# Migrating data between volumes
# ============================================
docker run --rm \
  -v old-volume:/from:ro \
  -v new-volume:/to \
  alpine sh -c "cp -a /from/. /to/"

10. Docker Compose vs Podman Compose

10.1 The Basic Structure of docker-compose.yml

# docker-compose.yml (or compose.yml)
version: '3.9' # The Compose file format version (optional in v2)

services:
  web:
    build: ./app # Path to the Dockerfile
    image: myapp:latest # Name of the built image
    container_name: myapp-web # Container name
    ports:
      - '3000:3000' # Port mapping
    environment: # Environment variables
      - NODE_ENV=production
      - DB_HOST=db
    env_file: # Environment variable file
      - .env
    volumes: # Volume mounts
      - ./app:/app
      - node_modules:/app/node_modules
    depends_on: # Dependencies
      db:
        condition: service_healthy
      redis:
        condition: service_started
    networks: # Networks
      - app-net
    restart: unless-stopped # Restart policy
    deploy: # Resource limits
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
        reservations:
          cpus: '0.5'
          memory: 256M
    healthcheck: # Health check
      test: ['CMD', 'curl', '-f', 'http://localhost:3000/health']
      interval: 30s
      timeout: 10s
      retries: 3
      start_period: 40s

  db:
    image: postgres:16-alpine
    container_name: myapp-db
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: admin
      POSTGRES_PASSWORD: secret
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
    networks:
      - app-net
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U admin -d myapp']
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    container_name: myapp-redis
    command: redis-server --appendonly yes --maxmemory 256mb
    volumes:
      - redis-data:/data
    networks:
      - app-net

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local
  node_modules:

networks:
  app-net:
    driver: bridge
    ipam:
      config:
        - subnet: 172.28.0.0/16

10.2 The Main Compose Commands

# ============================================
# Docker Compose (v2: docker compose / v1: docker-compose)
# ============================================

# Start the services (in the background)
docker compose up -d
docker compose -f docker-compose.prod.yml up -d

# Build, then start the services
docker compose up -d --build

# Start only a specific service
docker compose up -d web db

# Scaling (adjusting the number of service instances)
docker compose up -d --scale web=3

# Stop the services and clean up the resources
docker compose down

# Delete the volumes as well
docker compose down -v

# Delete the images as well
docker compose down --rmi all

# Service list and status
docker compose ps

# Service logs
docker compose logs
docker compose logs -f web
docker compose logs --tail 100 web db

# Run a command inside a service
docker compose exec web bash
docker compose exec db psql -U admin -d myapp

# Run a one-off command (run creates a new container)
docker compose run --rm web npm test
docker compose run --rm web python manage.py migrate

# Build the services
docker compose build
docker compose build --no-cache web

# Restart the services
docker compose restart
docker compose restart web

# Validate the configuration
docker compose config

# Pull the images
docker compose pull

# ============================================
# Podman Compose
# ============================================

# Install podman-compose
pip3 install podman-compose

# Or use podman compose on Podman 4.7+ (the plugin approach)
# The usage is identical to docker compose

podman compose up -d
podman compose down
podman compose ps
podman compose logs -f web
podman compose exec web bash

10.3 A Practical Example: a 3-Tier Web App + DB + Redis

# compose.yml — a production 3-tier architecture
services:
  # ============================================
  # Tier 1: Reverse Proxy (Nginx)
  # ============================================
  nginx:
    image: nginx:1.25-alpine
    container_name: proxy
    ports:
      - '80:80'
      - '443:443'
    volumes:
      - ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
      - ./nginx/ssl:/etc/nginx/ssl:ro
    depends_on:
      app:
        condition: service_healthy
    networks:
      - frontend
    restart: unless-stopped

  # ============================================
  # Tier 2: Application (Node.js)
  # ============================================
  app:
    build:
      context: ./app
      dockerfile: Dockerfile
      args:
        NODE_ENV: production
    container_name: app
    expose:
      - '3000'
    environment:
      - NODE_ENV=production
      - DB_HOST=postgres
      - DB_PORT=5432
      - DB_NAME=appdb
      - DB_USER=appuser
      - DB_PASS=${DB_PASSWORD}
      - REDIS_URL=redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - frontend
      - backend
    healthcheck:
      test:
        [
          'CMD',
          'node',
          '-e',
          "require('http').get('http://localhost:3000/health', (r) => { process.exit(r.statusCode === 200 ? 0 : 1) })",
        ]
      interval: 30s
      timeout: 10s
      retries: 3
    deploy:
      resources:
        limits:
          cpus: '2.0'
          memory: 1G
    restart: unless-stopped

  # ============================================
  # Tier 3: Database (PostgreSQL)
  # ============================================
  postgres:
    image: postgres:16-alpine
    container_name: postgres
    environment:
      POSTGRES_DB: appdb
      POSTGRES_USER: appuser
      POSTGRES_PASSWORD: ${DB_PASSWORD}
      PGDATA: /var/lib/postgresql/data/pgdata
    volumes:
      - postgres-data:/var/lib/postgresql/data
      - ./db/init:/docker-entrypoint-initdb.d:ro
    networks:
      - backend
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U appuser -d appdb']
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 512M
    restart: unless-stopped

  # ============================================
  # Cache: Redis
  # ============================================
  redis:
    image: redis:7-alpine
    container_name: redis
    command: >
      redis-server
      --appendonly yes
      --maxmemory 256mb
      --maxmemory-policy allkeys-lru
      --requirepass ${REDIS_PASSWORD}
    volumes:
      - redis-data:/data
    networks:
      - backend
    healthcheck:
      test: ['CMD', 'redis-cli', '-a', '${REDIS_PASSWORD}', 'ping']
      interval: 10s
      timeout: 5s
      retries: 5
    deploy:
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
    restart: unless-stopped

volumes:
  postgres-data:
    driver: local
  redis-data:
    driver: local

networks:
  frontend:
    driver: bridge
  backend:
    driver: bridge
    internal: true # Block external access

10.4 Per-Environment Configuration (Override)

# compose.override.yml — the development environment (loaded automatically)
services:
  app:
    build:
      args:
        NODE_ENV: development
    volumes:
      - ./app/src:/app/src # Hot-reload the source code
    environment:
      - NODE_ENV=development
      - DEBUG=app:*
    ports:
      - '3000:3000' # Direct access during development
      - '9229:9229' # The Node.js debugger port

  postgres:
    ports:
      - '5432:5432' # Direct access during development
# compose.prod.yml — the production environment
services:
  app:
    build:
      args:
        NODE_ENV: production
    deploy:
      replicas: 3
      resources:
        limits:
          cpus: '4.0'
          memory: 2G

  nginx:
    deploy:
      resources:
        limits:
          cpus: '1.0'
          memory: 256M
# The development environment (compose.yml + compose.override.yml merged automatically)
docker compose up -d

# The production environment (skips the override, uses the prod settings)
docker compose -f compose.yml -f compose.prod.yml up -d

# Using a per-environment .env file
docker compose --env-file .env.production up -d

11. Podman-Specific Features

11.1 Managing Pods

A Podman pod is the same concept as a Kubernetes Pod. The containers inside one pod share the network namespace, IPC namespace, and PID namespace.

# ============================================
# Creating a pod
# ============================================
# Create a basic pod
podman pod create --name my-pod

# Create it with port mappings (ports are defined at the pod level)
podman pod create --name web-pod -p 8080:80 -p 8443:443

# Specify the network
podman pod create --name web-pod --network my-network

# ============================================
# Adding containers to the pod
# ============================================
# Run a container inside the pod (the --pod option)
podman run -d --pod my-pod --name nginx nginx:latest
podman run -d --pod my-pod --name php php:8.2-fpm

# Containers inside a pod can talk to each other over localhost
# nginx → localhost:9000 → php-fpm

# ============================================
# Managing pods
# ============================================
# List the pods
podman pod list
podman pod ps

# Pod details
podman pod inspect my-pod

# Start/stop/restart a pod
podman pod start my-pod
podman pod stop my-pod
podman pod restart my-pod

# Pause/unpause a pod
podman pod pause my-pod
podman pod unpause my-pod

# Remove a pod (including its containers)
podman pod rm my-pod
podman pod rm -f my-pod    # Force removal

# Remove every pod
podman pod rm -a -f

# Check the pod's processes
podman pod top my-pod

# The pod's resource usage
podman pod stats my-pod

11.2 Generating Kubernetes YAML (podman generate kube)

You can convert a container/pod configuration you tested locally into Kubernetes YAML automatically.

# Generate K8s YAML from a container
podman generate kube my-container > deployment.yaml

# Generate K8s YAML from a pod
podman generate kube my-pod > pod.yaml

# Include the Service definition
podman generate kube --service my-pod > pod-with-service.yaml

# An example of the generated YAML:
# apiVersion: v1
# kind: Pod
# metadata:
#   name: my-pod
# spec:
#   containers:
#   - name: nginx
#     image: nginx:latest
#     ports:
#     - containerPort: 80
#       hostPort: 8080
#   - name: php
#     image: php:8.2-fpm

11.3 Running Kubernetes YAML (podman play kube)

Conversely, you can run a Kubernetes YAML file directly in Podman.

# Create a pod from a K8s YAML file
podman play kube deployment.yaml

# ConfigMap support
podman play kube pod.yaml --configmap configmap.yaml

# Secret support
podman play kube pod.yaml --seccomp-profile-root ./profiles

# Replace existing resources (update)
podman play kube --replace pod.yaml

# Delete the resources
podman play kube --down pod.yaml

# Include a build
podman play kube --build pod.yaml

11.4 Creating a systemd Service

# Turn a container into a systemd service
podman generate systemd --name my-container > ~/.config/systemd/user/my-container.service

# Pick up the new configuration (user level)
systemctl --user daemon-reload
systemctl --user enable my-container.service
systemctl --user start my-container.service

# Turn a pod into a systemd service
podman generate systemd --name my-pod --files
# → generates pod-my-pod.service, container-nginx.service, container-php.service

# Options
podman generate systemd --name my-container \
  --restart-policy always \
  --time 30 \
  --new    # Create a new container on start (recommended)

11.5 Quadlet: Native systemd Container Management

Introduced in Podman 4.4+, Quadlet manages containers declaratively in the systemd unit file format.

# ~/.config/containers/systemd/webapp.container
[Unit]
Description=My Web Application
After=network-online.target

[Container]
Image=docker.io/library/nginx:latest
ContainerName=webapp
PublishPort=8080:80
Volume=webapp-data:/usr/share/nginx/html:ro
Environment=NGINX_WORKER_PROCESSES=auto
AutoUpdate=registry
HealthCmd=curl -f http://localhost/ || exit 1
HealthInterval=30s

[Service]
Restart=always
TimeoutStartSec=300

[Install]
WantedBy=default.target
# Place the Quadlet file, then apply it
# System level: /etc/containers/systemd/
# User level: ~/.config/containers/systemd/

systemctl --user daemon-reload
systemctl --user start webapp.service
systemctl --user enable webapp.service
systemctl --user status webapp.service

# Check the logs
journalctl --user -u webapp.service -f

11.6 Rootless Containers in Detail

Podman's rootless containers use a user namespace to map root (UID 0) inside the container to an ordinary user UID on the host.

# Check the current user's UID mapping
cat /etc/subuid
# user1:100000:65536
# → user1 uses host UIDs 100000-165535 as the UIDs inside the container

cat /etc/subgid
# user1:100000:65536

# The mapping structure:
# Container UID 0 (root) → host UID 100000 (an ordinary user)
# Container UID 1        → host UID 100001
# Container UID 65535    → host UID 165535

# Check the rootless status
podman info --format '{{.Host.Security.Rootless}}'
# true

# Check the constraints of rootless mode
podman info | grep -A 5 rootless

# Run a rootless container (the default)
podman run -d --name rootless-nginx -p 8080:80 nginx

# Check the process on the host — it runs as an ordinary user, not root
ps aux | grep nginx
# user1    12345  ... nginx: master process

12. Security Commands and Configuration

12.1 Configuring Rootless Mode

# ============================================
# Configuring rootless Docker
# ============================================
# Install rootless Docker
curl -fsSL https://get.docker.com/rootless | sh

# Set the environment variables (~/.bashrc)
export PATH=$HOME/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock

# Start rootless Docker
systemctl --user start docker
systemctl --user enable docker

# ============================================
# Configuring rootless Podman (enabled by default)
# ============================================
# Check the subuid/subgid configuration
grep $USER /etc/subuid /etc/subgid

# Add it if it is missing
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

# Namespace migration (remaps existing images)
podman system migrate

12.2 Security Options

# ============================================
# Capability management
# ============================================
# Drop every capability, then add back only what you need (least privilege)
docker run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx
podman run --cap-drop ALL --cap-add NET_BIND_SERVICE nginx

# Check the default capabilities
docker run --rm alpine cat /proc/1/status | grep Cap
podman run --rm alpine cat /proc/1/status | grep Cap

# The main capabilities:
# NET_BIND_SERVICE : bind to ports below 1024
# SYS_PTRACE       : debug processes (strace and the like)
# NET_RAW           : use RAW sockets (ping and the like)
# CHOWN             : change file ownership
# DAC_OVERRIDE      : bypass file permissions
# SETUID/SETGID     : change UID/GID

# ============================================
# Security Options
# ============================================
# no-new-privileges: prevents privilege escalation on execve
docker run --security-opt no-new-privileges:true myapp
podman run --security-opt no-new-privileges:true myapp

# Apply a seccomp profile
docker run --security-opt seccomp=./custom-seccomp.json myapp
podman run --security-opt seccomp=./custom-seccomp.json myapp

# AppArmor profile (Docker/Ubuntu)
docker run --security-opt apparmor=docker-default myapp

# SELinux label (Podman/RHEL)
podman run --security-opt label=type:container_t myapp
podman run --security-opt label=disable myapp    # Disable SELinux

12.3 Read-Only Containers

# Make the filesystem read-only
docker run --read-only nginx
podman run --read-only nginx

# Mount only the directories that need writes as tmpfs
docker run --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  --tmpfs /var/cache/nginx \
  nginx

podman run --read-only \
  --tmpfs /tmp \
  --tmpfs /var/run \
  --tmpfs /var/cache/nginx \
  nginx

12.4 Verifying Image Security

# ============================================
# Docker Content Trust (DCT)
# ============================================
# Allow pulling/running only signed images
export DOCKER_CONTENT_TRUST=1
docker pull nginx:latest    # Signature verified

# Sign an image
docker trust sign myregistry.com/myapp:v1.0

# Verify the signature
docker trust inspect --pretty myregistry.com/myapp:v1.0

# ============================================
# Podman image signing (GPG-based)
# ============================================
# Check the signature policy file
cat /etc/containers/policy.json

# Sign an image with a GPG key
podman push --sign-by security@example.com myregistry.com/myapp:v1.0

# An example signature policy (/etc/containers/policy.json)
# {
#   "default": [{"type": "reject"}],
#   "transports": {
#     "docker": {
#       "myregistry.com": [
#         {
#           "type": "signedBy",
#           "keyType": "GPGKeys",
#           "keyPath": "/etc/pki/rpm-gpg/RPM-GPG-KEY-myorg"
#         }
#       ],
#       "docker.io": [{"type": "insecureAcceptAnything"}]
#     }
#   }
# }

# Verifying image integrity with Skopeo
skopeo inspect --raw docker://myregistry.com/myapp:v1.0 | jq .

12.5 A Security Best Practice Checklist

ItemDocker command / settingPodman command / setting
No running as rootUSER nonroot in DockerfileRootless by default
Minimize capabilities--cap-drop ALL --cap-add ...--cap-drop ALL --cap-add ...
Read-only FS--read-only--read-only
Prevent privilege escalation--security-opt no-new-privileges--security-opt no-new-privileges
Resource limits--memory --cpus --pids-limit--memory --cpus --pids-limit
Network isolation--network none / internal network--network none / internal network
Image signingDocker Content TrustGPG signing / sigstore
Secret managementDocker Secrets / environment variablesPodman secrets / environment variables
Base imagedistroless / scratch / alpinedistroless / scratch / alpine
Image scanningdocker scout / TrivyTrivy / Grype

13. System Management and Cleanup

13.1 Checking Disk Usage (system df)

# A disk usage summary
docker system df
podman system df

# Detailed information
docker system df -v
podman system df -v

# Example output:
# TYPE            TOTAL    ACTIVE   SIZE      RECLAIMABLE
# Images          15       5        4.2GB     2.8GB (66%)
# Containers      8        3        120MB     80MB (66%)
# Local Volumes   10       4        1.5GB     800MB (53%)
# Build Cache     20       0        500MB     500MB (100%)

13.2 Cleaning Everything Up (system prune)

# Clean stopped containers + dangling images + unused networks + the build cache
docker system prune
podman system prune

# Run without a confirmation prompt
docker system prune -f
podman system prune -f

# Include unused images too (careful!)
docker system prune -a -f
podman system prune -a -f

# Include volumes too (very careful! data loss is possible)
docker system prune -a --volumes -f
podman system prune -a --volumes -f

# Clean only resources older than a given age
docker system prune -a --filter "until=720h" -f    # older than 30 days

13.3 System Information and Version

# Full system information
docker info
podman info

# The main things to check:
# - Storage Driver
# - Cgroup Version (v1/v2)
# - Security Options
# - Kernel Version
# - OS/Architecture
# - Registry configuration

# Version information
docker version
podman version

# Client/server versions separately
docker version --format '{{.Client.Version}}'
docker version --format '{{.Server.Version}}'
podman version --format '{{.Client.Version}}'

14. A Practical Cheat Sheet: 30 Commands You Use Most

Below is a table of the 30 commands used most often when running containers, all in one place. Docker and Podman use identical syntax.

#TaskCommand
1Pull an imagedocker pull nginx:latest
2List imagesdocker images
3Remove an imagedocker rmi nginx:latest
4Prune dangling imagesdocker image prune -f
5Build an imagedocker build -t myapp:latest .
6Run a container (background)docker run -d --name web -p 80:80 nginx
7Run a container (interactive)docker run -it --rm alpine sh
8List running containersdocker ps
9List every containerdocker ps -a
10Stop a containerdocker stop web
11Start a containerdocker start web
12Restart a containerdocker restart web
13Remove a containerdocker rm web
14Force-remove a containerdocker rm -f web
15Remove all stopped containersdocker container prune -f
16Container logsdocker logs -f --tail 100 web
17Enter a container (exec)docker exec -it web bash
18Copy a file (host→container)docker cp file.txt web:/app/
19Copy a file (container→host)docker cp web:/app/log.txt ./
20Monitor resourcesdocker stats
21Detailed information (JSON)docker inspect web
22Create a networkdocker network create my-net
23List networksdocker network ls
24Create a volumedocker volume create my-vol
25List volumesdocker volume ls
26Start the Compose servicesdocker compose up -d
27Stop the Compose servicesdocker compose down
28Log in to a registrydocker login registry.example.com
29Push an imagedocker push myregistry.com/app:v1
30Clean up the whole systemdocker system prune -a -f

Tip: replace docker with podman in every command above and they all keep working.


15. Troubleshooting Guide

15.1 Permission Issues

Docker: "permission denied while trying to connect to the Docker daemon socket"

# Cause: the current user is not in the docker group
# Fix:
sudo usermod -aG docker $USER
newgrp docker    # or log back in

# Verify
groups $USER
docker ps        # should run without an error

Podman Rootless: "Error: could not get runtime: cannot re-exec process"

# Cause: subuid/subgid are not configured
# Fix:
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 $USER

# Namespace migration
podman system migrate

# Verify
podman unshare cat /proc/self/uid_map

Podman Rootless: Volume Mount Permission Problems

# Cause: the UID mapping between host and container does not line up
# Approach 1: change ownership with unshare
podman unshare chown 1000:1000 /host/path/data

# Approach 2: automatic UID mapping with the :U option (Podman 4.0+)
podman run -v /host/data:/data:U myapp

# Approach 3: add an SELinux label (RHEL/CentOS)
podman run -v /host/data:/data:Z myapp

15.2 Network Connectivity Problems

The Container Cannot Reach the Outside Network

# Check DNS
docker exec my-container cat /etc/resolv.conf
docker exec my-container nslookup google.com

# Fix 1: specify the DNS server directly
docker run --dns 8.8.8.8 --dns 8.8.4.4 myapp

# Fix 2: check the iptables/nftables rules
sudo iptables -L -n -t nat
sudo nft list ruleset

# Fix 3: enable IP forwarding
echo 1 | sudo tee /proc/sys/net/ipv4/ip_forward
# To make it permanent: add net.ipv4.ip_forward=1 to /etc/sysctl.conf

Containers Cannot Reach Each Other by Name

# Cause: you are on the default bridge network (no DNS resolution)
# Fix: create a user-defined network
docker network create my-net
docker run -d --network my-net --name db postgres:16
docker run -d --network my-net --name app \
  -e DB_HOST=db myapp    # now "db" resolves

Podman Rootless Fails to Bind a Port Below 1024

# Cause: rootless mode cannot use ports below 1024 by default
# Fix 1: use a port at or above 1024
podman run -d -p 8080:80 nginx

# Fix 2: change the unprivileged port range with sysctl
sudo sysctl -w net.ipv4.ip_unprivileged_port_start=80
# To make it permanent: /etc/sysctl.conf

# Fix 3: use rootful mode
sudo podman run -d -p 80:80 nginx

15.3 Storage and Disk Space Problems

The "no space left on device" Error

# Check the disk usage
docker system df -v
podman system df -v

# Clean up step by step:
# Step 1: remove stopped containers
docker container prune -f

# Step 2: remove dangling images
docker image prune -f

# Step 3: remove unused volumes (careful: check the data first!)
docker volume prune -f

# Step 4: remove the build cache
docker builder prune -f

# Step 5: full cleanup (last resort)
docker system prune -a --volumes -f

# Prune only images older than a given age
docker image prune -a --filter "until=720h" -f    # older than 30 days

Changing the Podman Storage Path

# The default storage location for rootless Podman
# ~/.local/share/containers/storage/

# Change the storage path: ~/.config/containers/storage.conf
# [storage]
# driver = "overlay"
# graphroot = "/mnt/large-disk/containers/storage"

# Migrate after the change
podman system reset    # careful: this deletes every container and image

15.4 Image Build Failures

Multi-Platform Build Errors (Docker)

# The QEMU emulator has to be installed
docker run --rm --privileged multiarch/qemu-user-static --reset -p yes

# Create a buildx builder
docker buildx create --name multiarch --use
docker buildx inspect multiarch --bootstrap

# Multi-platform build
docker buildx build --platform linux/amd64,linux/arm64 \
  -t myapp:latest --push .

When the Build Context Is Too Large

# Create a .dockerignore file
cat > .dockerignore << 'EOF'
.git
node_modules
dist
*.log
.env
.DS_Store
**/*.test.js
**/*.spec.js
coverage
.nyc_output
EOF

# Check the build context size
du -sh . --exclude=.git --exclude=node_modules

# Use only a specific path as the build context
docker build -f Dockerfile -t myapp . --build-context src=./src

15.5 Common Debugging Patterns

# ============================================
# Debugging a container that exits immediately
# ============================================
# Check the exit log
docker logs my-container
docker inspect -f '{{.State.ExitCode}}' my-container
docker inspect -f '{{.State.Error}}' my-container

# Override the entrypoint to drop into a shell
docker run -it --entrypoint sh myapp:latest
podman run -it --entrypoint sh myapp:latest

# ============================================
# Network debugging
# ============================================
# A container dedicated to network debugging
docker run --rm -it --network container:target-container \
  nicolaka/netshoot bash

# Debugging on a specific network
docker run --rm -it --network my-net nicolaka/netshoot bash
# → nslookup, dig, curl, tcpdump, iperf3, netstat, and more are available

# ============================================
# Filesystem debugging
# ============================================
# Check the filesystem changes in a container
docker diff my-container

# Extract the container's filesystem as a tar for analysis
docker export my-container | tar -tf - | head -50

# ============================================
# Checking resource limits
# ============================================
# Check the container's cgroup settings
docker exec my-container cat /sys/fs/cgroup/memory.max
docker exec my-container cat /sys/fs/cgroup/cpu.max

# Check for OOM kills
docker inspect -f '{{.State.OOMKilled}}' my-container

16. References and Further Reading

Official Documentation

OCI Standards

Security Guides

Comments

No comments yet.

Sign in to leave a comment