SSD Nodes Learn Hosting plans →
Hướng dẫn Matt ConnorBởi Matt Connor · Cập nhật ngày 2026-08-22

Docker trên VPS thay đổi gì so với laptop?

Docker trên VPS vẫn dùng cùng engine, nhưng RAM có thể cạn, port publish bỏ qua UFW, container không tự chạy sau reboot và disk nhanh đầy.

Những gì thay đổi khi chạy Docker trên VPS

Docker trên VPS sử dụng cùng engine và cùng image như Docker trên laptop, vì vậy 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, firewall không bị ai quét, và disk đủ lớn nên bạn không cần theo dõi. Server thuê có giới hạn memory cố định, địa chỉ IP public bị quét chỉ vài phút sau khi boot, và root filesystem sẽ bị Docker 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 xử lý 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 dưới đây nêu tên lỗi, chuỗi bạn thực sự 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 đa số người dùng. Container là một tiến trình trong cgroup (control group), không phải máy ảo, nên không có guest kernel và cũng không có mức cấp phát cố định. Chi phí RAM phụ thuộc vào những gì tiến trình bên trong thực sự truy cập. Vì vậy, một full stack có thể chạy vừa trong 2 GB, trong khi cùng stack đó nếu xây dựng bằng máy ảo thì không.

Các số liệu dưới đây là mức idle điển hình 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à số liệu 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 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à lượng RAM container sử dụng khi không làm gì. budget_mb là lượng RAM cần dành ra khi lập kế hoạch, vì workload thực tế không ở trạng thái idle. PostgreSQL idle ở mức gần 45 MB 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 budget. Hãy debug theo cột idle.

Hãy chú ý đến đặc điểm của 7 dòng đó. nginx idle ở mức 8 MB và Nextcloud ở mức 210 MB. Proxy đứng trước các ứng dụng gần như không tốn RAM. 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 chủ.

Một cảnh báo về docker stats: số liệu memory bao gồm page cache do các thao tác đọc file của chính container đưa vào, nên sẽ tăng trong một 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.

Sizing 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 RAM dành cho host. Kernel, systemd, journald, sshd và Docker daemon đều dùng chung RAM với các container, và dockerd cùng containerd chiếm khoảng 100 MB trong số đó. Bạn cũng cần chừa RAM trống cho page cache và cho các đợt tăng đột biến khi image build hoặc database dump chạy.

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 dự phòng giúp máy vẫn phản hồi khi tải tăng. Phần còn lại là container_mb, và đó là toàn bộ dung lượng bạn được phép phân bổ. Phần dự phòng tăng theo gói dịch vụ, từ 768 MB trên máy nhỏ nhất lê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 thì 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. Dung lượng này đủ cho một database, một reverse proxy, 3 ứng dụng và một monitoring container chạy đồng thời. Đây là kích thước nhỏ nhất đáng dùng cho các dịch vụ quan trọng, vì phần RAM dư là nơi hấp thụ tác động của một lần deploy lỗi.

Gói 8 GB còn 6656 MB trên tổng số 8192 MB, và giới hạn thường chuyển từ RAM sang CPU hoặc tốc độ ghi đọc disk. Một số loại container tự xác định kích thước từ cấu hình thay vì từ tải: local model server cấp phát KV cache theo context window, nên tăng num_ctx của Ollama có thể làm ngân sách tăng thêm hàng GB trước khi có request nào đến. Nếu phép tính cho thấy stack của bạn không vừa, hãy mua gói lớn hơn thay vì cố tinh chỉnh để vừa đủ: chi phí thực tế của một VPS cho biết thêm mỗi GB có giá trị bao nhiêu mỗi tháng.

Có 2 quy tắc giúp phép tính luôn chính xác. Đặt memory limit cho mọi service để một tiến trình chạy mất kiểm soát không kéo sập cả máy. Đồng thời, không phân bổ hết ngân sách, vì docker compose build và pg_dump đều cần RAM vào 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.

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

Vì kernel đã kill nó. 137 là 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 nó.

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 tên process mà nó đã 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ó limit nào. Khi không có limit, mức trần của nó là toàn bộ máy chủ. Vì vậy, 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 mất tiền tố 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, trong khi container bị leak vẫn tiếp tục chạy. Vì vậy, việc đặt limit cho mọi service quan trọng hơn việc chọn đúng giá trị cho bất kỳ limit riêng lẻ nào.

Swap thay đổi thời điểm xảy ra sự cố, không thay đổi phép tính. Hầu hết image VPS không có swap. Kiểm tra bằng swapon --show; lệnh này không in gì khi máy không có swap. Swap file cung cấp cho kernel một nơi để đưa các page ít được dùng vào, 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 làm tăng RAM. Máy chủ chịu áp lực memory liên tục sẽ chậm đến mức bạn không thể SSH vào để xử lý, vì vậy hãy xem swap là bộ đệm cảnh báo và điều chỉnh lại cấu hình tài nguyên.

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 khai báo ports: trong compose, daemon 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ì chuyển vào 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ể hiển thị 5432 DENY IN Anywhere trong khi bảng nat chứa 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 đó không được phép.

