Day 41 — Podman/Docker modules
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:
- Prefer a nixpkgs service module when it exists
- When you need an image, declare it in the flake/host modules
- 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:8081Theory 7 — Stateful volumes
If the container needs state:
- Create host directory with tmpfiles or activation
- Bind mount
- Include in backup story
- 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 psLab 1 — Enable Podman (or Docker)
# add module, rebuild
podman info 2>/dev/null || docker info 2>/dev/nullDocument 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/ | headLab 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 setupJournal: 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 failsWorked 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 || trueMap 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.