Nếu bạn từng nghĩ container là “máy ảo nhưng nhẹ hơn”, thử chạy htop trong một container: bạn sẽ thấy toàn bộ CPU của host, không chỉ riêng CPU cấp cho container. PID 1 trong container thực ra là PID 28471 ngoài host. ip addr trong container có thể chỉ thấy interface loopback. Container không phải VM — nó là process Linux được bọc trong isolation layer.

Hai kernel primitive tạo nên isolation đó: namespaces (cô lập tầm nhìn — process trong container chỉ thấy những gì thuộc về nó) và cgroups (giới hạn tài nguyên — process không thể dùng nhiều hơn mức được cấp). Bài này giải thích cả hai, cách Docker dùng chúng, và những hệ quả thực tế khi vận hành container.


Container là process, không phải VM

Chạy thử:

# Trong container
$ sleep 1000 &
$ echo $$
1

# Ngoài host
$ ps aux | grep sleep
root     28471  0.0  0.0   2384   756 ?  Ss   09:15   0:00 sleep 1000

Process sleep 1000 chạy trong container có PID 1 trong không gian tên PID (PID namespace). Nhưng trên host, nó là process 28471 — process bình thường, được Linux kernel schedule như mọi process khác. Không có hypervisor, không có kernel riêng, không có hardware emulation. Container dùng chung kernel với host.

Sự khác biệt nằm ở namespace: kernel “đánh lừa” process rằng nó có PID 1, có network interface riêng, có filesystem root riêng. Nhưng tất cả đều là ảo — kernel chỉ filter những gì process được phép thấy.


  flowchart TB
    subgraph Host["Host Linux Kernel"]
        subgraph NS1["PID Namespace 1"]
            C1["Container A<br/>PID 1 = Host PID 28471"]
            C2["Container A's child<br/>PID 2 = Host PID 28472"]
        end
        subgraph NS2["PID Namespace 2"]
            C3["Container B<br/>PID 1 = Host PID 30112"]
        end
        ProcHost["Host processes<br/>sshd, cron, dockerd..."]
    end

Namespace types: Docker dùng những gì?

Linux kernel hiện tại hỗ trợ 8 loại namespace. Docker dùng 7 trong số đó (trừ time namespace) để tạo isolation cho container:

NamespaceFlagDocker dùngCô lập cái gì
PIDCLONE_NEWPIDProcess ID — container thấy PID 1 là process của nó
NetworkCLONE_NEWNETNetwork interfaces, routing table, firewall rules
MountCLONE_NEWNSFilesystem mount points — rootfs của container
UTSCLONE_NEWUTSHostname và domain name
IPCCLONE_NEWIPCSystem V IPC, POSIX message queues
UserCLONE_NEWUSERCó (khi dùng user namespace)UID/GID mapping
CgroupCLONE_NEWCGROUPCgroup hierarchy view
TimeCLONE_NEWTIMEKhôngClock — mới, ít dùng

PID namespace

Process trong container thấy PID 1 là chính nó. PID 1 có trách nhiệm đặc biệt trong Linux: nhận SIGCHLD từ orphan process, và nếu nó chết, kernel panic — nhưng chỉ trong namespace đó, không ảnh hưởng host. Đây là lý do PID 1 problem: ứng dụng viết không đúng có thể không xử lý zombie process, dẫn đến resource leak. Sẽ bàn kỹ ở Phần 5.

# Xem PID namespace của container
$ docker inspect --format '{{.State.Pid}}' my-container
28471
$ ls -la /proc/28471/ns/pid
lrwxrwxrwx 1 root root 0 Jun 18 09:15 /proc/28471/ns/pid -> pid:[4026532154]

Network namespace

Mỗi container mặc định có network namespace riêng: interface riêng, IP riêng, routing table riêng — nhưng tất cả đều là virtual interface được kernel bridge lại. Đây là cách container cùng host giao tiếp được với nhau qua docker0 bridge mà không cần expose port.

Sẽ phân tích sâu ở Phần 11, nhưng ý chính: mỗi container thấy eth0@ifXX là một đầu của veth pair — cặp interface ảo nối container với bridge ngoài host.

Mount namespace

Mount namespace cho phép mỗi container có root filesystem riêng — chính là image layers được mount làm rootfs. Khi bạn docker run alpine, kernel mount các layer của Alpine image thành / cho container đó, trong khi process ngoài host thấy / là host rootfs.

Đây cũng là cách volume mount hoạt động: bind mount một thư mục từ host vào mount namespace của container.

User namespace

Mặc định Docker không bật user namespace. Container chạy với UID 0 (root) trong container map thẳng sang UID 0 trên host — một trong những rủi ro bảo mật lớn nhất. Khi bật user namespace (--userns-remap), UID 0 trong container được map sang UID không privileged trên host (thường là dải 100000+), giảm thiệt hại nếu container escape.

Root trong container mặc định là root trên host. Nếu không bật user namespace, mọi lệnh chạy với USER root trong Dockerfile đều có UID 0 trên host kernel. Container escape exploit + root trong container = root trên host.

Cgroup v2: giới hạn tài nguyên

