SSD Nodes Learn 🎉 VPS từ $4.99/tháng
Hướng dẫn Matt ConnorBởi Matt Connor

Docker trên VPS: 4 điểm khác biệt bạn phải biết

Docker trên VPS vẫn dùng cùng engine, nhưng dễ gặp OOM kill, port bypass UFW, container không tự chạy sau reboot và disk đầy vì image, volume, cache.

Điều gì thay đổi khi chạy Docker trên VPS

Docker trên VPS dùng cùng engine và cùng image như Docker trên laptop, nên mọi command bạn đã biết vẫn hoạt động. Điều thay đổi là phần tài nguyên xung quanh nó. Laptop thường còn dư memory, có firewall mà hầu như không ai scan, và có disk đủ lớn đến mức bạn không bao giờ phải kiểm tra. Server thuê có giới hạn memory cố định, có public IP bị scan chỉ vài phút sau khi boot, và có root filesystem mà Docker sẽ làm đầy mà không hỏi trước.

Bốn khác biệt gây ra phần lớn sự cố trên một máy nhỏ:

  • Memory có giới hạn, và kernel giải quyết tình trạng thiếu memory bằng cách kill một process.
  • Port được publish đi thẳng qua UFW (uncomplicated firewall), vì Docker tự ghi các rule firewall của nó.
  • Container không tự khởi động lại sau reboot nếu bạn không cấu hình việc đó từ trước.
  • Image, container, volume và build cache sẽ tăng dần cho đến khi disk đầy.

Mỗi phần bên dưới nêu rõ lỗi, chuỗi thông báo bạn sẽ thực sự thấy và hướng dẫn khắc phục chi tiết. Nếu bạn chưa viết file Compose, hãy đọc Kiến thức cơ bản về Docker Compose trên VPS trước rồi quay lại. Trang này giả định bạn đã biết cách khởi động một stack.

Container Docker sử dụng bao nhiêu RAM?

Ít hơn nhiều so với dự đoán của phần lớn người dùng. Container là một process trong cgroup (control group), không phải virtual machine, nên không có guest kernel và cũng không có mức cấp phát cố định. Chi phí bộ nhớ phụ thuộc vào những gì process bên trong thực sự truy cập. Vì vậy, một full stack có thể chạy trong 2 GB, trong khi cùng stack đó nếu dựng bằng virtual machine thì không thể.

Các số liệu dưới đây là mức idle thường gặp của các image mặc định trên Ubuntu 24.04 với cấu hình mặc định, được đọc từ docker stats vài phút sau khi khởi động. Đây là cơ sở ban đầu để lập kế hoạch, không phải benchmark cho workload của bạn. Hãy chạy docker stats --no-stream trên chính máy của bạn trước khi tin vào bất kỳ số liệu nào, kể cả các số liệu này.

ChartTypical idle container memory, stock images, megabytes
The data behind this chart
[
  {
    "label": "nginx",
    "idle_mb": 8,
    "budget_mb": 64
  },
  {
    "label": "Redis 7",
    "idle_mb": 12,
    "budget_mb": 128
  },
  {
    "label": "Traefik v3",
    "idle_mb": 40,
    "budget_mb": 128
  },
  {
    "label": "PostgreSQL 16",
    "idle_mb": 45,
    "budget_mb": 512
  },
  {
    "label": "Uptime Kuma",
    "idle_mb": 95,
    "budget_mb": 256
  },
  {
    "label": "MariaDB 11",
    "idle_mb": 190,
    "budget_mb": 512
  },
  {
    "label": "Nextcloud (Apache)",
    "idle_mb": 210,
    "budget_mb": 768
  }
]

Hai cột có mục đích khác nhau. idle_mb là mức container sử dụng khi không làm gì. budget_mb là mức cần dự trù khi lập kế hoạch, vì sử dụng thực tế không ở trạng thái idle. PostgreSQL dùng gần 45 MB khi idle và cần 512 MB khi các connection, thao tác sort và cache đã hoạt động. Hãy lập kế hoạch theo cột ngân sách. Hãy debug theo cột idle.

