Chạy ứng dụng Docker trên VPS an toàn: 9 việc cần làm đúng

Cách chạy ứng dụng Docker trên VPS an toàn - cổng chỉ mở nội bộ, user namespace, giới hạn RAM, cách ly mạng, SSL - kèm cấu hình mẫu và cách làm với ZoPanel.

Sơ đồ: container Docker sau nginx và SSL, cổng chỉ mở trên 127.0.0.1

Ý chính

  • Cổng Docker publish ra ngoài đi vòng qua UFW, nên hãy publish trên 127.0.0.1 và cho truy cập qua reverse proxy có SSL.
  • User namespace (userns-remap) ánh xạ root trong container sang một UID không có quyền trên máy chủ.
  • Mỗi container nên có giới hạn RAM, CPU, số tiến trình và không được tự nâng quyền.
  • Ai điều khiển được Docker daemon thì coi như có quyền root trên server.

Muốn chạy ứng dụng Docker trên VPS an toàn, bạn cần làm đúng bốn việc: chỉ publish cổng trên 127.0.0.1 rồi cho khách vào qua nginx có SSL, bật user namespace để root trong container không phải root thật, đặt giới hạn RAM, CPU và số tiến trình cho từng container, và không bao giờ đưa Docker socket hay thư mục hệ thống vào container. Phần còn lại (cập nhật image, sao lưu volume, xoay vòng log) giúp bạn ngủ ngon hơn khi server chạy nhiều ứng dụng cùng lúc.

Bài viết đi qua từng điểm, giải thích vì sao cấu hình mặc định của Docker chưa đủ cho một VPS chạy thật, đưa ra file cấu hình và lệnh mẫu cho Ubuntu 24.04 và Debian 12, rồi chỉ cách ZoPanel làm những việc này sẵn cho bạn trong Kho ứng dụng.

Vì sao Docker mặc định chưa an toàn trên VPS?

Docker rất tiện: một lệnh docker run là có n8n, Uptime Kuma hay Nextcloud. Nhưng sự tiện lợi đó đi kèm vài mặc định mà nhiều người không để ý cho tới khi gặp sự cố.

Cổng publish đi vòng qua firewall. Khi bạn chạy -p 8080:80, Docker tự thêm luật iptables để chuyển gói tin vào container. Theo tài liệu chính thức của Docker về packet filtering, gói tin được định tuyến trước khi các luật của UFW kịp áp dụng, nên Docker và UFW "không tương thích với nhau". Nghĩa là bạn đã chặn cổng 8080 bằng UFW, nhưng cả internet vẫn vào được. Đây là lỗi phổ biến nhất khi tự host trên VPS: trang quản trị của một ứng dụng nằm phơi trên IP công khai, không SSL, không ai biết.

Root trong container gần với root trên máy chủ. Mặc định, UID 0 trong container chính là UID 0 trên host, chỉ bị giới hạn bởi namespace, capability và seccomp. Nếu ứng dụng có lỗ hổng cho phép thoát khỏi container, kẻ tấn công ra ngoài với quyền root.

Không có giới hạn tài nguyên. Container không đặt --memory có thể ăn hết RAM của VPS. Một ứng dụng rò bộ nhớ là đủ làm database, mail và mọi website khác trên server bị kernel kết liễu.

Container nói chuyện được với nhau và với dịch vụ nội bộ. Trên mạng bridge mặc định, container có thể kết nối tới container khác, và thường tới được cả MariaDB, Redis đang lắng nghe trên máy chủ. Một ứng dụng bị chiếm quyền trở thành bàn đạp vào phần còn lại.

Docker daemon chạy bằng root. Tài liệu bảo mật của Docker nói rõ chỉ người dùng tin cậy mới được điều khiển daemon, và trang hướng dẫn sau cài đặt cảnh báo nhóm docker "cấp quyền tương đương root". Thêm user SSH vào nhóm docker cho đỡ gõ sudo thực chất là cho user đó quyền root.

9 việc để chạy container an toàn

1. Cài Docker từ kho chính thức

Trên Ubuntu 24.04 hoặc Debian 12, cài Docker Engine theo hướng dẫn chính thức (thêm kho apt của Docker, kiểm tra khoá GPG). Tránh chạy script cài đặt tải từ nguồn không rõ. Sau khi cài, đừng thêm tài khoản thường vào nhóm docker trừ khi bạn chấp nhận tài khoản đó có quyền root.

2. Chỉ publish cổng trên 127.0.0.1