Cách xử lý là hạn chế việc publish port. Các container trong cùng một compose project dùng chung network và có thể kết nối với nhau bằng service name, nên database chỉ phục vụ application bên cạnh nó không cần khai báo 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ẽ thất bại, còn psql -h 127.0.0.1 -p 5432 trên host vẫn hoạt động. Trong một stack nhỏ được cấu hình đúng, chỉ reverse proxy publish port, thường là 80 và 443. Vì sao port Docker đã publish bypass 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 Kiến thức cơ bản về firewall UFW giải thích các rule của host bên dưới.

Tại sao các container 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 đặt policy khác, nên sau khi reboot, container sẽ dừng và daemon không tự xử lý gì thêm. Reboot không hiếm trên VPS: kernel update từ unattended upgrades, hoạt động bảo trì của provider và chuỗi OOM ở trên đều có thể dẫn đến reboot.

Có 2 điều phải đúng. Daemon phải khởi động cùng hệ thống:

systemctl is-enabled docker

Lệnh này in ra enabled trên bản cài 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à vẫn tôn trọng container mà bạn đã chủ động dừng. always cũng khởi động lại các container mà 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 đang debug. Chỉ sửa file là chưa đủ, vì restart policy được đặt 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ó hiệu lực:

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 một lần reboot có kế hoạch thì cũng sẽ vượt qua reboot ngoài dự kiến. Nếu stack cần bảo đảm thứ tự khởi động hoặc một job chạy một lần khi boot, systemd unit là công cụ phù hợp hơn: khởi động Docker Compose cùng hệ thống có file unit. Để 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 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 đã stop, mọi anonymous volume còn sót lại sau khi recreate và mọi layer trong build cache đều vẫn chiếm chỗ trên ổ đĩa. Với root filesystem 40 GB hoặc 80 GB, vốn là mức dung lượng thường gặp ở các gói này, dữ liệu đó có thể gây outage chỉ sau vài tháng thay vì vài năm.

Ổ đĩa đầy không biểu hiện giống một vụ crash. Trong cùng một giờ, bạn có thể nhận no space left on device từ một container, từ apt, từ journald và từ docker pull. PostgreSQL ngừng nhận các thao tác ghi. Máy chủ vẫn đang chạy, nên vấn đề này khó nhận biết 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 lớn 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 vẫn đang chạy, vì các tài nguyên đang được sử dụng sẽ bị bỏ qua. 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 stop vào cuối tuần có đúng trạng thái đó, và database volume của stack sẽ bị xóa theo. Hãy đọc bind mount và named volume trước khi dùng flag đó, đồng thời backup trước.

Container log là nguồn tăng dung lượng âm thầm hơn. Driver mặc định json-file không giới hạn kích thước, nên một container nhiều log có thể ghi hàng gigabyte vào /var/lib/docker/containers. Hãy giới hạn dung lượng 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. Lệnh 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 giá trị này trống, container đó được tạo trước khi thay đổi và vẫn đang ghi log mà không có giới hạn.

Các thói quen giúp một máy Docker nhỏ luôn ổ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 df và df -h / vào ngày đầu tiên mỗi tháng. Hai lệnh, 30 giây, và bạn sẽ thấy xu hướng từ lâu trước khi nó gây outage.
  • Đặt memory limit cho mọi service, kể cả những service bạn chắc chắn là nhỏ. Limit biến outage trên toàn host thành việc chỉ cần restart một container.
  • Monitor máy từ một nơi khác để nhận biết áp lực memory hoặc disk trước khi kernel can thiệp. 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 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 chúng vào ngày bạn chọn. Với latest, version 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ể ổn định trong nhiều năm nếu 4 con số luôn nằm trong giới hạn: ngân sách memory, 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 không tốn nhiều tài nguyên. daemon và containerd cộng lại chỉ khoảng 100 MB; phần còn lại phụ thuộc vào các container của bạn. Trước tiên, hãy dành riêng phần tài nguyên cho host: 768 MB trên máy 2048 MB cho operating system, daemon và phần dự phòng. Như vậy còn 1280 MB cho các container. Một database dùng 512 MB, một reverse proxy dùng 128 MB và 2 ứng dụng nhỏ có thể chạy trong mức này. Hãy đo stack của bạn bằng docker stats --no-stream thay vì tin vào bất kỳ con số công bố nào.

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

Có, nếu chỉ chạy 1 hoặc 2 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 dùng sau khi operating system và Docker daemon chạy, nên vẫn đủ chỗ 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ế. Build image trên máy có dung lượng như vậy có thể bị fail hoặc làm tiến trình khác bị kill. Vì vậy, 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 áp dụng cho các port bạn publish. Docker tự thêm các rule DNAT và forward. Vì vậy, packet gửi đến port của container đã publish sẽ được forward đến container thay vì chuyển đến host, và các rule INPUT do UFW quản lý sẽ không 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 filter trong chain DOCKER-USER.

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

Chỉ khi container được tạo với restart policy. Đặt restart: unless-stopped cho từng service, chạy docker compose up -d để tạo lại các container với policy đó, 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 test 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ể thực hiện khi docker system df báo có dung lượng có thể reclaim mà bạn cần. docker image prune -a và docker 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 docker system prune --volumes trừ khi bạn 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.