Day 41 — Podman/Docker modules

Updated

July 30, 2026

Day 41 — Podman/Docker modules

Stage IV · ~4h
Goal: Run OCI containers on NixOS via declarative modules (prefer Podman), understand rootless vs rootful trade-offs, and compare honestly to Day 40 nspawn—without turning the host into an imperative docker run snowflake.

Why this day exists

Day 40 covered NixOS-native containers. The industry default many colleagues know is Docker/Podman + images. On NixOS you should still:

  1. Prefer a nixpkgs service module when it exists
  2. When you need an image, declare it in the flake/host modules
  3. Track ports, volumes, and update cadence

Theory 1 — Podman vs Docker on NixOS

Engine Notes
Podman Daemonless roots; good rootless story; often preferred on NixOS labs
Docker Familiar Compose ecosystem; virtualisation.docker module

House suggestion: Podman unless you must match a Docker-only workflow.

# modules/services/podman.nix
{
  virtualisation.podman = {
    enable = true;
    dockerCompat = true; # optional: docker CLI alias — document if used
  };
}

Docker alternative sketch:

{
  virtualisation.docker.enable = true;
  # users.users.alice.extraGroups = [ "docker" ]; # rootful socket risk
}

docker group membership is effectively near-root. Prefer rootless or rootful with care.


Theory 2 — Rootless vs rootful

Mode Pros Cons
Rootless Better default security Port <1024 hard; storage quirks
Rootful Simpler networking/ports Higher blast radius

Rootless Podman for user workloads; rootful only when needed for lab ports—or put proxy on host and map high ports.


Theory 3 — Declarative containers (OCI)

NixOS options vary by version; common patterns include virtualisation.oci-containers.containers (backend docker/podman).

# modules/services/oci-hello.nix
{
  virtualisation.oci-containers = {
    backend = "podman";
    containers = {
      hello-lab = {
        image = "docker.io/library/nginx:alpine";
        ports = [ "127.0.0.1:8081:80" ];
        # volumes = [ "/var/lib/hello-lab:/usr/share/nginx/html:ro" ];
        autoStart = true;
      };
    };
  };

  # 8081 localhost only — host proxy can front it later
}
Field Discipline
image Pin tags carefully; digests better for prod
ports Prefer 127.0.0.1: binds
volumes Host paths must be managed (tmpfiles/persist)
environment No secrets in plaintext—use secret files / sops-generated env

Exact option schema: confirm on search.nixos.org for 26.05.


Theory 4 — Images and supply chain (awareness)

Practice Why
Pin by digest Tags move
Prefer known registries Reduce surprise
Update deliberately Same as flake lock discipline
Scan when available Optional tooling

Nix can build images from packages (dockerTools)—Stage V+ packaging mindset. Today: consume one official image declaratively.


Theory 5 — Compare to Day 40

Dimension nspawn containers.* OCI Podman
Guest config NixOS modules Image + env/cmd
Updates nixpkgs guest Image pulls
Good for Nix-first isolation Vendor images
Store integration Strong Looser

Write your own two-sentence preference for the Stage IV slice.


Theory 6 — Firewall and proxy integration

clients → host :443 Caddy/Nginx → 127.0.0.1:8081 → OCI nginx

Do not publish OCI ports on 0.0.0.0 unless docs/ports.md says so and auth exists.

# proxy snippet idea
# reverse_proxy 127.0.0.1:8081

Theory 7 — Stateful volumes

If the container needs state:

  1. Create host directory with tmpfiles or activation
  2. Bind mount
  3. Include in backup story
  4. Never put secrets into world-readable volume without mode care

Worked example — Full small stack slice

{ ... }:
{
  imports = [ ./podman.nix ];

  virtualisation.oci-containers.backend = "podman";
  virtualisation.oci-containers.containers.webish = {
    image = "docker.io/library/nginx:alpine";
    ports = [ "127.0.0.1:8081:80" ];
    autoStart = true;
  };
}
curl -sS http://127.0.0.1:8081/ | head
podman ps

Lab 1 — Enable Podman (or Docker)

# add module, rebuild
podman info 2>/dev/null || docker info 2>/dev/null

Document rootless vs rootful choice.


Lab 2 — Declarative OCI container

Define one container with localhost port publish. Rebuild. Verify:

podman ps
curl -sS http://127.0.0.1:8081/ | head

Lab 3 — Imperative contrast (then undo)

# optional short imperative run to feel the snowflake
podman run --rm -p 127.0.0.1:8082:80 docker.io/library/nginx:alpine
# stop/remove; do NOT leave unmanaged containers as the real setup

Journal: why the flake definition wins for rebuildability.


Lab 4 — Wire to proxy (optional but valuable)

Add a reverse_proxy path or separate vhost to the OCI port. Update ports.md.


Lab 5 — Secret anti-pattern check

Confirm no environment = { PASSWORD = "..." } in nix. If the image needs a password, plan sops-backed env file mount (even if deferred).


Lab 6 — Comparison table filled

Complete Day 40 vs Day 41 table in docs/isolation.md with your results (commands, friction, restart behavior).



Theory 8 — Image pins and digests

# Prefer tags you understand; better: digest pins when available
image = "public.ecr.aws/docker/library/nginx:1.27";
# image = "nginx@sha256:…";

Floating :latest is the OCI equivalent of an unlocked flake input. For lab demos tags are OK; note the production preference for digests.


Theory 9 — Rootless paths and user namespaces

Rootless Podman stores containers under the user data dir. On NixOS:

Concern Note
subuid/subgid User must be mapped
Lingering User services may need lingering for boot-start
Ports <1024 Often need rootful or redirect
podman info | head
id alice  # check subuids if rootless fails

Worked example — oci-containers service shape

virtualisation.oci-containers.containers."lab-web" = {
  image = "docker.io/library/nginx:1.27";
  ports = [ "127.0.0.1:8081:80" ];
  autoStart = true;
};

Bind published ports to 127.0.0.1 and front with Day 38 proxy—do not publish :80 on all interfaces unless intentional.


Lab 7 — Volume inventory

podman volume ls 2>/dev/null || docker volume ls 2>/dev/null || true

Map each volume to a backup/persist decision in notes (even if “ephemeral OK”).


Lab 8 — Update story one-pager

Write five lines: how you will refresh images, who tests, whether flake lock/image digest changes ship together.

Common gotchas

Symptom / mistake What to do
Port already in use Conflict with Day 38 backend—choose free port
Image pull fails Network/registry; mirror; pin
Rootless port 80 bind fails Use high ports + host proxy
docker group casual Avoid; understand privilege
State lost on recreate Bind volume on host
Forgot autoStart Container missing after reboot

Checkpoint

  • Podman or Docker enabled via NixOS module
  • One OCI container declarative in flake
  • Localhost publish; ports documented
  • Imperative snowflake cleaned up
  • Isolation docs updated vs nspawn
  • No plaintext secrets in container env

Commit

git add .
git commit -m "day41: podman/docker OCI module pattern"

Write three personal gotchas before continuing.


Tomorrow

Day 42 — Observability pattern: metrics and/or structured logs for one service—Prometheus + Grafana or a minimal logs-first approach.