Hãy chú ý đến dạng phân bổ của 7 dòng đó. nginx dùng 8 MB khi idle, còn Nextcloud dùng 210 MB. Reverse proxy phía trước các ứng dụng gần như không tốn tài nguyên. Database và ứng dụng PHP mới là những thành phần quyết định mức tài nguyên cần cấp cho máy.

Có một lưu ý về docker stats: số liệu bộ nhớ bao gồm page cache được tạo ra khi container đọc file của chính nó, nên sẽ tăng trong một khoảng thời gian sau khi khởi động rồi ổn định. Hãy monitor trong một giờ trước khi kết luận có memory leak.

Chọn cấu hình VPS: 2 GB, 4 GB và 8 GB phù hợp với những gì

Trước tiên, hãy trừ phần tài nguyên dành cho host. Kernel, systemd, journald, sshd và Docker daemon đều dùng chung RAM với các container của bạn, và dockerd cùng containerd chiếm khoảng 100 MB trong đó. Bạn cũng cần để trống một phần memory cho page cache và cho mức tăng đột biến khi chạy image build hoặc database dump.

ChartRAM budget by plan size, megabytes
The data behind this chart
[
  {
    "plan": "2 GB VPS",
    "total_mb": 2048,
    "host_reserve_mb": 768,
    "container_mb": 1280
  },
  {
    "plan": "4 GB VPS",
    "total_mb": 4096,
    "host_reserve_mb": 1024,
    "container_mb": 3072
  },
  {
    "plan": "8 GB VPS",
    "total_mb": 8192,
    "host_reserve_mb": 1536,
    "container_mb": 6656
  }
]

host_reserve_mb bao gồm hệ điều hành, Docker daemon và phần headroom giúp máy vẫn phản hồi tốt khi có tải. Phần còn lại là container_mb, và đó là toàn bộ dung lượng bạn có thể phân bổ. Phần dự phòng tăng theo gói VPS, từ 768 MB trên máy nhỏ nhất đến 1536 MB trên máy lớn nhất, vì máy lớn hơn chạy nhiều container hơn, ghi nhiều log hơn và cần nhiều page cache hơn.

Gói 2 GB còn 1280 MB cho các container. Dành 512 MB trong số đó cho PostgreSQL và 128 MB cho Traefik là bạn đã dùng hết một nửa. Phần còn lại đủ cho 2 ứng dụng nhỏ, mỗi ứng dụng khoảng 256 MB. Đây là một server thực tế và hữu ích. Nhưng không đủ chỗ để chạy thêm Nextcloud và một search cluster.

Gói 4 GB còn 3072 MB, đủ để chạy đồng thời một database, một reverse proxy, 3 ứng dụng và một monitoring container. Đây là cấu hình nhỏ nhất đáng dùng cho các dịch vụ quan trọng, vì phần memory dư mới là thứ hấp thụ được tác động của một lần deploy lỗi.

Gói 8 GB còn 6656 MB trong tổng số 8192 MB, và giới hạn thường chuyển từ memory sang CPU hoặc disk throughput. Nếu tính toán cho thấy stack của bạn không vừa, hãy mua gói lớn hơn thay vì cố tối ưu để phù hợp: chi phí thực tế của một VPS giải thích giá trị mỗi tháng của phần dung lượng bổ sung.

Hai quy tắc giúp phép tính luôn chính xác. Hãy đặt memory limit cho mọi service để một process chạy mất kiểm soát không kéo sập cả máy. Đồng thời không dùng hết ngân sách, vì docker compose buildpg_dump đều cần memory vào đúng thời điểm tải cao nhất. Memory limit trong Docker Compose trình bày cú pháp và các lỗi thường gặp.

Tại sao container của tôi thoát với mã 137?

Vì kernel đã kill container. 137 bằng 128 cộng 9, và signal 9 là SIGKILL. Container yêu cầu nhiều memory hơn mức được phép, nên OOM killer đã kết thúc container.

CONTAINER ID   IMAGE         STATUS
9f2c1a4d7b3e   postgres:16   Exited (137) 4 minutes ago

Hãy xác nhận nguyên nhân thay vì đoán:

docker inspect my-db | grep -i oomkilled
sudo journalctl -k | grep -i "out of memory"

"OOMKilled": true có nghĩa là container đã chạm giới hạn cgroup của chính nó, và log của kernel ghi rõ process được chọn:

