

Ở Bài 14, bạn đã học cách quản lý Docker log hiệu quả, từ xem log, giới hạn log size, chọn log driver, đến setup Dozzle để xem log qua giao diện web. Giờ bạn đã biết cách theo dõi mọi thứ đang xảy ra bên trong container.


Nhưng theo dõi thôi chưa đủ. Nếu Docker trên VPS của bạn bị cấu hình sai, kẻ tấn công có thể chiếm quyền root cả máy chủ chỉ từ một container. Và đây không phải lý thuyết, đó là thực tế đã xảy ra rất nhiều lần.
Bài này sẽ hướng dẫn bạn bảo mật Docker trên VPS: từ việc không chạy container với root, giới hạn resource, bảo vệ network, đến quản lý secrets và scan image. Cuối bài có một checklist bảo mật để bạn kiểm tra lại toàn bộ hệ thống của mình.
Docker giúp deploy nhanh, gọn, nhưng nếu không cấu hình cẩn thận thì nó cũng mở ra kha khá lỗ hổng. Docker daemon mặc định chạy với quyền root, container có thể bị escape, và image từ registry công khai không phải lúc nào cũng sạch. Bài này mình sẽ đi qua từng lớp bảo mật cho Docker trên VPS Linux, từ daemon, image cho đến runtime.


- Tại sao Docker cần bảo mật riêng?
- Bảo mật Docker daemon
- Rootless Docker
- Bảo mật image
- Runtime security
- Network isolation
- Docker Compose security patterns
- Quản lý secrets
- Cấu hình logging
- Docker Bench Security
- Checkpoint: Kiểm tra lại
- Tóm lại
- Tại sao bảo mật Docker quan trọng?
- Không chạy container với root
- Giới hạn resource cho container
- Network security
- Docker socket security
- Image security
- Secrets management
- Read-only filesystem
- Checklist bảo mật Docker
- Tổng kết
Tại sao Docker cần bảo mật riêng?
Nhiều bạn nghĩ container đã cách ly rồi thì an toàn. Thực tế thì không hẳn vậy. Có vài điểm cần hiểu rõ:
- Docker daemon chạy root: Bất kỳ ai có quyền gọi Docker socket đều gián tiếp có quyền root trên host. Một container mount
/của host là game over. - Container escape: Các lỗ hổng kernel hoặc cấu hình sai (privileged mode, mount sensitive path) có thể cho phép tiến trình trong container thoát ra host.
- Chuỗi cung ứng attacks: Pull một image từ Docker Hub mà không verify, bạn có thể đang chạy code của người lạ trên server mình.
Hiểu được những rủi ro này rồi, mình sẽ đi vào từng phần cụ thể để khắc phục.


