Roadmap Học Podman Thật Sự — Từ Linux Primitives Đến Production

Không phải hướng dẫn lệnh. Đây là lộ trình học Podman từ nền tảng — hiểu Linux namespaces, cgroups, overlayfs, OCI spec trước, rồi mới hiểu tại sao Podman hoạt động như vậy. Dành cho tất cả levels.

Khau Van Nam 14 phút đọc
Mục lục

Hầu hết tutorial dạy Podman theo hướng: podman run, podman build, podman compose. Bạn học được lệnh nhưng khi có lỗi lạ — DNS không resolve, container không ra internet, permission bị sai — bạn không biết bắt đầu debug từ đâu.

Lộ trình này đi ngược lại. Học nền tảng trước, lệnh sau. Khi hiểu Linux tạo ra container như thế nào, mọi hành vi của Podman đều có lý do.


Tổng quan lộ trình

Giai đoạn 1 — Linux Primitives
  └── Namespaces, cgroups, capabilities, seccomp

Giai đoạn 2 — Storage
  └── Union filesystem, overlayfs, copy-on-write

Giai đoạn 3 — OCI Specification
  └── Image spec, runtime spec, distribution spec

Giai đoạn 4 — Podman Internals
  └── conmon, netavark, aardvark-dns, pasta/slirp4netns

Giai đoạn 5 — Rootful vs Rootless
  └── User namespaces, UID mapping, newuidmap

Giai đoạn 6 — Networking Deep Dive
  └── Bridge, MASQUERADE, Tailscale, UFW interaction

Giai đoạn 7 — Systemd Integration
  └── Quadlet, socket activation, linger

Giai đoạn 8 — Security Layer
  └── Seccomp, capabilities, SELinux/AppArmor

Giai đoạn 1 — Linux Primitives

Container không phải công nghệ mới. Nó là tập hợp các tính năng kernel Linux đã tồn tại từ lâu, đóng gói lại cho dễ dùng.

1.1 Namespaces — Cô lập tài nguyên

Namespace cho phép kernel tạo ra “view” riêng biệt của tài nguyên hệ thống cho mỗi process. Có 8 loại namespace:

NamespaceCô lậpKernel version
mntMount points, filesystem2.4.19
pidProcess IDs2.6.24
netNetwork interfaces, routing, ports2.6.24
ipcIPC, message queues, shared memory2.6.19
utsHostname, domain name2.6.19
userUIDs, GIDs3.8
cgroupCgroup root directory4.6
timeClock offsets5.6

Khi Podman tạo container, nó gọi clone() hoặc unshare() với các flag tương ứng:

// Tạo process mới với namespace riêng
clone(child_fn, stack, 
      CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | CLONE_NEWUTS | CLONE_NEWIPC,
      NULL);

Tự thử với namespace mà không cần Podman:

# Tạo shell với PID namespace riêng
sudo unshare --pid --fork --mount-proc bash

# Bên trong — chỉ thấy process của shell này
ps aux
# PID 1 là bash của bạn

# Tạo network namespace riêng
sudo unshare --net bash
ip link show
# Chỉ thấy loopback, không thấy eth0

Namespace giải thích tại sao container “không thấy” process của host, có hostname riêng, có network interface riêng.

1.2 Cgroups — Giới hạn tài nguyên

Namespace cô lập “cái nhìn”. Cgroups (control groups) giới hạn “mức sử dụng” — CPU, RAM, disk I/O, network bandwidth.

# Xem cgroup của một container đang chạy
cat /proc/$(podman inspect --format '{{.State.Pid}}' mycontainer)/cgroup

# Giới hạn trực tiếp qua Podman
podman run --memory=256m --cpus=0.5 nginx

