只要從 Docker 官方源安裝、限制容器日誌、為每個容器設定記憶體上限、把連接埠釋出在 127.0.0.1 並由反向代理對外(否則 Docker 會繞過 UFW 和 firewalld),再定期清理舊映象,Docker 在 1 GB 記憶體的 VPS 上也能穩定執行。本文按步驟介紹在 Debian 12/13、Ubuntu 22.04/24.04 和 Rocky Linux / AlmaLinux 上的做法。
步驟 1:從官方源安裝 Docker
Debian 和 Ubuntu 自帶的 docker.io 軟體包往往版本較舊,也沒有 Compose 外掛,請改用 Docker 官方源。在 Debian 和 Ubuntu 上以 root 身份執行:
# Debian 12/13 and Ubuntu 22.04/24.04 - remove distro packages that conflict
for p in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do
apt-get remove -y "$p" 2>/dev/null
done
apt-get update
apt-get install -y ca-certificates curl
install -m 0755 -d /etc/apt/keyrings
. /etc/os-release # sets ID (debian/ubuntu) and VERSION_CODENAME
curl -fsSL "https://download.docker.com/linux/$ID/gpg" -o /etc/apt/keyrings/docker.asc
chmod a+r /etc/apt/keyrings/docker.asc
cat > /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/$ID
Suites: $VERSION_CODENAME
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF
apt-get update
apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
部分 Rocky Linux 和 AlmaLinux 映象預裝了 Podman,會與 Docker 的軟體包衝突,請先解除安裝:
# Rocky Linux / AlmaLinux 8, 9, 10
dnf remove -y podman buildah runc 2>/dev/null
dnf install -y dnf-plugins-core
dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
dnf install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
systemctl enable --now docker
在 Debian 和 Ubuntu 上,服務會自動啟動。檢查一切是否正常:
systemctl is-active docker
docker version
docker compose version
docker run --rm hello-world
docker 組的成員可以控制 Docker 守護程序,這等同於擁有伺服器的 root 許可權。只用 usermod -aG docker username 新增可信使用者,並且絕不要把 Docker API(/var/run/docker.sock 或 TCP 2375 連接埠)暴露到公網。步驟 2:限制容器日誌並開啟 live-restore
預設情況下,Docker 會儲存容器輸出的每一行日誌,且不限大小。一個日誌量大的容器幾天就能寫滿 20 GB 硬碟。請在 /etc/docker/daemon.json 中設定全侷限制:
# /etc/docker/daemon.json
{
"log-driver": "local",
"log-opts": {
"max-size": "10m",
"max-file": "3"
},
"live-restore": true
}
local日誌驅動:壓縮並輪轉日誌檔案,這裡每個容器最多 3 × 10 MB,docker logs照常可用。live-restore:Docker 守護程序重啟或升級時,容器繼續執行,軟體包更新不會讓網站中斷。
如果伺服器上有工具需要直接讀取 JSON 日誌檔案,可改用 json-file,限制相同:
"log-driver": "json-file",
"log-opts": { "max-size": "10m", "max-file": "3" },
校驗配置檔案並重啟 Docker。這第一次重啟會讓正在執行的容器停止一次:
dockerd --validate --config-file /etc/docker/daemon.json
systemctl restart docker
docker info --format '{{.LoggingDriver}} live-restore={{.LiveRestoreEnabled}}'
docker compose up -d --force-recreate 重新建立現有容器。步驟 3:編寫帶記憶體和 CPU 限制的 compose 檔案
不設限制時,一個記憶體洩漏的容器就可能讓整臺 VPS 瘋狂使用 swap,或讓核心殺掉 sshd 甚至資料庫。設定上限後,只有這個容器會被殺掉並重啟。在 1 GB 方案上執行一個小應用加 MariaDB,可以這樣寫:
# /opt/app/compose.yaml
services:
app:
image: ghcr.io/example/app:1.4.2 # pin a version, avoid :latest
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080" # reachable only from the VPS itself
mem_limit: 256m
memswap_limit: 256m # no extra swap for this container
cpus: 1.0
pids_limit: 200
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:8080/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 30s
depends_on:
db:
condition: service_healthy
db:
image: mariadb:11.4
restart: unless-stopped
command: ["--innodb-buffer-pool-size=64M"]
environment:
MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MARIADB_DATABASE: app
volumes:
- db_data:/var/lib/mysql
mem_limit: 300m
memswap_limit: 300m
healthcheck:
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
interval: 30s
timeout: 5s
retries: 5
start_period: 60s
volumes:
db_data:
mem_limit/memswap_limit:記憶體硬上限。兩者設為相同值,容器就不會使用 swap。請給系統留出空間:1 GB 記憶體時,所有限制加起來控制在 600–700 MB 左右。cpus:1.0表示一個完整核心。這樣即使某個容器很忙,也不會把整臺 VPS 推過合理使用規則中的 CPU 平均值。restart: unless-stopped:容器崩潰或伺服器重啟後會自動恢復,但你主動停止的容器不會被拉起。healthcheck:把容器標記為 healthy 或 unhealthy,depends_on會等待資料庫就緒。檢查命令(這裡是wget)必須存在於映象中。注意:單純的 Docker 不會重啟 unhealthy 的容器,只會顯示狀態。
把密碼等敏感資訊放在 compose.yaml 旁邊的 .env 檔案中,然後啟動:
cd /opt/app
echo 'DB_ROOT_PASSWORD=REPLACE_WITH_A_LONG_RANDOM_PASSWORD' > .env
chmod 600 .env
docker compose up -d
docker compose ps # STATUS should become "healthy"
docker stats --no-stream # memory use against each limit
步驟 4:避開防火牆繞過陷阱(UFW、firewalld 與 Docker)
這是使用 Docker 時最常見的安全錯誤。用 -p 8080:8080 或 "8080:8080" 釋出連接埠時,Docker 會新增自己的 NAT 和轉發規則。這些流量根本不經過 UFW 和 firewalld 管理的 INPUT 規則,所以即使 ufw status 顯示已攔截,連接埠實際上仍對整個公網開放。
按推薦順序,解決辦法如下:
- 釋出在 127.0.0.1 上(
"127.0.0.1:8080:8080",即上面 compose 檔案的寫法)。連接埠只能從 VPS 本機訪問。 - 前面加一層反向代理。主機上的 Nginx 或 Caddy 監聽 80/443(照常在 UFW 或 firewalld 中放行),再轉發到
127.0.0.1:8080,HTTPS 也能集中在一處配置。 - 內部服務完全不要釋出連接埠。資料庫和快取只需要 Compose 網路,同一專案的容器可透過服務名互相訪問(
db:3306)。
為應用配置一個最簡單的 Nginx 站點(HTTPS 可按Nginx 與 Certbot 教程用 Let's Encrypt 新增):
# /etc/nginx/sites-available/app.example.com (Rocky/Alma: /etc/nginx/conf.d/app.conf)
server {
listen 80;
server_name app.example.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
檢查實際對外暴露了哪些連接埠:
# On the VPS: what is published, and on which address?
docker ps --format '{{.Names}}\t{{.Ports}}'
ss -tlnp | grep docker-proxy
# From ANOTHER machine: this must fail for a port bound to 127.0.0.1
nc -vz 203.0.113.10 8080
如果某個連接埠確實必須公開,但只允許特定地址訪問,可以在 DOCKER-USER 鏈中新增規則,Docker 會先於自己的規則檢查這條鏈。--ctorigdstport 匹配的是客戶端傳送時的原始連接埠,也就是 Docker 地址轉換之前的連接埠:
# Allow new connections to published port 8080 only from 203.0.113.0/24
iptables -I DOCKER-USER -i eth0 ! -s 203.0.113.0/24 -p tcp \
-m conntrack --ctstate NEW --ctorigdstport 8080 -j DROP
iptables -L DOCKER-USER -n -v --line-numbers
DOCKER-USER 中的規則不會自動儲存,重啟後就會丟失,寫錯規則還可能讓容器斷網。綁定到 127.0.0.1 更簡單,重啟後依然有效。主機防火牆本身的配置請參閱 UFW 與 firewalld 防火牆教程。步驟 5:保持磁碟整潔
在 10–40 GB 的硬碟上,舊映象、已停止的容器和構建快取很快就會堆積起來。先看看 Docker 佔用了多少空間:
docker system df # images, containers, volumes, build cache
docker system df -v # per image / container / volume
df -h / && du -sh /var/lib/docker
然後由安全到謹慎,逐步清理:
# Safe: stopped containers, unused networks, dangling images, build cache
docker system prune
# Unused images (not used by any container) older than 7 days
docker image prune -a --filter "until=168h"
# Build cache only
docker builder prune
# CAREFUL: deletes volumes not used by any container - this is data
docker volume ls
docker volume prune
docker volume prune 和 docker system prune --volumes 會刪除當時沒有被任何容器使用的卷。如果你停止並刪除了資料庫容器,它的卷就算作未使用,資料會隨之消失。務必先執行 docker volume ls,並備份需要的內容。每週自動清理一次,且永遠不碰資料卷:
# /etc/cron.d/docker-prune - weekly, never touches volumes
0 4 * * 0 root docker image prune -af --filter "until=168h" >/dev/null && docker builder prune -f --filter "until=168h" >/dev/null
如果硬碟已經滿了,請參閱如何查詢並釋放磁碟空間。
步驟 6:在 512 MB 和 1 GB 方案上使用 swap 或 zram
多個容器同時出現記憶體峰值時,少量 swap 可以避免小記憶體 VPS 突然因記憶體不足而殺程序。經典做法是在硬碟上建 swap 檔案(參閱如何新增 swap 空間)。在 Debian 和 Ubuntu 上,還可以加上 zram:它把壓縮後的 swap 放在記憶體裡,速度快得多,也不產生磁碟 I/O:
# Debian / Ubuntu: compressed swap in RAM
apt install -y zram-tools
cat > /etc/default/zramswap <<'EOF'
ALGO=zstd
PERCENT=50
PRIORITY=100
EOF
systemctl restart zramswap
swapon --show
# Prefer RAM, swap only under pressure
echo 'vm.swappiness=10' > /etc/sysctl.d/90-swappiness.conf
sysctl --system
Rocky Linux 和 AlmaLinux 請使用 swap 檔案。swap 只是安全網,不是額外記憶體:如果 free -h 顯示 swap 一直在被使用,說明當前方案對你的容器來說太小了。
步驟 7:安全地更新容器
固定映象版本,不要用 :latest。這樣只有在你決定時才會更新,而且隨時知道該回滾到哪個版本:
cd /opt/app
# 1. Back up first (database dump; replace "app" with your database)
docker compose exec -T db sh -c 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --single-transaction app' > /root/app-$(date +%F).sql
# 2. Change the image tag in compose.yaml (e.g. 1.4.2 -> 1.5.0), then:
docker compose pull
docker compose up -d
docker compose ps
docker compose logs --tail=50 app
# 3. Roll back if needed: put the old tag back and run "docker compose up -d"
Docker 引擎本身隨系統軟體包一起更新。有了 live-restore,升級期間容器會繼續執行:
apt-get update && apt-get install --only-upgrade -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# Rocky / AlmaLinux
dnf upgrade -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
FAQ
512 MB 方案夠跑 Docker 嗎?
跑一兩個輕量容器(靜態網站、小型代理或機器人)是夠的,前提是配好 swap 並嚴格限制記憶體。資料庫加 Web 應用則建議使用 1–2 GB 方案,會寬裕很多。
容器為什麼不停地重啟?
檢視 docker compose logs app 和 docker inspect $(docker compose ps -q app) --format '{{.State.OOMKilled}}'。如果結果是 true,說明容器觸及了記憶體上限:可以調高 mem_limit、降低應用的記憶體佔用,或在服務頁面升級方案。
JUSTG 能幫忙處理容器問題嗎?
VPS 本身的問題,例如網路或控制檯訪問,請透過工單聯絡 Technical support。JUSTG 提供 7×24 小時 AI 客服(工單),需要人工處理的問題會由工程師跟進。