Thay vì -p 3001:3001, hãy viết:

-p 127.0.0.1:3001:3001

Container chỉ nghe trên giao diện loopback, internet không thể chạm vào. Người dùng vào ứng dụng qua nginx (bước 7). Muốn chắc chắn hơn, đặt mặc định cho cả daemon trong /etc/docker/daemon.json bằng khoá "ip": "127.0.0.1", để lỡ quên ghi địa chỉ thì cổng vẫn không lộ ra ngoài.

3. Bật user namespace

Với userns-remap, root trong container được ánh xạ sang một dải UID cao không có quyền gì trên máy chủ. Tài liệu userns-remap của Docker hướng dẫn thêm vào daemon.json:

{
  "userns-remap": "default"
}

Lưu ý quan trọng: bật tính năng này sẽ "giấu" image và container đã có (chúng nằm ở thư mục dữ liệu khác), nên tốt nhất là bật ngay trên server mới. Nó cũng không dùng chung được với --network=host hay --pid=host, và volume gắn từ thư mục host cần chỉnh quyền sở hữu trước.

4. Tắt giao tiếp giữa các container và chặn mạng nội bộ

Thêm "icc": false để container trên bridge mặc định không gọi thẳng được nhau. Đây mới là một nửa: container vẫn có thể kết nối tới các dịch vụ trên máy chủ (database, Redis, panel) và tới dịch vụ metadata của nhà cung cấp cloud ở 169.254.169.254, nơi đôi khi chứa thông tin nhạy cảm. Chặn các dải này đòi hỏi luật iptables riêng trong chuỗi DOCKER-USER, và đây là chỗ dễ sai nhất nếu tự làm. Nếu không chắc, hãy để database chỉ nghe trên socket hoặc 127.0.0.1 và đặt mật khẩu mạnh, đừng dựa vào việc "chắc container không biết địa chỉ".

5. Đặt giới hạn tài nguyên cho từng container

--memory 512m --cpus 1 --pids-limit 512

--memory ngăn một ứng dụng nuốt hết RAM, --pids-limit chặn fork bomb. Cộng tổng giới hạn RAM của mọi container, rồi chừa thêm ít nhất 1 GB cho hệ thống, database và web server. Nếu tổng vượt RAM thật, bạn đang bán khống bộ nhớ của chính mình.

6. Không cho container tự nâng quyền

--security-opt no-new-privileges --cap-drop NET_RAW --cap-drop MKNOD

no-new-privileges ngăn tiến trình trong container leo quyền qua file setuid. Bỏ bớt capability không cần (như NET_RAW, dùng để giả mạo gói tin) thu hẹp những gì một container bị chiếm có thể làm. Có thể đặt "no-new-privileges": true trong daemon.json để áp cho mọi container.

7. Đưa ứng dụng ra ngoài qua nginx và SSL

Một server block tối thiểu cho ứng dụng đang nghe trên 127.0.0.1:3001:

server {
    listen 443 ssl;
    server_name status.example.com;
    # ssl_certificate ... (Let's Encrypt)

    location / {
        proxy_pass http://127.0.0.1:3001;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
    }
}

Hai dòng Upgrade/Connection cần cho ứng dụng dùng WebSocket như Uptime Kuma hay n8n. Chứng chỉ SSL lấy từ Let's Encrypt; một số ứng dụng như Vaultwarden từ chối chạy trên HTTP thường.

8. Ghim phiên bản image và cập nhật có kế hoạch

Dùng tag cụ thể (ví dụ louislam/uptime-kuma:1) thay vì latest trôi nổi, để biết chính xác đang chạy gì. Lên lịch cập nhật hằng tháng, và theo dõi dự án upstream: một ứng dụng đã ngừng nhận bản vá bảo mật thì nên thay, dù nó vẫn chạy.

9. Xoay vòng log và sao lưu volume

Log JSON của Docker mặc định không giới hạn kích thước và có thể lấp đầy đĩa. Thêm vào daemon.json:

