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.
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:
| Namespace | Cô lập | Kernel version |
|---|---|---|
mnt | Mount points, filesystem | 2.4.19 |
pid | Process IDs | 2.6.24 |
net | Network interfaces, routing, ports | 2.6.24 |
ipc | IPC, message queues, shared memory | 2.6.19 |
uts | Hostname, domain name | 2.6.19 |
user | UIDs, GIDs | 3.8 |
cgroup | Cgroup root directory | 4.6 |
time | Clock offsets | 5.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ạn | Chủ đề | Công cụ thực hành |
|---|---|---|
| 1 | Namespaces, cgroups, capabilities | unshare, nsenter, capsh |
| 2 | OverlayFS, CoW, layers | podman diff, podman history, ls /var/lib/containers |
| 3 | OCI spec, conmon, no-daemon | podman inspect, cat config.json |
| 4 | Netavark, aardvark-dns, pasta | ip link, ss, iptables, dig |
| 5 | User namespace, UID mapping | cat /etc/subuid, podman unshare |
| 6 | VPS networking, FORWARD, MASQUERADE | iptables -L, ufw status |
| 7 | Quadlet, socket activation, linger | systemctl, loginctl |
| 8 | Seccomp, capabilities, AppArmor | capsh, aa-status, seccomp.json |
Nguồn học thêm
man namespaces(7)— tài liệu kernel về namespaceman capabilities(7)— danh sách đầy đủ capabilitiesman overlayfshoặcDocumentation/filesystems/overlayfs.rsttrong 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.