Bảo mật Docker daemon
Docker daemon lắng nghe qua Unix socket /var/run/docker.sock. Mặc định chỉ local access, nhưng nếu bạn bật TCP socket để remote thì cần cẩn thận.
Bind socket chỉ localhost
Nếu cần expose Docker API qua TCP, chỉ bind vào localhost:
# /etc/docker/daemon.json
{
"hosts": ["unix:///var/run/docker.sock", "tcp://127.0.0.1:2376"]
}
Không bao giờ bind Docker daemon vào 0.0.0.0 mà không có TLS. Ai quét được port đó là có full quyền root trên server của bạn.
TLS cho remote access
Nếu bắt buộc phải truy cập Docker daemon từ xa (ví dụ CI/CD server khác), hãy bật TLS mutual authentication:
# /etc/docker/daemon.json
{
"tls": true,
"tlsverify": true,
"tlscacert": "/etc/docker/tls/ca.pem",
"tlscert": "/etc/docker/tls/server-cert.pem",
"tlskey": "/etc/docker/tls/server-key.pem",
"hosts": ["unix:///var/run/docker.sock", "tcp://0.0.0.0:2376"]
}
Sau khi chỉnh xong, restart Docker daemon:
sudo systemctl restart docker
Rootless Docker
Cách triệt để nhất để giảm rủi ro từ Docker daemon là chạy nó không cần root. Rootless Docker chạy toàn bộ daemon và container dưới user thường, nên dù container bị escape thì attacker cũng chỉ có quyền của user đó.
Cài đặt trên Ubuntu
# Cài các package cần thiết
sudo apt-get install -y uidmap dbus-user-session
# Chạy setup tool (với user thường, KHÔNG dùng sudo)
dockerd-rootless-setuptool.sh install
Sau khi cài xong, thêm các biến môi trường vào shell profile:
export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/docker.sock
Cài đặt trên AlmaLinux/Rocky Linux
# Cài package cần thiết
sudo dnf install -y fuse-overlayfs slirp4netns
# Chạy setup (với user thường)
dockerd-rootless-setuptool.sh install
Kiểm tra rootless Docker đang chạy:
docker info | grep -i "root"
# Kết quả mong đợi: rootless: true
Rootless Docker có một số hạn chế: không bind được port dưới 1024 (trừ khi cấu hình thêm), và một số volume mount có thể cần adjust permission. Với hầu hết ứng dụng web thông thường thì không ảnh hưởng gì.
Bảo mật image
Image là nền tảng của mọi container. Nếu image có vấn đề thì mọi thứ phía sau đều có vấn đề theo.
Chỉ dùng official images
Trên Docker Hub, các official images (có nhãn “Docker Official Image”) được Docker team review và cập nhật thường xuyên. Ưu tiên dùng các image này thay vì image từ nguồn không rõ.
# Tốt — official image
docker pull nginx
docker pull postgres
# Tránh — image từ nguồn không xác minh
docker pull randomuser123/nginx-custom
Pin version cụ thể
Tag latest thay đổi liên tục. Hôm nay build ngon, mai rebuild có thể lỗi vì base image đã thay đổi. Luôn pin version cụ thể:
# Không nên
FROM nginx:latest
# Nên
FROM nginx:1.25-alpine
Dùng Alpine variant khi có thể, vì image nhỏ hơn đồng nghĩa ít package hơn, ít lỗ hổng tiềm ẩn hơn.
Scan image tìm lỗ hổng
Trước khi đưa image lên production, hãy scan nó. Có hai công cụ phổ biến:
Docker Scout (tích hợp sẵn với Docker Desktop và CLI mới):
docker scout cve nginx:1.25
docker scout recommendations nginx:1.25
Trivy (open source, chạy được trên mọi môi trường):
# Cài Trivy trên Ubuntu
sudo apt-get install -y wget apt-transport-https gnupg
wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | sudo apt-key add -
echo "deb https://aquasecurity.github.io/trivy-repo/deb generic main" | sudo tee /etc/apt/sources.list.d/trivy.list
sudo apt-get update && sudo apt-get install -y trivy
# Cài Trivy trên AlmaLinux/Rocky Linux
sudo rpm -ivh https://github.com/aquasecurity/trivy/releases/latest/download/trivy_*_Linux-64bit.rpm
# Scan image
trivy image nginx:1.25
trivy image --severity HIGH,CRITICAL nginx:1.25
Nên tích hợp bước scan vào CI/CD pipeline. Image nào có lỗ hổng CRITICAL thì không cho deploy.
Multi-stage build giảm bề mặt tấn công
Multi-stage build cho phép bạn tách giai đoạn build (cần compiler, tools) khỏi giai đoạn chạy (chỉ cần runtime). Image cuối cùng sẽ nhỏ hơn và ít thành phần thừa hơn.
# Stage 1: Build
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# Stage 2: Production — chỉ copy artifacts cần thiết
FROM node:20-alpine
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/node_modules ./node_modules
USER appuser
EXPOSE 3000
CMD ["node", "dist/server.js"]
Lưu ý dòng USER appuser ở cuối. Luôn chạy ứng dụng trong container với user không phải root.
Runtime security
Kể cả khi image sạch, cách bạn chạy container cũng quan trọng không kém. Dưới đây là những flag bạn nên dùng.
Read-only filesystem
Ngăn container ghi vào filesystem. Nếu ứng dụng cần ghi thì mount riêng thư mục cần ghi:
docker run -d \
--read-only \
--tmpfs /tmp \
--tmpfs /var/cache/nginx \
nginx:1.25-alpine
Chặn privilege escalation
Flag --no-new-privileges ngăn các tiến trình trong container leo thang quyền qua setuid/setgid:
docker run -d \
--security-opt no-new-privileges:true \
nginx:1.25-alpine
Drop Linux capabilities
Mặc định Docker cấp cho container một số Linux capabilities. Cách an toàn nhất là drop hết rồi chỉ thêm lại những gì cần:
# Drop tất cả, chỉ thêm lại NET_BIND_SERVICE (bind port < 1024)
docker run -d \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
nginx:1.25-alpine
Một số capabilities phổ biến cần thêm lại tùy ứng dụng:
NET_BIND_SERVICE: Bind port dưới 1024CHOWN: Đổi owner file (một số app cần khi khởi động)SETUID,SETGID: Chuyển user (nếu entrypoint cần chạy init rồi switch user)
Giới hạn tài nguyên
Không giới hạn resource thì một container bị lỗi có thể ăn hết RAM/CPU của cả host:
docker run -d \
--memory 256m \
--memory-swap 256m \
--cpus 0.5 \
--pids-limit 100 \
nginx:1.25-alpine
--memory 256m: Giới hạn RAM 256MB--memory-swap 256m: Bằng memory, nghĩa là không dùng swap--cpus 0.5: Tối đa 50% một CPU core--pids-limit 100: Tối đa 100 process, chống fork bomb
Kết hợp tất cả
Một lệnh docker run với đầy đủ security options trông như thế này:
docker run -d \
--name secure-nginx \
--read-only \
--tmpfs /tmp \
--tmpfs /var/cache/nginx \
--cap-drop ALL \
--cap-add NET_BIND_SERVICE \
--security-opt no-new-privileges:true \
--memory 256m \
--cpus 0.5 \
--pids-limit 100 \
-p 80:80 \
nginx:1.25-alpine
Network isolation
Mặc định tất cả container trên cùng bridge network có thể giao tiếp với nhau. Trong thực tế, không phải container nào cũng cần nói chuyện với nhau.
Tạo internal network cho backend
# Tạo network internal (không ra internet) docker network create --internal backend-net # Container database chỉ nằm trong internal network docker run -d --name db --network backend-net postgres:16-alpine# Container app kết nối cả backend-net và frontend docker network create frontend-net docker run -d --name app \ --network backend-net \ nginx:1.25-alpine docker network connect frontend-net app
Với cấu hình này, container db không thể truy cập internet, và từ bên ngoài cũng không truy cập được trực tiếp vào database.
Chỉ expose port cần thiết
# Tốt — chỉ expose port 80 ra localhost docker run -d -p 127.0.0.1:80:80 nginx:1.25-alpine # Tránh — expose ra tất cả interface docker run -d -p 80:80 nginx:1.25-alpine# Tránh — dùng host network docker run -d --net=host nginx:1.25-alpine
Nếu bạn dùng reverse proxy (Nginx, Caddy, Traefik) trước container, thì container backend không cần expose port ra ngoài. Để reverse proxy và backend cùng một Docker network, giao tiếp internal là đủ.
Docker Compose security patterns
Hầu hết mọi người dùng Docker Compose để quản lý multi-container. Dưới đây là cách áp dụng các security options trong Compose file:
version: "3.8" services: web: image: nginx:1.25-alpine read_only: true tmpfs: - /tmp - /var/cache/nginx security_opt: - no-new-privileges:true cap_drop: - ALL cap_add: - NET_BIND_SERVICE deploy: resources: limits: memory: 256M cpus: "0.5" ports: - "127.0.0.1:80:80" networks: - frontend app: image: myapp:1.0 user: "1000:1000" read_only: true tmpfs: - /tmp security_opt: - no-new-privileges:true cap_drop: - ALL deploy: resources: limits: memory: 512M cpus: "1.0" networks: - frontend - backend depends_on: - db db: image: postgres:16-alpine user: "999:999" security_opt: - no-new-privileges:true cap_drop: - ALL deploy: resources: limits: memory: 1G cpus: "1.0" volumes: - db-data:/var/lib/postgresql/data networks: - backend environment: POSTGRES_PASSWORD_FILE: /run/secrets/db_password secrets: - db_password networks: frontend: backend: internal: true volumes: db-data:secrets: db_password: file: ./secrets/db_password.txt
Một vài điểm đáng chú ý:
user: "1000:1000": Chạy container với UID/GID cụ thể, không phải rootbackend: internal: true: Network backend không có đường ra internetsecrets: Dùng Docker secrets thay vì đặt password trực tiếp trong environment
Quản lý secrets
Đừng bao giờ đặt password, API key trực tiếp trong Dockerfile hay docker-compose.yml. Có mấy cách an toàn hơn:
Docker secrets (Compose)
Như ví dụ phía trên, Docker secrets mount file vào /run/secrets/ trong container. Ứng dụng đọc password từ file thay vì environment variable:
# Tạo file secret
echo "my-strong-password" > ./secrets/db_password.txt
chmod 600 ./secrets/db_password.txt
Bảo vệ .env file
Nếu vẫn dùng .env file, hãy đảm bảo permission chặt chẽ:
# Chỉ owner đọc được chmod 600 .env # Thêm vào .gitignore echo ".env" >> .gitignore# Kiểm tra không có secret nào trong git history git log --all --full-history -- .env
Nếu bạn lỡ commit secret vào git, chỉ xóa file rồi commit lại là không đủ. Secret vẫn nằm trong git history. Bạn cần dùng git filter-repo hoặc BFG Repo-Cleaner để xóa hoàn toàn, rồi rotate tất cả credential đã bị lộ.
Cấu hình logging
Container log mặc định không giới hạn size. Chạy lâu ngày, log có thể ăn hết ổ cứng. Cấu hình log rotation ngay từ đầu:
Cho từng container
docker run -d \
--log-driver json-file \
--log-opt max-size=10m \
--log-opt max-file=3 \
nginx:1.25-alpine
Cấu hình mặc định cho toàn bộ daemon
Thêm vào /etc/docker/daemon.json để áp dụng cho tất cả container mới:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}
sudo systemctl restart docker
Với cấu hình trên, mỗi container tối đa giữ 3 file log, mỗi file 10MB. Tổng cộng 30MB log cho mỗi container, đủ để debug mà không lo đầy ổ.
Docker Bench Security
Docker Bench Security là tool tự động kiểm tra cấu hình Docker theo CIS Benchmark. Chạy nó để xem server bạn đang thiếu gì:
docker run --rm --net host --pid host \
--userns host --cap-add audit_control \
-e DOCKER_CONTENT_TRUST=$DOCKER_CONTENT_TRUST \
-v /var/lib:/var/lib:ro \
-v /var/run/docker.sock:/var/run/docker.sock:ro \
-v /usr/lib/systemd:/usr/lib/systemd:ro \
-v /etc:/etc:ro \
docker/docker-bench-security
Output sẽ liệt kê từng mục kiểm tra với kết quả PASS, WARN, hoặc INFO. Tập trung fix những mục WARN trước.
Ví dụ output dạng:
[PASS] 1.1 - Ensure a separate partition for containers has been created
[WARN] 1.2 - Ensure only trusted users are allowed to control Docker daemon
[PASS] 2.1 - Run the Docker daemon as a non-root user
[WARN] 4.1 - Ensure that a user for the container has been created
[PASS] 5.1 - Ensure that, if applicable, an AppArmor Profile is enabled
Nên chạy Docker Bench định kỳ (ví dụ mỗi tháng hoặc sau mỗi lần thay đổi cấu hình Docker) để đảm bảo không có gì bị bỏ sót.
Checkpoint: Kiểm tra lại
Sau khi áp dụng các bước trên, hãy kiểm tra lại bằng checklist này:
1. Scan image
# Scan image đang dùng trivy image nginx:1.25-alpine trivy image postgres:16-alpine# Kiểm tra không có lỗ hổng CRITICAL trivy image --severity CRITICAL --exit-code 1 nginx:1.25-alpine
2. Chạy container với security options
# Chạy container test với đầy đủ security flags docker run -d \ --name security-test \ --read-only \ --tmpfs /tmp \ --cap-drop ALL \ --cap-add NET_BIND_SERVICE \ --security-opt no-new-privileges:true \ --memory 128m \ --cpus 0.25 \ --pids-limit 50 \ nginx:1.25-alpine # Verify container đang chạy docker ps | grep security-test # Kiểm tra security settings docker inspect security-test | jq '.[0].HostConfig.CapDrop' docker inspect security-test | jq '.[0].HostConfig.ReadonlyRootfs' docker inspect security-test | jq '.[0].HostConfig.SecurityOpt'# Cleanup docker rm -f security-test
3. Chạy Docker Bench Security
# Chạy audit docker run --rm --net host --pid host \ --userns host --cap-add audit_control \ -v /var/lib:/var/lib:ro \ -v /var/run/docker.sock:/var/run/docker.sock:ro \ -v /usr/lib/systemd:/usr/lib/systemd:ro \ -v /etc:/etc:ro \ docker/docker-bench-security# Đọc kết quả, focus vào các mục [WARN] # Fix từng mục theo recommendation
Nếu Docker Bench không báo WARN nào liên quan đến các mục mình đã cấu hình, nghĩa là bạn đã cover được phần lớn. Các mục còn lại tùy vào môi trường cụ thể mà bạn quyết định có cần fix hay không.
Tóm lại
Bảo mật Docker không phải chỉ một bước mà xong. Nó là nhiều lớp chồng lên nhau:
- Daemon: Không expose socket, dùng TLS nếu remote, cân nhắc rootless mode
- Image: Dùng official image, pin version, scan trước khi deploy, multi-stage build
- Runtime: Read-only filesystem, drop capabilities, giới hạn resource, chặn privilege escalation
- Network: Tách network, chỉ expose port cần thiết, tránh host network
- Secrets: Không hardcode, dùng Docker secrets hoặc bảo vệ .env file
- Logging: Cấu hình rotation, không để log vô hạn
- Audit: Chạy Docker Bench Security định kỳ
Không cần áp dụng hết cùng lúc. Bắt đầu từ những thứ dễ nhất (pin image version, cấu hình log rotation, drop capabilities) rồi dần dần nâng lên. Quan trọng là mỗi container chạy trên production đều có ít nhất một vài lớp bảo vệ, thay vì chạy mặc định hoàn toàn.
Bạn có thể tìm hiểu thêm bài này dưới một góc nhìn khác mình trình bày dưới đây!
Tại sao bảo mật Docker quan trọng?
Nhiều người nghĩ container = cách ly = an toàn. Thực tế phức tạp hơn nhiều. Docker có 3 đặc điểm khiến nó trở thành mục tiêu tấn công nếu không được bảo mật đúng cách:
Docker daemon chạy với quyền root
Docker daemon (dockerd) chạy với quyền root trên máy host. Điều này có nghĩa là bất kỳ ai có quyền truy cập Docker daemon (qua CLI, API, hoặc Docker socket) đều có thể thực thi lệnh với quyền root trên máy chủ.
# Kiểm tra Docker daemon chạy với user nào
ps aux | grep dockerd
# root 1234 ... /usr/bin/dockerd
Container share kernel với host
Khác với virtual machine (mỗi VM có kernel riêng), tất cả container đều dùng chung kernel với máy host. Nếu kẻ tấn công tìm được cách escape ra khỏi container (container escape), họ có thể truy cập trực tiếp vào máy host.
┌─────────────────────────────────┐
│ Máy Host (VPS) │
│ ┌───────────┐ ┌───────────┐ │
│ │Container A│ │Container B│ │
│ └───────────┘ └───────────┘ │
│ ───────────────────────────── │
│ Shared Linux Kernel │ ← Cùng kernel!
└─────────────────────────────────┘
Misconfiguration = exposed
Docker mặc định ưu tiên tiện lợi hơn bảo mật. Container chạy root, port mở ra 0.0.0.0, không giới hạn resource… Tất cả đều là mặc định. Nếu bạn không chủ động cấu hình bảo mật, VPS của bạn đang mở cửa cho kẻ tấn công.
Hãy đi qua từng layer bảo mật mà bạn cần áp dụng.
Không chạy container với root
Đây là nguyên tắc quan trọng nhất: không bao giờ chạy process bên trong container với quyền root nếu không thực sự cần thiết. Nếu container bị xâm nhập và process chạy root, kẻ tấn công có nhiều khả năng escalate ra máy host hơn.
Vấn đề: Container mặc định chạy root
Nếu Dockerfile không chỉ định user, container sẽ chạy với quyền root:
# ❌ Dockerfile KHÔNG an toàn — process chạy root
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["node", "server.js"]
# Kiểm tra user đang chạy trong container
docker exec my_app whoami
# root ← Không tốt!
Giải pháp 1: USER directive trong Dockerfile
Tạo một non-root user trong Dockerfile và chuyển sang user đó trước khi chạy ứng dụng:
# ✅ Dockerfile AN TOÀN — process chạy non-root
FROM node:20-alpine
# Tạo group và user mới
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
COPY . .
RUN npm install
# Đổi ownership cho app directory
RUN chown -R appuser:appgroup /app
# Chuyển sang non-root user
USER appuser
CMD ["node", "server.js"]
# Kiểm tra lại
docker exec my_app whoami
# appuser ← An toàn hơn!
Giải pháp 2: –user flag khi docker run
Nếu bạn dùng image có sẵn (không tự build), có thể override user lúc chạy:
# Chạy container với user cụ thể (UID:GID)
docker run --user 1000:1000 nginx:alpine
# Hoặc dùng tên user nếu đã tồn tại trong image
docker run --user nobody nginx:alpine
Trong Docker Compose:
services:
web:
image: nginx:alpine
user: "1000:1000"
Lưu ý: Một số image cần quyền root để bind port thấp (dưới 1024) hoặc ghi vào thư mục hệ thống. Trong trường hợp đó, hãy dùng image đã được thiết kế để chạy non-root (ví dụ: nginxinc/nginx-unprivileged).
Giới hạn resource cho container
Mặc định, một container có thể dùng toàn bộ CPU và RAM của máy host. Điều này có nghĩa là một container bị lỗi hoặc bị tấn công DDoS có thể “giết” cả VPS, kéo theo tất cả container khác chết theo.
Giới hạn CPU
# Giới hạn container chỉ dùng tối đa 1 CPU core
docker run --cpus="1.0" nginx:alpine
# Giới hạn 50% của 1 core
docker run --cpus="0.5" nginx:alpine
# cpu-shares: ưu tiên tương đối giữa các container (mặc định 1024)
# Container này được ưu tiên CPU gấp đôi container khác
docker run --cpu-shares=2048 nginx:alpine
Khác biệt: --cpus là giới hạn cứng (hard limit), --cpu-shares là ưu tiên tương đối (chỉ có tác dụng khi CPU đang bị tranh chấp).
Giới hạn RAM
# Giới hạn container chỉ dùng tối đa 512MB RAM
docker run --memory="512m" nginx:alpine
# Giới hạn RAM + swap (tổng cộng 1GB, trong đó 512MB swap)
docker run --memory="512m" --memory-swap="1g" nginx:alpine
# Không cho dùng swap
docker run --memory="512m" --memory-swap="512m" nginx:alpine
Nếu container vượt quá giới hạn RAM, Docker sẽ kill container đó (OOM Killed) thay vì để nó ảnh hưởng đến cả VPS.
Giới hạn resource trong Docker Compose
services:
web:
image: nginx:alpine
deploy:
resources:
limits:
cpus: "1.0"
memory: 512M
reservations:
cpus: "0.25"
memory: 128M
limits là giới hạn tối đa, reservations là resource tối thiểu được đảm bảo cho container. Mình khuyên bạn nên set cả hai để Docker quản lý resource hiệu quả hơn.
Kiểm tra resource đang dùng của từng container:
docker stats --no-stream
Network security
Network là lớp bảo mật hay bị bỏ qua nhất khi dùng Docker. Rất nhiều người expose port vô tội vạ mà không biết hậu quả.
Không expose port không cần thiết
Nguyên tắc đơn giản: chỉ expose port ra bên ngoài khi thực sự cần. Nếu container chỉ cần giao tiếp với container khác, hãy dùng internal Docker network.
services:
web:
image: nginx:alpine
ports:
- "80:80" # Expose ra ngoài (cần thiết)
networks:
- frontend
- backend
app:
image: myapp:latest
networks:
- backend # Chỉ giao tiếp internal, KHÔNG expose port
db:
image: mysql:8
# ❌ KHÔNG expose port 3306 ra ngoài!
# ports:
# - "3306:3306"
networks:
- backend # Chỉ app mới truy cập được
networks:
frontend:
backend:
internal: true # Network này hoàn toàn nội bộ
Với cấu hình trên, database chỉ có thể được truy cập bởi app qua network backend. Không ai từ bên ngoài có thể kết nối trực tiếp vào MySQL.
Bind port trên 127.0.0.1 thay vì 0.0.0.0
Khi bạn viết ports: "8080:80", Docker mặc định bind trên 0.0.0.0: tức là lắng nghe trên tất cả network interface, ai cũng truy cập được.
Nếu service chỉ cần truy cập từ localhost (ví dụ: đang chạy sau reverse proxy), hãy bind trên 127.0.0.1:
services:
admin-panel:
image: admin:latest
ports:
# ✅ Chỉ truy cập từ localhost
- "127.0.0.1:8080:80"
# ❌ Ai cũng truy cập được (mặc định)
# - "8080:80"
Docker và firewall – Vấn đề UFW/iptables bypass
Đây là một cái bẫy rất nguy hiểm mà nhiều người mới không biết:
Docker bypass UFW! Khi Docker expose port, nó ghi rules trực tiếp vào iptables, bỏ qua hoàn toàn UFW. Tức là dù bạn đã chặn port 3306 trong UFW, nếu Docker expose port đó, nó vẫn mở cho cả thế giới.
# Bạn tưởng đã chặn port 3306
sudo ufw deny 3306
# ✅ UFW rule đã thêm
# Nhưng Docker expose port 3306
docker run -p 3306:3306 mysql:8
# ❌ Port 3306 vẫn mở cho cả internet! UFW bị bypass!
Có hai cách khắc phục:
Cách 1: Tắt Docker tự quản lý iptables
# Tạo hoặc sửa file /etc/docker/daemon.json
sudo nano /etc/docker/daemon.json
{
"iptables": false
}
# Restart Docker
sudo systemctl restart docker
Cảnh báo: Cách này sẽ làm container không thể truy cập internet nếu bạn không tự cấu hình iptables rules. Chỉ dùng nếu bạn hiểu rõ iptables.
Cách 2: Dùng ufw-docker (khuyên dùng)
# Cài đặt ufw-docker
sudo wget -O /usr/local/bin/ufw-docker \
https://github.com/chaifeng/ufw-docker/raw/master/ufw-docker
sudo chmod +x /usr/local/bin/ufw-docker
# Cài đặt UFW rules cho Docker
sudo ufw-docker install
# Cho phép truy cập port 80 của container web
sudo ufw-docker allow my_web_container 80/tcp
# Xem rules hiện tại
sudo ufw-docker status
ufw-docker sẽ chèn rules vào đúng chain trong iptables, giúp UFW và Docker hoạt động hài hoà với nhau.
Docker socket security
Docker socket (/var/run/docker.sock) là chìa khoá master của Docker. Ai có quyền truy cập vào socket này, người đó có thể tạo container, xoá container, mount filesystem của host, thực thi lệnh với quyền root, nói cách khác, kiểm soát toàn bộ máy chủ.
Tại sao mount Docker socket nguy hiểm?
Một số ứng dụng yêu cầu mount Docker socket vào container (ví dụ: Portainer, Traefik, Watchtower). Khi bạn làm điều này, container đó có quyền truy cập Docker API, tương đương quyền root trên host:
# ❌ Nguy hiểm: mount Docker socket vào container
docker run -v /var/run/docker.sock:/var/run/docker.sock some-app
# Nếu container bị xâm nhập, kẻ tấn công có thể:
# 1. Tạo container mới với quyền root
# 2. Mount filesystem của host vào container
# 3. Đọc/ghi bất kỳ file nào trên host
# 4. Cài backdoor vào máy host
Nguyên tắc: Không mount trừ khi cần thiết
Trước khi mount Docker socket, hãy tự hỏi: “Ứng dụng này có thực sự cần truy cập Docker API không?” Nếu không, đừng mount.
Nếu phải mount: Dùng proxy
Nếu bắt buộc phải cho container truy cập Docker API, hãy dùng docker-socket-proxy thay vì mount trực tiếp socket. Proxy này cho phép bạn kiểm soát chính xác container được phép gọi API nào:
services:
# Proxy bảo vệ Docker socket
docker-proxy:
image: tecnativa/docker-socket-proxy
environment:
- CONTAINERS=1 # Cho phép list containers
- SERVICES=0 # Không cho quản lý services
- TASKS=0 # Không cho quản lý tasks
- NETWORKS=0 # Không cho quản lý networks
- VOLUMES=0 # Không cho quản lý volumes
- IMAGES=0 # Không cho quản lý images
- POST=0 # Chỉ cho GET, không cho POST (read-only)
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
networks:
- docker-api
# Ứng dụng kết nối qua proxy, không trực tiếp vào socket
monitoring-app:
image: my-monitoring:latest
environment:
- DOCKER_HOST=tcp://docker-proxy:2375
networks:
- docker-api
networks:
docker-api:
internal: true # Network nội bộ, không ra internet
Với cách này, monitoring-app chỉ có quyền đọc danh sách container (GET /containers), không thể tạo/xoá container hay truy cập volumes. Ngay cả khi container bị xâm nhập, thiệt hại được hạn chế tối đa.
Image security


