Docker 在 VPS 上运行有哪些实际变化?
Docker 引擎和镜像基本不变,但 VPS 更容易遇到内存不足、已发布端口绕过 UFW、重启后容器不恢复,以及磁盘空间耗尽。
在 VPS 上运行 Docker 时会发生哪些变化
VPS 上的 Docker 使用与笔记本电脑上相同的引擎和镜像,因此您已经熟悉的命令仍然有效。变化在于运行环境。笔记本电脑通常有可用内存,防火墙也很少有人扫描,磁盘容量通常大到无需关注。租用的服务器则有固定的内存上限,公网 IP 地址在启动几分钟内就可能遭到扫描,而且 Docker 会在未提示的情况下逐渐占满根文件系统。
在小型服务器上,大多数问题都由以下 4 个差异引起:
- 内存有限,内核会通过终止进程来处理内存不足。
- 已发布的端口会直接绕过 UFW(uncomplicated firewall),因为 Docker 会自行写入防火墙规则。
- 除非提前配置,否则容器不会在重启后自动恢复。
- 镜像、容器、卷和构建缓存会不断增长,直到磁盘空间耗尽。
下面的每个部分都会说明故障、您实际会看到的字符串,以及深入解决该问题的指南。如果您还没有编写 compose 文件,请先阅读 VPS 上的 Docker Compose 基础,然后再回来。本页假定您已经能够启动整个服务栈。
Docker 容器使用多少 RAM?
低于大多数人的预期。容器是 cgroup(控制组)中的一个进程,而不是虚拟机,因此没有客户机内核,也没有固定的内存分配。它的开销取决于容器内进程实际使用的资源。因此,完整技术栈可以运行在 2 GB 内存中,而使用虚拟机构建的相同技术栈可能无法运行。
下面的数据是 Ubuntu 24.04 上使用默认配置运行标准镜像时的典型空闲值,来自启动几分钟后读取的 docker stats。这些数据可用于初步规划,不能作为你的工作负载基准。在采信任何数据(包括下列数据)前,请在自己的主机上运行 docker stats --no-stream。
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
}
]这两列的用途不同。idle_mb 表示容器空闲时的内存使用量。budget_mb 表示规划时应预留的内存,因为实际使用并不等于空闲状态。PostgreSQL 空闲时接近 45 MB,但连接、排序和缓存启用后需要 512 MB。请使用预算列进行规划,使用空闲列进行故障排查。
请注意这 7 行数据的分布。nginx 空闲时使用 8 MB,Nextcloud 使用 210 MB。应用前面的反向代理几乎不占用内存。规划主机资源时,应重点考虑数据库和 PHP 应用。
关于 docker stats,请注意:该内存数据包含容器自身读取文件时带入的页缓存,因此容器启动后的一段时间内数值会持续上升,随后趋于稳定。请监控一小时,再判断是否存在内存泄漏。
VPS 规格选择:2 GB、4 GB 和 8 GB 能容纳什么
首先扣除主机自身占用的内存。内核、systemd、journald、sshd 和 Docker daemon 与容器共享同一块 RAM,dockerd 和 containerd 会占用其中约 100 MB。您还需要为页缓存,以及镜像构建或数据库转储运行时的内存峰值预留空间。
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 包括操作系统、Docker daemon,以及确保主机在负载下仍能正常响应的余量。剩余部分是 container_mb,这才是您可以分配的内存。该预留量会随规格增加,从最小规格的 768 MB 增长到最大规格的 1536 MB,因为更大的主机会运行更多容器、写入更多日志,并需要更多页缓存。
2 GB 规格为容器留下 1280 MB。PostgreSQL 占用其中的 512 MB,Traefik 占用 128 MB,这部分内存已经用掉一半。剩余内存可供两个各约 256 MB 的小型应用使用。这是一台实际可用的服务器,但没有足够空间再运行 Nextcloud 和搜索集群。
4 GB 规格为容器留下 3072 MB,足以同时运行数据库、反向代理、3 个应用和一个监控容器。这是值得用于重要服务的最小规格,因为空闲内存可以吸收一次有问题的部署。
8 GB 规格在总计 8192 MB 中为容器留下 6656 MB。此时限制通常会从内存转移到 CPU 或磁盘吞吐量。某类容器会根据配置而不是负载确定内存大小:本地模型服务器会按照上下文窗口大小预留 KV cache,因此在收到任何请求前,增大 Ollama 的 num_ctx 就可能使内存预算增加数 GB。如果计算结果表明您的服务栈无法容纳,请直接购买更大的规格,不要试图通过调整配置勉强运行:VPS 的实际成本说明了每月增加这些 GB 内存的价值。
以下两条规则可以避免内存计算失真。为每个服务设置内存限制,避免单个失控进程拖垮整台主机。并且不要用尽预算顶部的内存,因为 docker compose build 和 pg_dump 都会在最需要的时候占用内存。Docker Compose 中的内存限制介绍了语法和常见问题。
为什么我的容器会以代码 137 退出?
因为内核终止了它。137 等于 128 加 9,而信号 9 是 SIGKILL。容器请求的内存超过了允许使用的上限,因此 OOM killer 终止了它。
CONTAINER ID IMAGE STATUS
9f2c1a4d7b3e postgres:16 Exited (137) 4 minutes ago先确认原因,不要凭猜测判断:
docker inspect my-db | grep -i oomkilled
sudo journalctl -k | grep -i "out of memory""OOMKilled": true 表示容器触及了自身的 cgroup 限制,内核日志还会记录它选择终止的进程:
Memory cgroup out of memory: Killed process 2417 (postgres) total-vm:1284416kB这是较好的情况,因为影响只会停留在一个容器内。较差的情况是容器完全没有限制。没有限制时,容器的上限就是整台机器的内存,因此一个服务发生内存泄漏就可能耗尽主机资源,随后内核会在整个系统中按进程大小选择终止对象。日志行会失去 Memory cgroup 前缀,并显示为 Out of memory: Killed process 2417 (postgres)。它选择的进程通常是数据库,而发生内存泄漏的容器仍会继续运行。因此,为每个服务设置限制比精确确定任何单个限制的值更重要。
Swap 会改变发生时间,但不会改变计算方式。大多数 VPS 镜像都未配置 Swap。使用 swapon --show 检查;没有 Swap 时,该命令完全不会输出内容。Swap 文件可以让内核将不常用的内存页写入其中,从而争取几分钟来发现问题。
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 应在 Swap 行中显示非零总量。Swap 不会增加 RAM。持续承受内存压力的主机会变慢到无法通过 SSH 登录修复,因此应将 Swap 视为告警缓冲,并修正内存配置。
为什么 UFW 不阻止我发布的 Docker 端口?
因为流量根本不会经过 UFW 保护的链。使用 -p 5432:5432 发布端口,或在 compose 中使用 ports: 条目时,daemon 会在 nat 表中写入 DNAT(目标网络地址转换)规则,并在自己的 DOCKER 链中写入 accept 规则。发往容器的数据包会转发到该容器,而不是交付给主机,因此会经过 FORWARD 路径,不会经过 UFW 写入的 INPUT 规则。
您可以在服务器上观察这一过程:
sudo ufw status | grep 5432
sudo iptables -t nat -L DOCKER -n | grep 5432即使 UFW 显示 5432 DENY IN Anywhere,nat 表中仍可能存在同一端口对应的 DNAT tcp ... to:172.18.0.2:5432 规则。从另一台机器上,nc -vz your.server.ip 5432 仍然可以连接。此时,数据库已暴露在公网,而防火墙却显示该端口未开放。
解决方法是减少发布的端口。同一个 compose 项目中的容器共享网络,并可通过服务名相互访问。因此,只为旁边的应用提供服务的数据库根本不需要 ports: 条目。如果需要本机访问,请将发布绑定到 loopback:
services:
db:
image: postgres:16
ports:
- "127.0.0.1:5432:5432"执行 docker compose up -d 后,外部的 nc -vz your.server.ip 5432 会失败,而主机上的 psql -h 127.0.0.1 -p 5432 仍然可以正常工作。在结构良好的小型服务栈中,只有反向代理发布端口,通常为 80 和 443。为什么 Docker 发布的端口会绕过 UFW介绍了必须发布端口但仍需对其进行过滤时使用的 DOCKER-USER 链,UFW 防火墙基础介绍了底层的主机规则。
重启后容器为什么消失了?
因为没有任何配置要求它们重新启动。除非设置重启策略,否则容器创建时使用重启策略 no。因此重启后容器会保持停止状态,daemon 也不会处理它。在 VPS 上,重启并不罕见:unattended upgrades 安装内核更新、服务商执行维护,以及前文所述的 OOM 处理流程,最终都可能导致重启。
以下两点必须同时满足。daemon 必须在启动时启动:
systemctl is-enabled docker在标准 Ubuntu 安装中,该命令会输出 enabled。然后为每个服务设置重启策略:
services:
app:
image: ghcr.io/example/app:1.4
restart: unless-stoppedunless-stopped 会在重启后重新启动容器,同时尊重您主动停止的容器。always 还会在 daemon 重启时重新启动您主动停止的容器,这会在调试过程中造成意外。仅编辑文件还不够,因为重启策略是在创建容器时设置的。运行 docker compose up -d 重新创建容器,然后检查当前值:
docker inspect my-app | grep -A3 RestartPolicy然后主动重启主机,并在项目目录中运行 docker compose ps。能够经受计划内重启的 stack,也能经受计划外重启。如果 stack 需要保证启动顺序,或需要在启动时运行一次性任务,systemd unit 是更合适的工具:在启动时启动 Docker Compose 包含该 unit 文件。若要确认重新启动的容器确实在提供服务,请添加 Compose 健康检查。
为什么我的 VPS 磁盘已满?
因为 Docker 会一直保留所有内容,除非您明确清理。您拉取过的每个镜像标签、每个已停止的容器、重建时遗留的每个匿名卷,以及每一层构建缓存,都会继续占用磁盘。对于 40 GB 或 80 GB 的根文件系统(这在此类套餐容量中很常见),磁盘可能在几个月内耗尽,而不是几年后才出现问题。
磁盘已满时,表现通常不像崩溃。您可能会在同一小时内,分别从容器、apt、journald 和 docker pull 收到 no space left on device。PostgreSQL 会停止接受写入。服务器仍然在线,因此比反复重启更难及时发现。
删除前先检查:
docker system df
df -h /docker system df 会将总用量拆分为镜像、容器、本地卷和构建缓存,并在每项旁边显示 RECLAIMABLE 列。在自行构建镜像的服务器上,构建缓存通常占用最多空间。
docker image prune -a
docker builder prune
docker system dfdocker image prune -a 会删除所有未被任何容器使用的镜像。docker builder prune 会清除构建缓存。服务运行时执行这两个命令通常是安全的,因为正在使用的内容会被跳过。docker system prune --volumes 则不安全,因为它会删除当前没有任何容器引用的所有卷。您为周末停止的堆栈正是这种情况,其数据库卷也会随之删除。输入该标志前,请先阅读 绑定挂载与命名卷,并先创建备份。
容器日志增长得更隐蔽。默认的 json-file 驱动没有大小限制,因此某个日志较多的容器可能会将数 GB 写入 /var/lib/docker/containers。请在 /etc/docker/daemon.json 中为每个容器设置上限:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "10m",
"max-file": "3"
}
}使用 sudo systemctl restart docker 应用设置。该命令会重启容器,因此请安排合适的时间执行。该上限只适用于设置更改后创建的容器,因此请使用 docker compose up -d --force-recreate 重建正在运行的容器,然后确认:
docker inspect my-app | grep -A5 LogConfig
sudo du -sh /var/lib/docker/containersinspect 输出应显示已设置 max-size。如果该项为空,说明该容器早于此设置创建,仍在无大小限制的情况下写入日志。
保持小型 Docker 主机健康的习惯
这些操作不需要仪表板,也不需要学习额外工具。
- 每月1日运行
docker system df和df -h /。只需执行两个命令、花费30秒,就能在问题导致服务中断前很久看到趋势。 - 为每个服务设置内存限制,包括那些您确定占用很小的服务。内存限制可将整台主机的中断限制为单个容器重启。
- 从其他位置监控这台主机,以便在内核采取措施前发现内存或磁盘压力。Uptime Kuma 在容器中运行,空闲时占用约 95 MB。
- 备份卷,而不是容器。容器可以丢弃,卷不能。VPS 上的 restic 备份涵盖备份计划和恢复测试。
- 在 compose 文件中固定镜像标签,并在您选择的日期更新它们。使用
latest时,下一个docker compose pull获取的版本就是当天发布的版本。
运行 Docker 的小型 VPS 如果以下4个数值保持在合理范围内,就能多年保持健康:内存预算、已发布端口列表、每个服务的重启策略,以及可用磁盘空间。其他方面与您已经在家中运行的 Docker 相同。
FAQ
运行 Docker 的 VPS 需要多少 RAM?
Docker 本身占用很少。daemon 和 containerd 合计约占 100 MB,其余需求取决于容器。先为主机预留资源:在 2048 MB 的主机上,为操作系统、daemon 和余量预留 768 MB,剩余 1280 MB 可供容器使用。一个占用 512 MB 的数据库、一个占用 128 MB 的反向代理,以及两个小型应用可以在此范围内运行。请使用 docker stats --no-stream 测量自己的服务栈,不要盲目采用已发布的数值。
我可以在 1 GB 的 VPS 上运行 Docker 吗?
可以运行一两个轻量级容器,但请在开始前添加 swap 文件。操作系统和 Docker daemon 运行后,1 GB 主机大约有一半内存会被占用,因此还可运行一个小型应用和反向代理,但无法承受实际负载下的数据库。在这种配置的主机上构建镜像可能失败,或导致其他服务被终止。因此应在其他主机上构建,再拉取构建完成的镜像。
UFW 能保护 Docker 容器吗?
不能保护已发布端口。Docker 会写入自己的 DNAT 和转发规则,因此,发往已发布容器端口的数据包会被转发到容器,而不会交给主机,UFW 管理的 INPUT 规则也看不到这些数据包。即使 ufw deny 5432 处于启用状态,该端口仍可能从互联网访问。使用 127.0.0.1:5432:5432 将端口发布到 loopback,不要发布内部服务,或在 DOCKER-USER 链中进行过滤。
VPS 重启后容器会自动重启吗?
只有在创建容器时设置了 restart policy 才会自动重启。为每个服务设置 restart: unless-stopped,运行 docker compose up -d,以便重新创建带有该策略的容器,并确认 systemctl is-enabled docker 输出 enabled。然后主动重启主机,再检查 docker compose ps。未经测试的 restart policy 不应视为可靠的 restart policy。
应多久清理一次 Docker 镜像?
对于大多数小型服务器,每月清理一次即可;如果 docker system df 报告可回收空间,而你确实需要这些空间,也可以立即清理。服务运行期间,docker image prune -a 和 docker builder prune 都是安全的,因为正在使用的镜像和缓存会被跳过。除非你明确知道哪些卷未被引用,否则不要使用 docker system prune --volumes,因为它会删除任何当前停止的服务栈中的数据。