Memory cgroup out of memory: Killed process 2417 (postgres) total-vm:1284416kB

Đây là trường hợp tốt vì sự cố chỉ ảnh hưởng đến một container. Trường hợp xấu là container không có giới hạn nào. Khi đó giới hạn trên của container là toàn bộ máy chủ. Memory leak trong một service sẽ làm host cạn memory, rồi kernel chọn một process để kill dựa trên kích thước trong toàn hệ thống. Dòng log không còn prefix Memory cgroup và có dạng Out of memory: Killed process 2417 (postgres). Process được chọn thường là database của bạn, còn container bị leak vẫn tiếp tục chạy. Vì vậy, việc đặt giới hạn cho mọi service quan trọng hơn giá trị chính xác của từng giới hạn riêng lẻ.

Swap thay đổi thời điểm xảy ra sự cố, không thay đổi phép tính. Phần lớn image VPS không có swap. Kiểm tra bằng swapon --show. Lệnh này không in gì nếu máy không có swap. Swap file cung cấp nơi để kernel chuyển các page ít được dùng, giúp bạn có thêm vài phút để phát hiện sự cố.

sudo fallocate -l 2G /swapfile
sudo chmod 600 /swapfile
sudo mkswap /swapfile
sudo swapon /swapfile
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
free -h

free -h lúc này phải hiển thị tổng Swap khác 0 trong dòng Swap. Swap không bổ sung RAM. Máy luôn chịu memory pressure sẽ chậm đến mức bạn không thể SSH vào để xử lý, vì vậy hãy xem swap là vùng đệm cảnh báo và điều chỉnh lại sizing.

Vì sao UFW không chặn port Docker đã publish?

Vì traffic không đi qua chain mà UFW bảo vệ. Khi bạn publish một port bằng -p 5432:5432 hoặc một mục compose ports:, daemon sẽ ghi một rule DNAT (destination network address translation) vào bảng nat và một rule accept vào chain DOCKER riêng của nó. Packet gửi đến container được forward đến container đó thay vì được chuyển đến host, nên đi qua luồng FORWARD và không bao giờ đi qua các rule INPUT do UFW tạo.

Bạn có thể theo dõi việc này trên server:

sudo ufw status | grep 5432
sudo iptables -t nat -L DOCKER -n | grep 5432

UFW có thể in 5432 DENY IN Anywhere trong khi bảng nat chứa một rule DNAT tcp ... to:172.18.0.2:5432 cho cùng port đó. Từ một máy khác, nc -vz your.server.ip 5432 vẫn kết nối được. Database đang public trên Internet dù firewall cho biết port này không mở.

Cách xử lý là publish ít port hơn. Các container trong cùng một project compose dùng chung network và có thể truy cập nhau bằng service name, nên database chỉ phục vụ application bên cạnh không cần mục ports:. Khi cần truy cập cục bộ, hãy bind port publish vào loopback:

services:
  db:
    image: postgres:16
    ports:
      - "127.0.0.1:5432:5432"

Sau docker compose up -d, nc -vz your.server.ip 5432 từ bên ngoài sẽ fail, còn psql -h 127.0.0.1 -p 5432 trên máy vẫn hoạt động. Với một stack nhỏ được cấu hình đúng, chỉ reverse proxy publish port, tại 80 và 443. Vì sao port Docker đã publish vượt qua UFW giải thích chain DOCKER-USER trong trường hợp bạn bắt buộc phải publish port nhưng vẫn muốn filter traffic, còn Cơ bản về firewall UFW trình bày các rule của host ở bên dưới.

Tại sao các container của tôi biến mất sau khi reboot?

Vì không có cấu hình nào yêu cầu chúng khởi động lại. Container được tạo với restart policy no nếu bạn không chỉ định policy khác. Vì vậy, sau khi reboot, container dừng lại và daemon không tự xử lý gì thêm. Reboot không hiếm trên VPS: cập nhật kernel từ unattended upgrades, bảo trì của nhà cung cấp và chuỗi OOM ở trên đều có thể dẫn đến reboot.

Có 2 điều kiện cần được đáp ứng. Daemon phải khởi động khi boot:

systemctl is-enabled docker