Nếu namespace trả lời “process thấy gì”, cgroup trả lời “process được dùng bao nhiêu”. Cgroup (control group) giới hạn CPU, memory, I/O, và PID count cho một nhóm process.

Docker dùng cgroup v2 kể từ Engine v22 (hỗ trợ đầy đủ từ v25). Cgroup v2 có một hierarchy duy nhất (/sys/fs/cgroup/) thay vì nhiều subsystem riêng biệt như v1.

Khi bạn chạy docker run --memory 512m --cpus 2 nginx, Docker tạo một cgroup cho container đó:

# Xem cgroup của container
$ docker inspect --format '{{.HostConfig.CgroupParent}}' my-container
/docker/abc123...

# Trên host
$ cat /sys/fs/cgroup/system.slice/docker-abc123.scope/memory.max
536870912   # 512 MB
$ cat /sys/fs/cgroup/system.slice/docker-abc123.scope/cpu.max
200000 100000   # 2 CPUs

Ba giới hạn quan trọng nhất:

docker run \
  --memory 512m \          # Hard limit: OOM kill nếu vượt quá
  --memory-swap 1g \       # Swap limit (bao gồm memory)
  --cpus 2 \               # CPU quota (2 cores)
  --pids-limit 100 \       # Số process tối đa (chống fork bomb)
  nginx
Luôn set memory limit cho container production. Không có limit, một container memory leak có thể tiêu thụ toàn bộ RAM host, trigger kernel OOM killer và kill container khác — kể cả database. Đây là incident #1 mình thấy ở team mới dùng Docker.

CoW filesystem: cách image layer hoạt động

Container không copy toàn bộ filesystem — nó dùng copy-on-write (CoW). Image gồm nhiều layer read-only xếp chồng, container có một lớp read-write mỏng trên cùng. Khi container ghi file, kernel copy file từ layer read-only lên layer read-write rồi ghi lên bản copy đó.

Docker hỗ trợ nhiều storage driver: overlay2 (mặc định, khuyên dùng), btrfs, zfs, vfs (test only). overlay2 dùng kernel module overlayfs, hiệu năng tốt và ổn định nhất hiện nay.

# Kiểm tra storage driver
$ docker info --format '{{.Driver}}'
overlay2

Hệ quả của CoW: container càng ghi nhiều, layer read-write càng phình. Khi xóa container, layer read-write mất — đây là lý do mọi dữ liệu trong container mất khi container bị xóa. Volume là cách duy nhất để giữ dữ liệu (Phần 6).


Tổng kết

Container không phải VM — nó là process Linux được bọc trong namespace isolation + cgroup limits:

  1. Namespace cô lập tầm nhìn của process (PID, network, mount, user…) — nhưng tất cả process vẫn chạy trên cùng một kernel
  2. Cgroup giới hạn tài nguyên process được dùng (CPU, memory, I/O, PID) — không có limit là công thức cho OOM disaster
  3. CoW filesystem cho phép nhiều container share chung base image layer, chỉ tạo layer mới khi có thay đổi — lý do image pull nhanh và container start nhanh

Hiểu ba thứ này giúp bạn debug những vấn đề “kỳ lạ” — kiểu container thấy 16 core nhưng chỉ được dùng 2, hay file ghi trong container mất sau khi restart.


Câu hỏi hay gặp

Container có kernel riêng không?

Không. Mọi container trên cùng một host dùng chung Linux kernel. Docker image chỉ chứa userspace (binary, library, config), không chứa kernel. Đây là lý do bạn không thể chạy Windows container trên Linux host — kernel khác nhau. Trên Windows, Docker dùng Hyper-V isolation cho Windows container, hoặc WSL2 cho Linux container.

Làm sao xem process trong container từ host?

# Tìm PID trên host
$ docker inspect --format '{{.State.Pid}}' container-name
28471

# Xem tất cả process trong container (kể cả child)
$ ps --ppid 28471
# Hoặc dùng cgroup
$ cat /sys/fs/cgroup/system.slice/docker-<id>.scope/cgroup.procs

Tại sao không nên chạy nhiều process trong một container?

Một container = một PID namespace. Process chính (PID 1) chịu trách nhiệm nhận SIGTERM và reap zombie. Nếu bạn chạy nhiều process không liên quan, một process chết không được cleanup, và khi Docker gửi SIGTERM để stop container, signal chỉ đến PID 1 — các process con có thể bị SIGKILL đột ngột sau 10 giây. Mỗi container nên chạy một process chính. Nếu cần nhiều process, dùng init system như tini hoặc dumb-init (Phần 5).

Docker dùng cgroup v1 hay v2?

Từ Docker Engine v22 trở đi, nếu host kernel ≥ 5.2, Docker tự động dùng cgroup v2. Kiểm tra:

$ docker info --format '{{.CgroupVersion}}'
2

Nếu host vẫn dùng cgroup v1 (kernel cũ hoặc distro chưa migrate), Docker fallback về v1. Cgroup v2 có lợi thế về quản lý memory chính xác hơn và hỗ trợ rootless container tốt hơn.


Bài tiếp theo (Core Docker): Phần 3: Image, layer & pull/inspect, tìm hiểu image thực sự là gì, layer model, tagging, manifest, và cách inspect image để biết bên trong có gì.