Image là nền tảng của mọi container. Nếu image chứa malware hoặc lỗ hổng bảo mật, mọi container chạy từ image đó đều bị ảnh hưởng.
Chỉ dùng official images hoặc verified publishers
Trên Docker Hub, hãy ưu tiên dùng:
- Official Images: Được Docker và cộng đồng duy trì, review code thường xuyên (ví dụ:
nginx,postgres,node) - Verified Publishers: Từ các công ty đã được Docker xác minh (ví dụ:
bitnami/nginx)
Tránh dùng image random từ người dùng không rõ nguồn gốc. Bạn không biết bên trong có gì.
Pin version cụ thể – Không dùng :latest cho production
# ❌ Không biết đang dùng version nào, có thể thay đổi bất kỳ lúc nào
FROM nginx:latest
# ✅ Pin version cụ thể, kết quả build nhất quán
FROM nginx:1.25.4-alpine
# ✅ Pin bằng digest (SHA256) — chắc chắn 100% đúng image
FROM nginx@sha256:6a5c657a4a12...
Tag :latest là một tag di động, nó luôn trỏ đến version mới nhất. Nếu maintainer push một version mới có breaking change hoặc lỗ hổng, container của bạn sẽ tự động dùng version đó lần rebuild tiếp theo mà bạn không biết.
Scan vulnerabilities
Trước khi deploy image lên production, hãy scan tìm lỗ hổng bảo mật:
# Dùng Docker Scout (tích hợp sẵn trong Docker Desktop / CLI)
docker scout cves nginx:1.25.4-alpine
# Dùng Trivy (open-source, phổ biến)
# Cài đặt Trivy
curl -sfL https://raw.githubusercontent.com/aquasecurity/trivy/main/contrib/install.sh | sh
# Scan image
trivy image nginx:1.25.4-alpine
Kết quả scan sẽ liệt kê các CVE (Common Vulnerabilities and Exposures) được tìm thấy trong image, kèm mức độ nghiêm trọng (Critical, High, Medium, Low).
Dùng minimal base images
Image càng nhỏ = càng ít packages = càng ít attack surface. Ưu tiên dùng:
- Alpine Linux: ~5MB, có package manager (
apk), phù hợp hầu hết use case - Distroless (Google): Chỉ chứa runtime, không có shell, package manager, hay bất kỳ tool nào: lý tưởng cho production
# So sánh kích thước base image
# node:20 → ~1GB
# node:20-slim → ~200MB
# node:20-alpine → ~130MB
# Dùng Alpine
FROM node:20-alpine
# Hoặc dùng distroless cho production (Go, Java, Node, Python)
FROM gcr.io/distroless/nodejs20-debian12
Secrets management
Passwords, API keys, database credentials, đây là những thông tin nhạy cảm mà bạn cần xử lý cẩn thận khi dùng Docker.
Không hardcode secrets trong Dockerfile hoặc image
# ❌ TUYỆT ĐỐI KHÔNG LÀM THẾ NÀY
FROM node:20-alpine
ENV DB_PASSWORD=SuperSecret123
ENV API_KEY=sk-abc123xyz
Secrets nằm trong image sẽ bị lộ cho bất kỳ ai có quyền pull image. Thậm chí nếu bạn xoá biến môi trường ở layer sau, nó vẫn tồn tại trong các layer trước của image (có thể trích xuất bằng docker history).
Không truyền secrets qua build args
# ❌ Build args cũng bị lưu trong image layers
docker build --build-arg DB_PASSWORD=SuperSecret123 .
# Ai cũng có thể xem build args bằng lệnh này
docker history my_image --no-trunc
Dùng .env file
Cách phổ biến và đơn giản nhất: lưu secrets vào file .env và truyền vào container lúc runtime:
# Tạo file .env
cat > .env << 'EOF'
DB_PASSWORD=SuperSecret123
API_KEY=sk-abc123xyz
REDIS_URL=redis://redis:6379
EOF
# QUAN TRỌNG: Thêm .env vào .gitignore
echo ".env" >> .gitignore
# Thêm .env vào .dockerignore (không copy vào image)
echo ".env" >> .dockerignore
# docker-compose.yml
services:
app:
image: myapp:latest
env_file:
- .env
Docker secrets (Swarm mode)
Nếu bạn dùng Docker Swarm, có thể dùng Docker secrets, cơ chế an toàn hơn, secrets được mã hoá và chỉ mount vào container cần thiết:
# Tạo secret
echo "SuperSecret123" | docker secret create db_password -
# Dùng trong docker-compose.yml (Swarm mode)
# Secret sẽ được mount tại /run/secrets/db_password
services:
app:
image: myapp:latest
secrets:
- db_password
environment:
- DB_PASSWORD_FILE=/run/secrets/db_password
secrets:
db_password:
external: true
Ứng dụng sẽ đọc password từ file /run/secrets/db_password thay vì từ biến môi trường. Cách này an toàn hơn vì secrets không xuất hiện trong docker inspect hay process list.
Read-only filesystem
Nếu ứng dụng của bạn không cần ghi file vào filesystem, hãy chạy container với read-only filesystem. Điều này ngăn kẻ tấn công cài đặt malware, tạo script, hoặc sửa đổi file cấu hình bên trong container.
Dùng –read-only flag
# Chạy container với filesystem read-only
docker run --read-only nginx:alpine
# Nếu ứng dụng cần ghi vào /tmp, mount tmpfs
docker run --read-only --tmpfs /tmp:rw,noexec,nosuid nginx:alpine
# Nginx cần ghi vào /var/cache/nginx và /var/run
docker run --read-only \
--tmpfs /tmp:rw,noexec,nosuid \
--tmpfs /var/cache/nginx:rw \
--tmpfs /var/run:rw \
nginx:alpine
Trong Docker Compose:
services:
web:
image: nginx:alpine
read_only: true
tmpfs:
- /tmp:rw,noexec,nosuid
- /var/cache/nginx:rw
- /var/run:rw
Giải thích tmpfs options:
rw: cho phép đọc/ghi vào tmpfsnoexec: không cho phép thực thi binary từ tmpfs (ngăn chạy malware)nosuid: không cho phép setuid bit (ngăn privilege escalation)
Kết hợp read-only filesystem với non-root user, bạn đã giảm đáng kể attack surface của container.