{
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Dữ liệu thật của ứng dụng nằm trong volume. Hãy đưa thư mục volume vào lịch sao lưu và thử khôi phục ít nhất một lần.

Một file daemon.json và lệnh chạy mẫu

Gộp các điểm trên, /etc/docker/daemon.json trên một VPS mới có thể như sau:

{
  "ip": "127.0.0.1",
  "icc": false,
  "userns-remap": "default",
  "no-new-privileges": true,
  "log-driver": "json-file",
  "log-opts": { "max-size": "10m", "max-file": "3" }
}

Áp dụng bằng sudo systemctl restart docker (mọi container đang chạy sẽ khởi động lại). Rồi chạy một ứng dụng, ví dụ Uptime Kuma:

docker run -d --name uptime-kuma --restart unless-stopped \
  -p 127.0.0.1:3001:3001 \
  --memory 512m --cpus 1 --pids-limit 512 \
  --cap-drop NET_RAW --cap-drop MKNOD \
  -v uptime-kuma-data:/app/data \
  louislam/uptime-kuma:1

Kiểm tra cổng thật sự chỉ nghe nội bộ:

sudo ss -ltnp | grep 3001
# kết quả phải là 127.0.0.1:3001, không phải 0.0.0.0:3001

Từ một máy khác, curl http://IP-server:3001 phải thất bại. Nếu vẫn vào được, cổng đang lộ.

Những sai lầm hay gặp

Sai lầm Hậu quả Cách đúng
-p 8080:80 và tin rằng UFW sẽ chặn Ứng dụng lộ ra internet -p 127.0.0.1:8080:80 + reverse proxy
Gắn /var/run/docker.sock vào container "cho tiện" Container điều khiển được Docker, tức có root Không gắn, trừ công cụ bạn tin tuyệt đối
--privileged khi gặp lỗi quyền Gần như tắt mọi lớp cách ly Tìm đúng capability còn thiếu
Không đặt --memory Một ứng dụng làm sập cả server Giới hạn RAM cho từng container
Gắn / hoặc /etc của host vào container Container sửa được hệ thống Chỉ gắn thư mục dữ liệu riêng
Ứng dụng mới cài để mở, chưa tạo admin Người lạ vào trước, tạo tài khoản quản trị Tạo admin ngay hoặc khoá truy cập tới khi xong

Dòng cuối ít được nhắc nhưng rất thật: với n8n, Gitea, Nextcloud và nhiều ứng dụng khác, người đầu tiên mở trang sẽ tạo tài khoản quản trị. Bot quét chứng chỉ SSL mới cấp có thể tìm thấy tên miền của bạn trong vài phút.

Chạy ứng dụng Docker an toàn với ZoPanel

ZoPanel làm sẵn các bước trên khi bạn cài Docker qua Kho ứng dụng → Cài Docker hoặc Thành phần → Docker. Theo tài liệu Kho ứng dụng:

  • User namespace (userns-remap) được bật tự động, ánh xạ UID container sang dải không trùng với tài khoản hosting nào.
  • Container chỉ publish cổng trên 127.0.0.1. Khách vào ứng dụng qua nginx của website, có SSL Let's Encrypt cấp tự động khi tên miền đã trỏ về server.
  • Container không kết nối được với nhau, kể cả khi ai đó đổi icc trong daemon.json. Container cũng bị chặn khỏi các dải địa chỉ nội bộ, link-local (dịch vụ metadata 169.254.169.254) và loopback; trên chính server, container chỉ tới được các cổng mà internet cũng tới được (web, mail, DNS), không tới được database, Redis hay panel. Nếu không áp được các luật firewall này, ZoPanel không khởi động container.
  • Mỗi container chạy với no-new-privileges, tối đa 512 tiến trình, bỏ các capability NET_RAW, MKNOD, AUDIT_WRITE, có giới hạn RAM và 1 CPU. Container của khách chạy trong nhóm tài nguyên của chính tài khoản đó.
  • Dữ liệu nằm trong /var/lib/zopanel-apps/<instance>/, chỉ root và container đọc được; biến môi trường truyền qua file chỉ root đọc, không qua dòng lệnh. Log xoay vòng 3 file 10 MB.

Cài một ứng dụng gồm bốn bước: mở Kho ứng dụng, chọn ứng dụng (n8n, Uptime Kuma, Nextcloud, Gitea, Vaultwarden, Metabase, Ghost, Open WebUI…), nhập tên miền, bấm Cài đặt, rồi trỏ bản ghi A của tên miền về server. Xem từng ứng dụng dùng để làm gì và cần bao nhiêu RAM trước khi chọn.

Ứng dụng mới bị khoá cho tới khi bạn cài đặt xong. Người ngoài chỉ thấy trang "đang được thiết lập". Trên tab Docker của website, bạn bấm mở ứng dụng cho riêng mình, tạo tài khoản admin, rồi mới mở cho mọi người. Đây chính là lời giải cho sai lầm cuối trong bảng trên.

Cho khách hàng dùng Docker. Nếu bạn bán hosting, đặt số ứng dụng tối đa trong Gói hosting → Ứng dụng Docker (0 là không cho phép). RAM của mỗi ứng dụng được tính vào RAM của gói, cài vượt sẽ bị từ chối. Khách chỉ cài được ứng dụng trong danh mục. Image tuỳ chỉnh dành cho quản trị viên, và chỉ bật được bằng lệnh chạy với quyền root trên server:

zopanel ctl feature enable custom-docker

Công tắc này cố ý không có trong giao diện web: một phiên đăng nhập panel bị đánh cắp cũng không thể khởi chạy container tuỳ ý. Container do khách tự build còn bị bỏ gần hết capability, dùng profile seccomp và AppArmor mặc định, và không bao giờ thấy Docker socket hay thư mục của host. Bạn có thể đọc thêm lý do thiết kế này trong bài kiến trúc panel hosting an toàn.

Cần VPS bao nhiêu RAM để chạy ứng dụng Docker?

Cộng giới hạn RAM của các ứng dụng rồi thêm khoảng 1 GB cho hệ thống và panel. Vài con số tham khảo từ danh mục của ZoPanel: Vaultwarden và Memos 256 MB, Uptime Kuma và Gitea 512 MB, n8n và Nextcloud 1 GB, Metabase 1,5 GB, Open WebUI 2 GB. Một VPS 2 GB chạy thoải mái Uptime Kuma, Vaultwarden và vài website; muốn thêm n8n hay Nextcloud thì nên từ 4 GB. Nếu bạn định tự host n8n, bài tự host n8n trên VPS đi sâu hơn vào cấu hình.

Kết luận

Docker không làm VPS kém an toàn, nhưng các mặc định của nó được thiết kế cho máy dev, không phải server chạy thật ngoài internet. Chỉ mở cổng nội bộ, bật user namespace, giới hạn tài nguyên, chặn mạng nội bộ và khoá ứng dụng cho tới khi tạo xong admin: làm đủ năm việc đó, bạn đã tránh được phần lớn sự cố thường gặp.

Nếu không muốn tự duy trì luật iptables và daemon.json, gói Free của ZoPanel (3 tài khoản hosting, 10 website, 10 database, không giới hạn thời gian) đã có Kho ứng dụng và các lớp cách ly Docker nói trên. Cài bằng curl -fsSL https://get.zopanel.net | sudo bash trên Ubuntu 22.04/24.04 hoặc Debian 12/13, và xem bảng giá khi cần nhiều hơn.

Câu hỏi thường gặp

UFW có chặn được cổng Docker không?

Không, với cổng publish theo kiểu mặc định. Docker chèn luật iptables của riêng nó, gói tin được chuyển vào container trước khi luật UFW được xét. Cách đơn giản nhất là publish trên 127.0.0.1 (-p 127.0.0.1:8080:80) và đưa ứng dụng ra ngoài qua nginx.

Rootless Docker hay userns-remap tốt hơn?

Cả hai đều làm root trong container không còn là root thật trên host. Rootless mode còn chạy chính daemon bằng user thường, an toàn hơn nhưng có giới hạn về mạng và cổng thấp. userns-remap giữ daemon chạy root nhưng tương thích tốt hơn, là lựa chọn thực dụng cho server chạy nhiều ứng dụng.

Có nên thêm user vào nhóm docker không?

Chỉ khi bạn chấp nhận user đó có quyền tương đương root. Tài liệu của Docker cảnh báo rõ nhóm docker cấp quyền root. Trên server có nhiều người dùng hoặc nhiều khách, hãy để việc điều khiển Docker cho root hoặc cho panel quản lý.

Chạy Docker cạnh website WordPress trên cùng VPS có ổn không?

Ổn, nếu mỗi container có giới hạn RAM và tổng các giới hạn vẫn chừa đủ chỗ cho PHP, database và web server. Điều cần tránh là container không giới hạn RAM, vì một ứng dụng rò bộ nhớ có thể kéo sập mọi website.

Khách hàng hosting có tự cài ứng dụng Docker được trên ZoPanel không?

Được, khi gói của họ cho phép (Gói hosting → Ứng dụng Docker). Khách chỉ cài được ứng dụng trong danh mục, RAM của ứng dụng tính vào RAM của gói, và container chạy trong giới hạn tài nguyên của chính tài khoản đó.