Lệnh này in ra enabled trên bản Ubuntu mặc định. Sau đó, mỗi service cần có một policy:

services:
  app:
    image: ghcr.io/example/app:1.4
    restart: unless-stopped

unless-stopped khởi động lại container sau reboot và tôn trọng container mà bạn đã chủ động dừng. always cũng khởi động lại các container bạn đã cố ý dừng mỗi khi daemon khởi động lại. Điều này có thể gây bất ngờ khi bạn đang debug. Chỉ sửa file là chưa đủ, vì restart policy được thiết lập khi container được tạo. Chạy docker compose up -d để tạo lại container, rồi kiểm tra giá trị đang được áp dụng:

docker inspect my-app | grep -A3 RestartPolicy

Sau đó, chủ động reboot máy và chạy docker compose ps trong thư mục project. Stack vượt qua được một lần reboot có kế hoạch thì cũng sẽ vượt qua reboot ngoài kế hoạch. Nếu stack cần bảo đảm thứ tự khởi động hoặc cần chạy một one-shot job khi boot, systemd unit là công cụ phù hợp hơn: khởi động Docker Compose khi boot có sẵn unit file. Để biết container đã khởi động lại có thực sự phục vụ request hay không, hãy thêm healthcheck cho Compose.

Vì sao ổ đĩa trên VPS của tôi bị đầy?

Vì Docker giữ lại mọi thứ cho đến khi bạn yêu cầu nó dọn. Mọi image tag bạn từng pull, mọi container đã dừng, mọi anonymous volume bị bỏ lại sau khi recreate và mọi layer trong build cache đều tiếp tục chiếm dung lượng trên ổ đĩa. Với root filesystem 40 GB hoặc 80 GB, mức dung lượng thường gặp ở các plan này, tình trạng đó có thể gây outage chỉ sau vài tháng thay vì vài năm.

Ổ đĩa đầy không giống một vụ crash. Trong cùng một giờ, bạn có thể nhận no space left on device từ container, từ apt, từ journald và từ docker pull. PostgreSQL dừng nhận thao tác ghi. Máy chủ vẫn hoạt động, nên vấn đề khó nhận ra hơn một vòng lặp reboot.

Hãy kiểm tra trước khi xóa:

docker system df
df -h /

docker system df chia tổng dung lượng thành images, containers, local volumes và build cache, đồng thời hiển thị cột RECLAIMABLE bên cạnh từng mục. Trên máy chủ tự build image, build cache thường là mục chiếm nhiều dung lượng nhất.

docker image prune -a
docker builder prune
docker system df

docker image prune -a xóa mọi image không được container nào sử dụng. docker builder prune xóa build cache. Cả hai thao tác đều an toàn khi service đang chạy, vì các tài nguyên đang được sử dụng sẽ được bỏ qua. Thao tác không an toàn là docker system prune --volumes, vì lệnh này xóa mọi volume hiện không được container nào tham chiếu. Một stack bạn dừng vào cuối tuần có đúng trạng thái đó, và database volume của stack cũng sẽ bị xóa. Đọc bind mounts và named volumes trước khi dùng flag đó, đồng thời tạo backup trước.

Container logs là nguồn tăng dung lượng âm thầm hơn. Driver mặc định json-file không có giới hạn kích thước, nên một container ghi log quá nhiều có thể ghi hàng gigabyte vào /var/lib/docker/containers. Đặt giới hạn cho mọi container trong /etc/docker/daemon.json:

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

Áp dụng thay đổi bằng sudo systemctl restart docker. Thao tác này restart các container, nên hãy chọn thời điểm phù hợp. Giới hạn chỉ áp dụng cho các container được tạo sau khi thay đổi. Vì vậy, hãy recreate các container đang chạy bằng docker compose up -d --force-recreate rồi xác nhận:

docker inspect my-app | grep -A5 LogConfig
sudo du -sh /var/lib/docker/containers

Output của inspect phải hiển thị max-size đã được thiết lập. Nếu trường này trống, container đó được tạo trước khi thay đổi và vẫn ghi log mà không có giới hạn.

Thói quen giúp một máy Docker nhỏ hoạt động ổn định

