❤️ AZDIGI chính thức cập nhật hệ thống blog mới hoàn chỉnh. Tuy nhiên có thể một số bài viết bị sai lệch hình ảnh, hãy ấn nút Báo cáo bài viết ở cuối bài để AZDIGI cập nhật trong thời gian nhanh nhất. Chân thành cám ơn.

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.

Bảo mật Docker - Hardening Docker trên VPS
Minh họa: Bảo mật Docker – Hardening Docker trên VPS

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.

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

Scan Docker image bằng Trivy
Trivy scan phát hiện vulnerabilities trong image

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 tmpfs
  • noexec: 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.

Kiểm tra security settings
Kiểm tra user, capabilities, read-only filesystem

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:

STTHạng mụcMô tảTrạng thái
1Non-root userContainer chạy với non-root user (USER directive hoặc –user flag)
2Resource limitsĐã set CPU và RAM limits cho tất cả container
3Network isolationDatabase và internal services không expose port ra ngoài
4Port bindingCác service nội bộ bind trên 127.0.0.1 thay vì 0.0.0.0
5Firewall + DockerĐã xử lý UFW/iptables bypass issue (ufw-docker hoặc iptables: false)
6Docker socketKhông mount docker.sock vào container (hoặc dùng socket proxy)
7Official imagesChỉ dùng official images hoặc verified publishers
8Image versionPin version cụ thể, không dùng :latest cho production
9Image scanningĐã scan images bằng docker scout hoặc trivy
10Minimal base imageDùng Alpine hoặc distroless thay vì full OS image
11No hardcoded secretsKhông có password/API key trong Dockerfile hay image
12.env + .gitignoreSecrets trong .env file, đã thêm vào .gitignore và .dockerignore
13Read-only filesystemContainer production chạy với –read-only khi có thể
14Docker versionĐang dùng Docker version mới nhất (đã patch security fixes)
15Log 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.

📚 Serie Docker từ A đến Z

  1. Bài 1: Docker là gì? Tại sao nên dùng Docker trên VPS
  2. Bài 2: Cài đặt Docker và Docker Compose trên VPS Ubuntu
  3. Bài 3: Làm quen với Docker – Các lệnh cơ bản cần biết
  4. Bài 4: Docker Image & Dockerfile – Tự tạo Image riêng
  5. Bài 5: Docker Volume & Network – Quản lý dữ liệu và mạng
  6. Bài 6: Docker Compose là gì? Cài đặt và cú pháp cơ bản
  7. Bài 7: Deploy WordPress + MySQL + phpMyAdmin bằng Docker Compose
  8. Bài 8: Deploy LEMP Stack (Nginx + PHP-FPM + MariaDB) bằng Docker Compose
  9. Bài 9: Biến môi trường & file .env trong Docker Compose
  10. Bài 10: Reverse Proxy với Nginx Proxy Manager + SSL tự động
  11. Bài 11: Deploy ứng dụng Node.js / Python với Docker Compose
  12. Bài 12: Backup & Restore dữ liệu Docker Volume
  13. Bài 13: Monitoring Docker với Portainer, Uptime Kuma và cAdvisor
  14. Bài 14: Docker Logging – Quản lý log hiệu quả
  15. Bài 15: Bảo mật Docker trên VPS (đang đọc)
  16. Bài 16: CI/CD đơn giản – Auto deploy với Webhook + Docker Compose
  17. Bài 17: Docker Compose trong thực tế – Tổng hợp project mẫu

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é!

👈 Bài trước: Docker Logging – Quản lý log hiệu quả

👉 Bài tiếp: CI/CD đơn giản – Auto deploy với Webhook + Docker Compose

Chia sẻ:
Bài viết đã được kiểm duyệt bởi AZDIGI Team

Về tác giả

Thạch Phạm

Thạch Phạm

Đồng sáng lập và Giám đốc điều hành của AZDIGI. Có hơn 15 năm kinh nghiệm trong phổ biến kiến thức liên quan đến WordPress tại thachpham.com, phát triển website và phát triển hệ thống.

Hơn 10 năm phục vụ 80.000+ khách hàng

Bắt đầu dự án web của bạn với AZDIGI