Checklist bảo mật Docker
Dưới đây là checklist để bạn kiểm tra lại hệ thống Docker trên VPS. Đi qua từng mục và đảm bảo bạn đã thực hiện:
| STT | Hạng mục | Mô tả | Trạng thái |
|---|---|---|---|
| 1 | Non-root user | Container chạy với non-root user (USER directive hoặc –user flag) | ☐ |
| 2 | Resource limits | Đã set CPU và RAM limits cho tất cả container | ☐ |
| 3 | Network isolation | Database và internal services không expose port ra ngoài | ☐ |
| 4 | Port binding | Các service nội bộ bind trên 127.0.0.1 thay vì 0.0.0.0 | ☐ |
| 5 | Firewall + Docker | Đã xử lý UFW/iptables bypass issue (ufw-docker hoặc iptables: false) | ☐ |
| 6 | Docker socket | Không mount docker.sock vào container (hoặc dùng socket proxy) | ☐ |
| 7 | Official images | Chỉ dùng official images hoặc verified publishers | ☐ |
| 8 | Image version | Pin version cụ thể, không dùng :latest cho production | ☐ |
| 9 | Image scanning | Đã scan images bằng docker scout hoặc trivy | ☐ |
| 10 | Minimal base image | Dùng Alpine hoặc distroless thay vì full OS image | ☐ |
| 11 | No hardcoded secrets | Không có password/API key trong Dockerfile hay image | ☐ |
| 12 | .env + .gitignore | Secrets trong .env file, đã thêm vào .gitignore và .dockerignore | ☐ |
| 13 | Read-only filesystem | Container production chạy với –read-only khi có thể | ☐ |
| 14 | Docker version | Đang dùng Docker version mới nhất (đã patch security fixes) | ☐ |
| 15 | Log management | Đã giới hạn log size để tránh đầy disk (Bài 14) | ☐ |
Không cần hoàn thành 100% ngay lập tức. Hãy bắt đầu từ những mục quan trọng nhất (1-6), sau đó dần hoàn thiện các mục còn lại. Bảo mật là một quá trình liên tục, không phải việc làm một lần.
Tổng kết
Trong bài này, bạn đã học cách bảo mật Docker trên VPS ở nhiều lớp:
- Process level: Không chạy container với root
- Resource level: Giới hạn CPU và RAM cho từng container
- Network level: Không expose port không cần, bind 127.0.0.1, xử lý UFW bypass
- Docker API level: Bảo vệ Docker socket bằng proxy
- Image level: Dùng official images, pin version, scan vulnerabilities
- Secrets level: Không hardcode, dùng .env hoặc Docker secrets
- Filesystem level: Read-only filesystem + tmpfs
Bảo mật không phải là thêm một công cụ hay chạy một lệnh. Nó là tư duy: luôn tự hỏi “nếu container này bị hack, kẻ tấn công làm được gì?” và giảm thiểu câu trả lời đến mức tối thiểu.
Ở Bài 16, mình sẽ hướng dẫn bạn setup CI/CD cho Docker: tự động build image, chạy test, scan vulnerabilities và deploy lên VPS mỗi khi push code. Đón đọc nhé!
- Tại sao Docker cần bảo mật riêng?
- Bảo mật Docker daemon
- Bind socket chỉ localhost
- TLS cho remote access
- Rootless Docker
- Cài đặt trên Ubuntu
- Cài đặt trên AlmaLinux/Rocky Linux
- Bảo mật image
- Chỉ dùng official images
- Pin version cụ thể
- Scan image tìm lỗ hổng
- Multi-stage build giảm bề mặt tấn công
- Runtime security
- Read-only filesystem
- Chặn privilege escalation
- Drop Linux capabilities
- Giới hạn tài nguyên
- Kết hợp tất cả
- Network isolation
- Tạo internal network cho backend
- Chỉ expose port cần thiết
- Docker Compose security patterns
- Quản lý secrets
- Docker secrets (Compose)
- Bảo vệ .env file
- Cấu hình logging
- Cho từng container
- Cấu hình mặc định cho toàn bộ daemon
- Docker Bench Security
- Checkpoint: Kiểm tra lại
- 1. Scan image
- 2. Chạy container với security options
- 3. Chạy Docker Bench Security
- Tóm lại
- Tại sao bảo mật Docker quan trọng?
- Docker daemon chạy với quyền root
- Container share kernel với host
- Misconfiguration = exposed
- Không chạy container với root
- Vấn đề: Container mặc định chạy root
- Giải pháp 1: USER directive trong Dockerfile
- Giải pháp 2: –user flag khi docker run
- Giới hạn resource cho container
- Giới hạn CPU
- Giới hạn RAM
- Giới hạn resource trong Docker Compose
- Network security
- Không expose port không cần thiết
- Bind port trên 127.0.0.1 thay vì 0.0.0.0
- Docker và firewall – Vấn đề UFW/iptables bypass
- Docker socket security
- Tại sao mount Docker socket nguy hiểm?
- Nguyên tắc: Không mount trừ khi cần thiết
- Nếu phải mount: Dùng proxy
- Image security
- Chỉ dùng official images hoặc verified publishers
- Pin version cụ thể – Không dùng :latest cho production
- Scan vulnerabilities
- Dùng minimal base images
- Secrets management
- Không hardcode secrets trong Dockerfile hoặc image
- Không truyền secrets qua build args
- Dùng .env file
- Docker secrets (Swarm mode)
- Read-only filesystem
- Dùng –read-only flag
- Checklist bảo mật Docker
- Tổng kết




















No Comment! Be the first one.