Bạn không cần dashboard hay công cụ mới phải học để làm những việc này.

  • Chạy docker system dfdf -h / vào ngày đầu tiên mỗi tháng. Hai lệnh, 30 giây, và bạn thấy xu hướng từ lâu trước khi nó trở thành sự cố gián đoạn dịch vụ.
  • Đặt memory limit cho mọi service, kể cả những service bạn chắc chắn là nhỏ. Giới hạn này biến sự cố toàn máy chủ thành việc chỉ cần restart một container.
  • Monitor máy chủ từ một nơi khác để nhận cảnh báo về áp lực memory hoặc disk trước khi kernel xử lý. Uptime Kuma chạy trong một container và khi idle chỉ dùng khoảng 95 MB.
  • Backup volume, không backup container. Container có thể bỏ đi, còn volume thì không. backup bằng restic trên VPS bao gồm lịch backup và kiểm tra restore.
  • Pin image tag trong file compose và cập nhật vào ngày bạn chọn. Với latest, version bạn nhận được từ docker compose pull tiếp theo là version được release vào sáng hôm đó.

Một VPS nhỏ chạy Docker có thể hoạt động ổn định trong nhiều năm nếu 4 con số luôn nằm trong giới hạn: memory budget, danh sách port được publish, restart policy của từng service và dung lượng disk còn trống. Mọi thứ khác vẫn là Docker mà bạn đã chạy ở nhà.

FAQ

Tôi cần bao nhiêu RAM để chạy Docker trên VPS?

Bản thân Docker tiêu tốn ít tài nguyên. Docker daemon và containerd cộng lại khoảng 100 MB, còn phần yêu cầu còn lại phụ thuộc vào các container. Trước tiên, hãy dành phần tài nguyên cho host: 768 MB trên máy 2048 MB cho hệ điều hành, daemon và phần dự phòng. Như vậy còn 1280 MB cho các container. Một database ở mức 512 MB, một reverse proxy ở mức 128 MB và hai ứng dụng nhỏ có thể chạy trong mức này. Hãy đo stack thực tế bằng docker stats --no-stream thay vì tin vào một con số được công bố.

Tôi có thể chạy Docker trên VPS 1 GB không?

Có, nếu chỉ chạy một hoặc hai container nhẹ. Hãy tạo swap file trước khi bắt đầu. Khoảng một nửa máy 1 GB sẽ được sử dụng sau khi hệ điều hành và Docker daemon chạy, nên phần còn lại đủ cho một ứng dụng nhỏ và một reverse proxy, nhưng không đủ cho database khi có tải thực tế. Việc build image trên máy có dung lượng như vậy có thể fail hoặc làm chết tiến trình khác. Hãy build ở nơi khác rồi pull image đã hoàn tất.

UFW có bảo vệ Docker container không?

Không bảo vệ các port bạn publish. Docker tự ghi các rule DNAT và forward, nên packet gửi đến port của container đã publish sẽ được forward đến container thay vì chuyển đến host. Vì vậy, các rule INPUT do UFW quản lý không nhìn thấy packet đó. ufw deny 5432 vẫn có thể đang active trong khi port đó vẫn nhận kết nối từ Internet. Hãy publish vào loopback bằng 127.0.0.1:5432:5432, không publish các service nội bộ, hoặc lọc trong chain DOCKER-USER.

Container có tự restart sau khi VPS reboot không?

Chỉ khi container được tạo với restart policy. Hãy đặt restart: unless-stopped cho từng service, chạy docker compose up -d để tạo lại các container với policy này, rồi xác nhận systemctl is-enabled docker in ra enabled. Sau đó, chủ động reboot và kiểm tra docker compose ps. Một restart policy chưa từng được kiểm thử thì chưa thể xem là restart policy đáng tin cậy.

Tôi nên prune Docker image bao lâu một lần?

Mỗi tháng một lần là đủ với hầu hết server nhỏ. Bạn cũng có thể chạy khi docker system df báo có dung lượng có thể reclaim mà bạn cần. docker image prune -adocker builder prune đều an toàn khi service đang chạy, vì các image và cache đang được sử dụng sẽ được bỏ qua. Tránh dùng docker system prune --volumes nếu bạn không biết chính xác volume nào không còn được tham chiếu, vì lệnh này sẽ xóa dữ liệu của mọi stack đang bị dừng.