只要从 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 客服(工单),需要人工处理的问题会由工程师跟进。