# Xem giới hạn đã được ghi vào cgroup
cat /sys/fs/cgroup/system.slice/*/memory.max

Podman dùng cgroups v2 trên Debian 13. Toàn bộ container hierarchy nằm dưới /sys/fs/cgroup/machine.slice/ (rootful) hoặc /sys/fs/cgroup/user.slice/ (rootless).

1.3 Capabilities — Phân quyền root

Root trên Linux không phải một quyền monolithic. Từ kernel 2.2, root được chia thành ~40 capabilities độc lập:

CAP_NET_BIND_SERVICE  — bind port < 1024
CAP_NET_ADMIN         — thao tác network interface, routing
CAP_SYS_PTRACE        — attach debugger vào process
CAP_CHOWN             — thay đổi file ownership
CAP_SETUID            — thay đổi UID của process
...

Podman rootful drop hầu hết capabilities theo mặc định, chỉ giữ những gì cần thiết:

# Xem capabilities của container process
podman run --rm alpine cat /proc/1/status | grep Cap

# Thêm capability cụ thể nếu cần
podman run --cap-add=NET_ADMIN alpine ip link add dummy0 type dummy

Điều này giải thích tại sao container chạy root bên trong nhưng vẫn không làm được nhiều thứ trên host.


Giai đoạn 2 — Storage và Overlayfs

2.1 Union Filesystem

Container image được build theo layer. Mỗi instruction trong Dockerfile tạo một layer mới. Các layer này là read-only, chồng lên nhau theo cơ chế union mount.

Layer 4 (RW) — container layer (writable khi chạy)
Layer 3 (RO) — COPY . /app
Layer 2 (RO) — RUN composer install
Layer 1 (RO) — FROM php:8.2-fpm

2.2 OverlayFS

Podman dùng overlay storage driver trên Linux hiện đại. OverlayFS là implementation của union filesystem trong kernel:

upperdir  — writable layer (container-specific changes)
lowerdir  — read-only layers (image layers, có thể nhiều tầng)
workdir   — internal scratch space của overlayfs
merged    — mount point cuối cùng, container thấy tại đây
# Xem overlay mount của container đang chạy
podman inspect mycontainer --format '{{.GraphDriver}}'

# Trực tiếp trên filesystem
ls ~/.local/share/containers/storage/overlay/  # rootless
ls /var/lib/containers/storage/overlay/         # rootful

Cấu trúc thực tế:

/var/lib/containers/storage/overlay/
  ├── <layer-hash-1>/          # image base layer
  │     └── diff/              # nội dung thực của layer
  ├── <layer-hash-2>/          # layer tiếp theo
  │     └── diff/
  └── <container-hash>/        # container layer (upperdir)
        ├── diff/              # những gì container đã thay đổi
        ├── work/              # overlayfs workdir
        └── merged/            # mount point container thấy

2.3 Copy-on-Write

Khi container đọc file từ image layer — không có gì xảy ra, đọc thẳng từ lowerdir. Khi container ghi vào file — overlayfs copy file đó lên upperdir trước, rồi ghi vào bản copy. Image layer gốc không bao giờ bị chỉnh sửa.

# Xem những gì container đã thay đổi so với image
podman diff mycontainer

# Output:
# C /etc/nginx/nginx.conf  (Changed)
# A /var/log/nginx/custom.log  (Added)

2.4 Volume vs Bind Mount

Volume Podman được lưu ở /var/lib/containers/storage/volumes/ (rootful). Không đi qua overlayfs, được mount trực tiếp vào container — hiệu năng tốt hơn cho write-heavy workload như database.

# Volume — Podman quản lý
podman run -v mydata:/var/lib/postgresql/data postgres

# Bind mount — bạn quản lý
podman run -v /host/path:/container/path nginx

Giai đoạn 3 — OCI Specification

OCI (Open Container Initiative) định nghĩa 3 spec để đảm bảo interoperability giữa các container runtime và registry.

3.1 Image Spec

Một OCI image gồm:

Image Index (manifest list)
  └── Manifest (per platform)
        ├── Config (JSON) — entrypoint, env, labels, history
        └── Layers (tar.gz) — filesystem diffs theo thứ tự
# Inspect raw image manifest
podman image inspect nginx --format '{{json .}}' | jq

# Pull và xem layer hashes
podman pull nginx
podman history nginx

3.2 Runtime Spec

Runtime spec (config.json) mô tả cách chạy container: namespace nào cần tạo, capabilities nào giữ lại, mount nào thực hiện, seccomp profile nào áp dụng.

# Xem config.json của container đang chạy
cat /run/containers/storage/overlay-containers/<id>/userdata/config.json

3.3 Podman vs Docker — Không có daemon

Docker có dockerd chạy nền, nhận lệnh từ docker CLI qua Unix socket. Mọi container đều là child process của daemon.

Podman không có daemon. Mỗi podman run fork trực tiếp container process. conmon (container monitor) là process nhỏ đứng giữa Podman CLI và container để giữ stdin/stdout/stderr và capture exit code.

Docker:                          Podman:
docker CLI                       podman CLI
  │                                │
  ▼                                ▼
dockerd (daemon)                conmon (per container)
  │                                │
  ▼                                ▼
containerd                      runc / crun
  │                                │
  ▼                                ▼
runc                            container process


container process
# Xác nhận: sau khi podman run, không có podman process nào còn chạy
podman run -d nginx
ps aux | grep podman
# Chỉ thấy conmon và nginx process

ps aux | grep conmon
# /usr/lib/podman/conmon --api-version 1 ...

Giai đoạn 4 — Podman Networking Internals

4.1 Netavark — Network driver mặc định

Từ Podman 4.0, netavark thay thế CNI plugins làm network driver mặc định. Netavark viết bằng Rust, tạo và quản lý bridge network, veth pairs, và iptables/nftables rules.

Khi tạo container network:

Podman
  │ gọi netavark

Netavark
  ├── Tạo bridge interface (podman1, podman2, ...)
  ├── Tạo veth pair (một đầu trong container namespace, một đầu trên host)
  ├── Gán IP cho container
  └── Ghi iptables/nftables rules cho forwarding và NAT
# Xem bridge Podman đã tạo
ip link show type bridge
# podman1: <BROADCAST,MULTICAST,UP,LOWER_UP>

# Xem veth pairs
ip link show type veth

# Xem config netavark lưu
ls /run/containers/networks/

4.2 Aardvark-dns — DNS nội bộ

Aardvark-dns là DNS server nhỏ chạy trên host, lắng nghe trên gateway address của mỗi Podman network. Nó đọc danh sách container và hostname từ file config, trả về IP tương ứng khi container query.

# Config file aardvark-dns đọc
cat /run/containers/networks/aardvark-dns/<network_name>
# gateway_ip
# container_id  container_ip  name1,name2,...

# Confirm aardvark-dns lắng nghe
ss -ulnp | grep aardvark
# UNCONN 0 0  10.88.0.1:53  0.0.0.0:*

# Query trực tiếp
dig @10.88.0.1 postgres

Quan trọng: aardvark-dns lắng nghe trên host network stack. Nếu iptables INPUT policy là DROP và không có rule ACCEPT cho port 53 từ Podman bridge, DNS query từ container bị drop tại iptables trước khi tới aardvark-dns.

4.3 Rootless networking — Pasta và slirp4netns

Rootless container không thể tạo bridge interface hay thao tác iptables. Thay vào đó dùng userspace network stack:

slirp4netns (cũ hơn): emulate network stack trong userspace, tạo TAP interface trong container namespace. Packet đi từ container → TAP → slirp4netns userspace → socket trên host → internet.

pasta (mới hơn, Podman 4.4+): dùng cùng network namespace với host nhưng có isolated routing. Hiệu năng tốt hơn slirp4netns vì ít copy data hơn.

# Xem rootless dùng driver nào
podman info | grep networkBackend
podman info | grep pasta

Giai đoạn 5 — Rootful vs Rootless Deep Dive

5.1 User Namespace và UID Mapping

Rootless hoạt động được nhờ user namespace. Bên trong user namespace, process có UID 0 (root). Bên ngoài (trên host), process map sang UID cao:

# Xem UID mapping của user hiện tại
cat /etc/subuid
# haonam:100000:65536
# Nghĩa là: user haonam có thể map UID 0-65535 trong namespace
#           sang UID 100000-165535 trên host

cat /etc/subgid
# Tương tự cho GID
# Xem mapping thực tế của container đang chạy
podman unshare cat /proc/self/uid_map
# 0  100000  65536
# UID 0 trong container = UID 100000 trên host

5.2 newuidmap và newgidmap

Đây là hai SUID binary cho phép user thường thiết lập UID mapping mà không cần root:

ls -la /usr/bin/newuidmap
# -rwsr-xr-x root root ... /usr/bin/newuidmap
# s = SUID bit

Podman gọi newuidmap khi setup rootless container. Nếu /etc/subuid không có entry cho user, rootless không hoạt động được.

5.3 So sánh storage path

Rootful:
  Images:    /var/lib/containers/storage/
  Volumes:   /var/lib/containers/storage/volumes/
  Networks:  /run/containers/networks/
  Config:    /etc/containers/

Rootless:
  Images:    ~/.local/share/containers/storage/
  Volumes:   ~/.local/share/containers/storage/volumes/
  Networks:  /run/user/<UID>/containers/networks/
  Config:    ~/.config/containers/

Giai đoạn 6 — Networking Thực Tế Trên VPS

Đây là phần nhiều người bỏ qua và sau đó gặp lỗi khó debug.

6.1 Toàn bộ luồng packet từ container ra internet

App trong container (10.88.0.5)
  │ src: 10.88.0.5:random  dst: 1.1.1.1:443

veth trong container namespace


veth trên host (pair của veth container)


podman1 bridge (10.88.0.1)


iptables FORWARD chain
  ├── Cần rule ACCEPT (UFW: DEFAULT_FORWARD_POLICY=ACCEPT)


iptables POSTROUTING (nat table)
  ├── Cần MASQUERADE rule: -s 10.88.0.0/16 -o eth0 -j MASQUERADE
  ├── src được rewrite: 10.88.0.5 → public IP VPS


eth0 (public IP)
  │ src: public_ip:random  dst: 1.1.1.1:443

Internet

6.2 Tại sao Tailscale không bị ảnh hưởng bởi UFW FORWARD DROP

Tailscale tự thêm ts-forward rule vào FORWARD chain khi khởi động, trước khi UFW rules được evaluate. Rule này ACCEPT traffic của Tailscale. Podman không làm điều tương tự — nó dựa vào FORWARD policy hoặc rule có sẵn.

sudo iptables -L FORWARD -n -v
# ts-forward  — Tailscale tự thêm, luôn ACCEPT
# ufw-before-forward  — UFW kiểm soát, mặc định không có rule cho Podman subnet

6.3 DNS query từ container đi đâu

Container query DNS (10.88.0.1:53)


veth → podman1 bridge → host network stack


iptables INPUT chain
  ├── Nếu policy DROP và không có rule ACCEPT port 53 → DROP

  ▼ (nếu pass)
aardvark-dns lắng nghe 10.88.0.1:53


Response về container

Giai đoạn 7 — Systemd Integration

7.1 Quadlet — Cách đúng để chạy container với systemd

Quadlet (tích hợp từ Podman 4.4) cho phép định nghĩa container như systemd unit file, thay vì dùng podman generate systemd (deprecated).

# /etc/containers/systemd/myapp.container  (rootful)
# ~/.config/containers/systemd/myapp.container  (rootless)

[Unit]
Description=My Application
After=network-online.target

[Container]
Image=myapp:latest
PublishPort=8080:8080
Environment=APP_ENV=production
Volume=/data/myapp:/data

[Service]
Restart=always

[Install]
WantedBy=multi-user.target
# Reload systemd để pick up file mới
systemctl daemon-reload

# Start
systemctl start myapp

# Enable auto-start
systemctl enable myapp

7.2 Socket Activation

Container chỉ khởi động khi có connection đến, không chạy idle. Hữu ích cho service ít dùng trên VPS ít RAM.

# myapp.socket
[Socket]
ListenStream=8080

[Install]
WantedBy=sockets.target

# myapp.container
[Unit]
After=myapp.socket
Requires=myapp.socket

[Container]
...

7.3 Linger cho rootless

Mặc định, rootless systemd user service chỉ chạy khi có user session. Khi SSH disconnect, service bị kill.

# Enable linger — service chạy ngay cả khi không có session
loginctl enable-linger $USER

# Xác nhận
loginctl show-user $USER | grep Linger
# Linger=yes

Giai đoạn 8 — Security Layer

8.1 Seccomp — Giới hạn syscall

Container có thể gọi bất kỳ syscall nào theo mặc định. Seccomp profile giới hạn danh sách syscall được phép — giảm attack surface nếu container bị compromise.

Podman dùng seccomp profile mặc định block ~44 syscall nguy hiểm. Profile lưu ở /usr/share/containers/seccomp.json.

# Xem syscall nào bị block
cat /usr/share/containers/seccomp.json | jq '.syscalls[] | select(.action=="SCMP_ACT_ERRNO") | .names'

# Chạy không có seccomp (không khuyến nghị production)
podman run --security-opt seccomp=unconfined myapp

# Dùng profile tùy chỉnh
podman run --security-opt seccomp=/path/to/profile.json myapp

8.2 Linux Capabilities trong container

Podman drop hầu hết capabilities theo mặc định, chỉ giữ:

CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FSETID, CAP_FOWNER,
CAP_MKNOD, CAP_NET_RAW, CAP_SETGID, CAP_SETUID,
CAP_SETFCAP, CAP_SETPCAP, CAP_NET_BIND_SERVICE,
CAP_SYS_CHROOT, CAP_KILL, CAP_AUDIT_WRITE
# Xem capabilities của process trong container
podman exec mycontainer capsh --print

# Drop tất cả capabilities (tối thiểu nhất)
podman run --cap-drop=all --cap-add=NET_BIND_SERVICE nginx

# Kiểm tra container có cần privilege gì không
podman run --security-opt no-new-privileges myapp

8.3 AppArmor trên Debian

Debian dùng AppArmor. Podman tự động áp dụng profile container-default cho tất cả container nếu AppArmor đang chạy:

# Kiểm tra AppArmor status
aa-status

# Xem profile Podman dùng
cat /etc/apparmor.d/containers/container-default

# Chạy không có AppArmor profile (debug)
podman run --security-opt apparmor=unconfined myapp

8.4 Read-only rootfs

Best practice cho production — container không thể ghi vào rootfs của mình:

podman run --read-only --tmpfs /tmp --tmpfs /run myapp
# Trong compose
services:
  app:
    read_only: true
    tmpfs:
      - /tmp
      - /run

Tóm tắt lộ trình học

Giai đoạnChủ đềCông cụ thực hành
1Namespaces, cgroups, capabilitiesunshare, nsenter, capsh
2OverlayFS, CoW, layerspodman diff, podman history, ls /var/lib/containers
3OCI spec, conmon, no-daemonpodman inspect, cat config.json
4Netavark, aardvark-dns, pastaip link, ss, iptables, dig
5User namespace, UID mappingcat /etc/subuid, podman unshare
6VPS networking, FORWARD, MASQUERADEiptables -L, ufw status
7Quadlet, socket activation, lingersystemctl, loginctl
8Seccomp, capabilities, AppArmorcapsh, aa-status, seccomp.json

Nguồn học thêm

  • man namespaces(7) — tài liệu kernel về namespace
  • man capabilities(7) — danh sách đầy đủ capabilities
  • man overlayfs hoặc Documentation/filesystems/overlayfs.rst trong kernel source
  • OCI Image Spec: github.com/opencontainers/image-spec
  • OCI Runtime Spec: github.com/opencontainers/runtime-spec
  • Netavark source: github.com/containers/netavark
  • Aardvark-dns source: github.com/containers/aardvark-dns

Môi trường tham chiếu: Debian 13, Podman 5.x rootful, kernel 6.x.

Nhắn qua Telegram