ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

云服务器Docker生产环境搭建与运维:配置、编排、排障

云服务器Docker生产环境搭建与运维:配置、编排、排障

1. 云服务器上搭 Docker,先想明白要解决什么问题

在云服务器上搭建 Docker 这件事,看着像一条命令就能搞定,实际上真正的门槛不在安装那几步,而在“装完之后怎么让它稳定、安全、可维护地跑起来”。我做运维和项目交付这些年,帮人看过太多跑偏的机器:有人把容器日志写满系统盘导致整台机器卡死,有人装完 Docker 就把防火墙关了图省事,也有人用 1 核 1G 的机器跑数据库容器,天天被 OOM 打断。这些问题的根子都不在 Docker 本身,而在于搭之前没想清楚目标。

这篇内容面向的是这样一批人:手里有一台刚买的云服务器,想用它承载自己的小服务、练手环境或者几个常驻的自动化任务;知道 Docker 这个词,也敲过docker run hello-world,但不确定生产环境该怎么配;或者之前踩过坑,想把机器重新收拾一遍。我会从选机器、系统初始化、安装方式选型、镜像加速、存储迁移、日志治理、编排到故障排查,一条链路讲完,所有命令都可以直接复制改参数使用。

先明确一点:容器不是虚拟机。很多人第一反应是“我装个 Docker 就等于有了很多台小服务器”,这个理解会让你在后面配置资源限制、数据持久化、网络端口时处处别扭。容器共享宿主机内核,它只是一组被隔离和限制过的进程。理解这句话,你就明白为什么容器里的数据默认会随容器删除而消失,也就明白为什么必须做卷映射;也明白为什么容器里跑systemd或改内核参数会失败。这些认知上的差异,比记住命令重要得多。

1.1 三种典型场景,配置要求差得很远

同样是“云服务器 + Docker”,用途不同,选型思路完全不同。我按最常见的三类整理了一张对照表,你对着自己的目标挑:

场景典型用途推荐配置关键注意点
学习练手跑官方镜像、学 Compose、试新工具2 核 2G,系统盘 40G别装太重的东西,随时可以重建
长期跑小服务个人站点、定时任务、轻量 API2 核 4G,系统盘 40G + 数据盘 50G必须做日志轮转和数据备份
多容器编排数据库 + 应用 + 反向代理4 核 8G 起,SSD 数据盘内存要留 1G 给系统,数据库单独挂卷

我把“系统盘 + 数据盘”拆开是有原因的。系统盘跟着机器生命周期走,重装系统就没了;数据盘可以卸载挂到新机器上。你把 Docker 的数据目录放到数据盘,将来换机器、扩容、重装,数据都能带走。这是我在吃过一次“重装系统数据全丢”的亏之后养成的习惯,后面第 4 章会讲具体怎么迁。

1.2 买机器时容易被忽略的四个参数

CPU 架构。现在不少云厂商默认推 ARM 机型,价格便宜、性能也不错,但你要清楚:很多镜像的 ARM 版本不全,尤其是些小众工具和旧版本软件。docker pull报no matching manifest for linux/arm64就是这个原因。新手我建议先选 x86_64,生态最全,遇到问题搜到的答案也最多。

内存。这是最容易低估的一项。Docker 守护进程本身要占一两百兆,系统基础服务再占几百兆,2G 内存的机器上你实际可用的容器内存不到 1.5G。跑一个 MySQL 8 的默认配置就可能直接触发 OOM。我一般按“容器需求总和 × 1.3 + 1G 系统预留”来估算,比如三个容器各要 512M,那就是 1.5G × 1.3 + 1G ≈ 3G,选 4G 机型比较稳。

磁盘类型与容量。镜像层是很占空间的,一个 Node 项目的构建镜像动辄 1G 以上,加上各种中间层,20G 系统盘很快就见底。我通常系统盘给 40G 装系统和 Docker 程序,数据盘给 50G 起步专门放/var/lib/docker的迁移目录。磁盘类型优先选 SSD,机械盘上跑数据库容器你会怀疑人生。

带宽计费方式。按固定带宽计费适合流量稳定的站点,按使用流量计费适合平时没什么访问、偶尔传大文件的场景。这条不影响 Docker 本身,但会影响你拉镜像的速度体验。拉镜像走的是出方向流量,如果你按流量计费又频繁重建镜像,账单会有点难看。

2. 系统初始化:把地基打牢再动 Docker

拿到服务器第一件事不是装 Docker,而是把系统收拾干净。我见过太多“装完 Docker 才发现 SSH 登不上”“跑两天发现时间慢了八小时导致证书校验失败”的情况。这一章的操作大概花十分钟,能省掉后面好几小时的排查。

2.1 登录、更新与基础工具

首次登录用云厂商控制台提供的密钥或初始密码,登录后第一件事是更新软件源和系统包。以 Ubuntu 22.04 为例:

# 更新软件包索引并升级已安装的包 apt update && apt upgrade -y # 安装后续会用到的工具 apt install -y curl wget vim git unzip htop ca-certificates gnupg lsb-release

ca-certificates这个包千万别省,Docker 拉镜像走 HTTPS,证书链不全就会报x509: certificate signed by unknown authority,这个报错看着吓人,其实就是缺证书。

顺手把时区调对:

timedatectl set-timezone Asia/Shanghai timedatectl status # 确认输出里有 Asia/Shanghai

时区这事看着无关紧要,实际上容器里的应用读的是宿主机时间(除非你在容器里单独配),日志时间戳错八小时,排查问题时你会被误导得很惨。我遇到过一次证书过期告警,查了半天发现是机器时间漂了,chrony或systemd-timesyncd没跑起来,手动同步之后一切正常。

2.2 建普通用户,别拿 root 到处跑

云服务器默认给 root,很多人图省事就一直用 root 操作。风险在于:一条rm -rf打错一个空格,可能就把系统目录删了;容器里如果配了目录映射,攻击面也会更大。我的做法是建一个普通用户,加入 sudo 组和 docker 组:

adduser deploy usermod -aG sudo deploy # 把本地公钥写进新用户(前提是你已经配好密钥登录) mkdir -p /home/deploy/.ssh cp ~/.ssh/authorized_keys /home/deploy/.ssh/ chown -R deploy:deploy /home/deploy/.ssh chmod 700 /home/deploy/.ssh chmod 600 /home/deploy/.ssh/authorized_keys

然后修改 SSH 配置,关掉密码登录、关掉 root 直接登录:

vim /etc/ssh/sshd_config # 修改或确认以下三项 # PermitRootLogin no # PasswordAuthentication no # PubkeyAuthentication yes systemctl restart sshd

注意:改 SSH 配置前一定要先另开一个终端窗口测试新用户能不能登录成功,确认没问题再关掉旧窗口。改配置把自己锁在门外的场景,我见过不止一次。

docker组这个事单独说一句:把用户加进 docker 组,等于给了这个用户接近 root 的权限,因为你可以用容器挂载宿主机根目录。单人使用的机器无所谓,多人共用的机器要掂量一下。加完组需要重新登录才生效,直接su - deploy或者退出重连。

2.3 安全组和系统防火墙,两道门都要管

云服务器有两层网络防护:云平台控制台的安全组(在机器外面)和系统内的iptables / ufw(在机器里面)。很多人只配了一层,然后疑惑“为什么端口还是不通”或者“为什么我明明关了端口还能访问”。

我的原则是:安全组只放行真正需要的端口,系统防火墙只做兜底。

安全组建议配置:

  • 22 端口:只对自己常用 IP 开放,不要 0.0.0.0/0
  • 80 / 443:对全部开放(如果跑 Web 服务)
  • 其他业务端口:一律不开放,需要通过反向代理走 80/443
  • 数据库端口(3306、5432、6379):绝对不要对公网开放

系统内用 ufw 做个兜底(Ubuntu):

ufw default deny incoming ufw default allow outgoing ufw allow 22/tcp ufw allow 80/tcp ufw allow 443/tcp ufw enable ufw status verbose

这里有个经典陷阱:Docker 在启动容器做端口映射时,会直接往 iptables 的 nat 表和 filter 表插规则,绕过 ufw 的规则链。也就是说,你ufw deny 8080,但容器用-p 8080:8080发布端口,公网照样能访问。解决办法有两个:一是发布端口时绑定到127.0.0.1,例如-p 127.0.0.1:8080:8080,只让本机访问再由反向代理转发;二是干脆不发布端口,让容器加入同一个 Docker 网络,由 nginx 容器内部转发。我强烈推荐第二种,这是最干净的做法。

3. 安装 Docker:三条路线怎么选

Docker 的安装方式有好几种,各自的适用场景不一样。网上教程鱼龙混杂,有的还在教用apt install docker.io,装出来版本老、缺插件;有的直接甩一个curl | bash的脚本,方便是方便,但你不知道它到底改了什么。我把三条路线的差异说清楚,你按需选。

3.1 三条安装路线对比

方式命令要点优点缺点适用
官方仓库安装添加 GPG 密钥 + 软件源 + apt install版本新、可升级、组件完整步骤稍多推荐,长期使用
包管理器自带源apt install docker.io一条命令版本落后 1-2 年临时测试
便捷安装脚本官方 get 脚本最快不透明、默认装最新版一次性环境

我自己的标准做法是官方仓库安装。理由很简单:能跟着系统包管理器一起升级,出了版本兼容问题也好回滚。这里有一个细节,很多教程没提:如果你之前装过docker.io、docker-engine或者podman-docker,先卸干净再装,否则会冲突。

# 卸载可能存在的旧版本(没装过会提示找不到包,正常现象) apt remove -y docker docker-engine docker.io containerd runc

3.2 官方仓库安装的完整步骤(Ubuntu / Debian 系)

# 1. 创建密钥目录并下载 GPG 密钥 install -m 0755 -d /etc/apt/keyrings curl -fsSL https://<官方源地址>/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc chmod a+r /etc/apt/keyrings/docker.asc # 2. 写入软件源 echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ https://<官方源地址>/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \ | tee /etc/apt/sources.list.d/docker.list > /dev/null # 3. 更新索引并安装 apt update apt install -y docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin

最后两个插件包别漏了:buildx用来做多架构镜像构建,compose-plugin提供了docker compose子命令(注意是中间带空格的 V2 版本,不是老的docker-compose独立二进制)。很多人装完发现docker compose命令不存在,就是少了这个包。

国内环境从官方源拉取可能比较慢,可以在软件源里换成可用的镜像地址,或者干脆用系统自带的包管理器安装——代价是版本旧。如果只是练手,旧版本也不影响学习,功能上大概差个一两年,docker composeV2 这种新特性可能就没有。我的建议是:能装新版就装新版,后续学 Compose、Buildx 都不会卡住。

CentOS / Rocky 系的步骤大同小异,用dnf config-manager --add-repo添加源,然后dnf install docker-ce,这里不展开。

3.3 装完必须做的五项验证

装完不要急着跑业务,先花两分钟验证这套东西是健康的。这五项我每次都要过一遍:

# 1. 服务是否开机自启并正在运行 systemctl enable --now docker systemctl is-active docker systemctl is-enabled docker # 2. 版本信息,确认 Client 和 Server 都在 docker version # 3. 守护进程详细信息,看存储驱动和根目录 docker info | grep -E "Storage Driver|Docker Root Dir|Cgroup" # 4. 跑个最简容器验证拉取和运行链路 docker run --rm hello-world # 5. Compose 插件是否就位 docker compose version

docker info里的Cgroup Driver一项值得留意,应该是systemd。如果是cgroupfs,在高负载下可能出现资源统计不准甚至容器被误杀,新版 Docker 默认已经是 systemd,老版本可能需要手动在daemon.json里配"exec-opts": ["native.cgroupdriver=systemd"]。

4. 让 Docker 真正好用:四个必做配置

安装完成只完成了 30%,剩下 70% 是让它适合长期跑。这一章的四项配置,是我认为“不做就不算搭好”的底线要求。

4.1 镜像加速:解决拉取超时的第一手段

拉镜像慢、拉一半超时,是新机器上最高频的困扰。Docker 守护进程支持配置镜像加速地址(registry mirror),配置后先走加速源,源上没有的再回源。

编辑/etc/docker/daemon.json(文件不存在就新建):

{ "registry-mirrors": ["https://<你的加速地址>"], "log-driver": "json-file", "log-opts": { "max-size": "10m", "max-file": "3" }, "data-root": "/data/docker" }

改完重载配置:

systemctl daemon-reload systemctl restart docker docker info | grep -A 3 "Registry Mirrors"

有几个点必须说清楚:

加速地址不是越多越好。配三五个源,Docker 会依次尝试,第一个超时了才轮下一个,反而更慢。配一到两个稳定可用的就够。

加速器只加速镜像拉取,不会加速你构建镜像时的apt install或npm install,那两个要走各自的国内源配置。

不是所有镜像都会被缓存。加速源本质是个代理缓存,冷门镜像照样要回源拉,所以“配了加速器还是慢”是正常现象。

如果镜像实在拉不下来,还有一个办法是用一台能正常拉取的机器docker pull之后docker save成 tar 包,传过去docker load。这个操作我做过很多次,尤其在网络条件受限的环境里,虽然土但很管用:

# 有网的机器上 docker pull nginx:1.25-alpine docker save -o nginx.tar nginx:1.25-alpine # 传到目标机器后 docker load -i nginx.tar

4.2 数据盘挂载与 Docker 根目录迁移

默认情况下,镜像、容器层、卷数据都堆在/var/lib/docker,也就是系统盘上。一台跑三个月的机器,这个目录涨到二三十 G 是常事。系统盘满了的后果很严重:Docker 服务起不来、SSH 可能都登不上、数据库直接崩。

所以我的标准动作是:挂一块数据盘,把 Docker 根目录迁过去。

# 1. 确认数据盘设备名(未格式化的话没有文件系统信息) lsblk df -h # 2. 格式化为 ext4(新盘首次操作,已有数据的盘千万别执行) mkfs.ext4 /dev/vdb # 3. 挂载 mkdir -p /data mount /dev/vdb /data # 4. 写进 fstab 实现开机自动挂载,用 UUID 更稳 blkid /dev/vdb # 拿到 UUID echo 'UUID=<上一步的UUID> /data ext4 defaults,nofail 0 2' >> /etc/fstab mount -a # 验证 fstab 写得对不对

这里有个必须强调的坑:fstab写错了会导致机器重启后进入紧急模式,SSH 都登不上,只能去控制台救。所以写完一定要执行mount -a验证一遍,nofail参数也别省,它能让挂载失败时系统照常启动。

然后是迁移 Docker 目录:

# 1. 停掉 Docker systemctl stop docker docker.socket # 2. 用 rsync 拷贝并保留权限(比 cp 更适合这个场景) mkdir -p /data/docker rsync -aP /var/lib/docker/ /data/docker/ # 3. 在 daemon.json 里加上 "data-root": "/data/docker" # (就是 4.1 里那份配置) # 4. 启动并验证 systemctl start docker docker info | grep "Docker Root Dir"

确认新目录生效、容器能正常跑之后,再删掉旧目录释放空间。顺序千万别弄反,我见过先删旧目录再改配置的,结果镜像全没了。

容量估算可以这么算:镜像层大小 + 容器可写层 + 卷数据 + 10% 冗余。日常用docker system df看总体占用,用docker system df -v看每个镜像、卷的细账。

4.3 日志轮转:别让一个容器撑爆磁盘

这是最隐蔽也最致命的问题。Docker 默认的日志驱动是json-file,而且不限制大小。一个不断打日志的容器,一天能写出几十 G。磁盘满了之后,表现是No space left on device,然后各种服务莫名崩溃。

在 4.1 的daemon.json里已经配了全局日志策略,含义是:

  • max-size: "10m":单个日志文件最大 10MB
  • max-file: "3":最多保留 3 个文件(当前 + 2 个轮转)

算一下:单个容器日志占用上限 = 10MB × 3 = 30MB。哪怕跑 30 个容器,总共也就 900MB 左右,完全可控。这个配置我认为是每个用 Docker 的人都该做的,成本几乎为零。

要注意两点:

全局配置只对新创建的容器生效。已有容器需要删掉重建,或者用docker compose down && docker compose up -d重建才能应用。

个别容器需要更大的日志量时,可以在 Compose 里单独覆盖,而不是把全局值调大:

services: api: image: my-api:latest logging: driver: json-file options: max-size: "50m" max-file: "5"

已经在跑的容器,日志文件可以用truncate -s 0 <日志路径>清空,但注意这只是应急,不解决根因。根因一般是应用在调试模式下疯狂打日志,或者某个循环里每毫秒都写一行,这个得从代码层面治。

4.4 资源限制:给容器套上缰绳

默认情况下,所有容器可以无限制地吃宿主机的 CPU 和内存。一个内存泄漏的容器会把整台机器的内存吃光,触发内核 OOM Killer,被杀的往往是别的无辜进程,甚至是你自己的 SSH。

所以每个容器都要设上限。在 Compose 里写:

services: app: image: my-app:latest deploy: resources: limits: cpus: "0.50" # 最多用半个核 memory: 512M # 最多 512MB 内存 reservations: memory: 256M # 保证至少 256MB

limits是硬上限,超了就会被限制或被 OOM Kill;reservations是软保证。CPU 的0.50表示 50% 的单个核心,多核机器上写成2.0就是两个核心的配额。

命令行方式对应--cpus和--memory参数:

docker run -d --name app --cpus="0.5" --memory="512m" my-app:latest

参数怎么定?我的经验值是:

  • 无状态 Web 服务:0.5 核 / 256-512M 起步,压测后再调
  • 数据库:按数据量给,MySQL 至少 1G,PostgreSQL 至少 1G
  • 定时任务型容器:CPU 可以给很低(0.1),内存按峰值给
  • 构建型容器(如 CI 构建):临时给足,构建完就销毁

还有个容易忽略的点:没有限制的容器数量本身也是一种资源消耗。每个容器都有独立的网络栈、文件系统挂载、监控开销,跑几十上百个小容器,光管理开销就不少。我一般控制单机容器数量在 20 个以内。

5. Docker Compose 编排:从能跑到好维护

docker run一条条敲,跑三个容器就开始乱了:端口记不住、参数记不住、重启之后怎么恢复不记得。Compose 解决的就是这个问题——把整台机器的服务拓扑写成一份 YAML 文件,纳入版本管理,一条命令拉起全部。

5.1 安装与项目目录规范

前面装docker-compose-plugin时已经带上了,验证一下:

docker compose version

注意命令是docker compose(中间空格),不是老版的docker-compose(中间横杠)。老版本独立二进制在较新的系统上会提示docker-compose: command not found,这是正常的,用新写法就行。

项目目录我习惯这样组织,好处是备份的时候整个目录打包带走就行:

/opt/docker/ ├── nginx/ │ ├── docker-compose.yml │ ├── .env │ └── conf/ │ └── default.conf ├── app/ │ ├── docker-compose.yml │ ├── .env │ └── data/ └── db/ ├── docker-compose.yml ├── .env └── data/

每个服务一个独立目录、独立 Compose 文件、独立.env,互不干扰。有人喜欢把所有服务塞进一个大的docker-compose.yml,好处是一键启动全部,坏处是改一个服务要动整个文件,一个服务起不来全组都起不来。我倾向于拆分,用docker compose -f指定,或者干脆分别cd进去操作。

5.2 一份可以直接抄的 Compose 模板

下面这份是反向代理 + 静态站的组合,覆盖了日常最常用的配置项,注释写清了每一行的意图:

services: web: image: nginx:1.25-alpine # 固定小版本,避免 latest 变动导致意外 container_name: web restart: unless-stopped # 异常退出自动重启,手动 stop 则不重启 ports: - "127.0.0.1:8080:80" # 只绑本机,公网走外层代理 volumes: - ./conf/default.conf:/etc/nginx/conf.d/default.conf:ro - ./html:/usr/share/nginx/html:ro - ./logs:/var/log/nginx environment: - TZ=Asia/Shanghai logging: driver: json-file options: max-size: "10m" max-file: "3" deploy: resources: limits: cpus: "0.50" memory: 256M healthcheck: test: ["CMD", "wget", "-q", "-O", "-", "http://localhost/"] interval: 30s timeout: 3s retries: 3 start_period: 5s networks: - webnet networks: webnet: driver: bridge

几个关键设计说明:

镜像标签固定小版本。nginx:1.25-alpine而不是nginx:latest。latest会在你不知情的时候变成新版本,某次重启突然行为不一样,排查起来极其费劲。我所有的 Compose 文件都固定版本号,升级是有意识的行为。

端口绑定 127.0.0.1。如 2.3 说的,这样能绕开 Docker 绕过 ufw 的问题。真正对外的是外层反向代理或者云平台的负载均衡。

restart: unless-stopped而不是always。区别在于:手动docker stop的容器,机器重启后unless-stopped不会把它拉起来,always会。调试时你手动停掉一个容器,不希望它自己又活过来,所以用前者。

healthcheck 很值得加。它让 Docker 知道容器“是不是真的健康”,而不只是“进程还在不在”。后面接监控、做自动重启都靠它。start_period是给应用启动留的宽限期,期间健康检查失败不计入重试次数。

卷挂载用相对路径。./conf相对于 Compose 文件所在目录,整个项目目录搬到哪都能跑,比绝对路径灵活。

5.3 环境变量与密钥管理

密码、密钥这类东西不要直接写在 Compose 文件里,因为这份文件大概率会进 git。做法是写到同目录的.env文件,Compose 会自动读取:

# .env DB_PASSWORD=<强密码> DB_NAME=appdb
services: db: image: postgres:16-alpine environment: POSTGRES_PASSWORD: ${DB_PASSWORD} POSTGRES_DB: ${DB_NAME} volumes: - ./data:/var/lib/postgresql/data

.env一定要加进.gitignore,并且chmod 600。我见过把.env提交到公开仓库导致数据库被扫的例子,成本极低的事,别省。

如果环境更敏感,可以用 Docker 的 secrets 机制,或者挂载只读文件进去。单人小项目用.env足够,团队协作再考虑更重的方案。

5.4 常用命令与重建习惯

# 后台启动(-d),首次会自动拉镜像 docker compose up -d # 改了镜像或环境变量后重建 docker compose up -d --force-recreate # 看状态(包含健康状态) docker compose ps # 看日志,-f 跟随,--tail 只看最后 N 行 docker compose logs -f --tail=100 web # 停止并删除容器(保留卷) docker compose down # 连卷一起删(谨慎,会丢数据) docker compose down -v

down -v这个命令我建议你在敲之前默念三遍。它会删掉这个项目定义的所有命名卷,数据库数据直接没了。默认不加-v,养成这个习惯。

6. 常见问题与排查实录

Docker 的报错信息有时候很含糊,一个新手的直觉往往是“重装一遍”,但重装解决不了配置问题。这一章把我实际遇到的高频问题整理成速查表,再讲讲排查思路。

6.1 高频故障速查表

现象可能原因排查与解决
Cannot connect to the Docker daemon服务未启动 / 用户不在 docker 组systemctl status docker;groups看是否含 docker
permission denied /var/run/docker.sock同上usermod -aG docker $USER后重新登录
No space left on device磁盘满 / inode 满df -h、df -i、docker system df
容器起来就跑(Exited 0/1)主进程是后台任务,无前台进程docker logs <容器>;确保 CMD 是前台运行
端口访问不通安全组未放行 / 服务监听 127.0.0.1ss -lntp看监听地址;检查安全组规则
拉镜像超时 / TLS 错误加速源不可用 / 证书缺失换加速地址;apt install ca-certificates
容器退出码 137内存超限被 OOM Killdocker inspect看 OOMKilled;调大内存限制
容器时间不对时区未设置加TZ=Asia/Shanghai环境变量
修改配置后不生效容器没重建docker compose up -d --force-recreate
磁盘占用只增不减悬空镜像和停止的容器堆积docker system prune -a(确认无有用数据)

6.2 我用的四步排查法

遇到容器起不来,我基本按这个顺序走,八成问题在前两步就能定位:

第一步:看容器状态和退出码。

docker compose ps -a # 看 STATUS 和退出码 docker inspect <容器> --format '{{.State.ExitCode}} {{.State.OOMKilled}}'

退出码 0 通常是“进程正常结束但容器本该常驻”,比如你的启动命令是service xxx start(后台启动后立即返回),容器就没前台进程了。退出码 137 是 128+9,被 SIGKILL,最常见是内存超限。退出码 1 是应用自己报错退出。

第二步:看日志。

docker logs --tail 200 <容器>

这一步能解决绝大部分问题。日志里通常会直接告诉你缺什么依赖、连不上什么地址、配置文件哪行写错了。

第三步:进容器里手动复现。

docker run -it --rm --entrypoint /bin/sh my-image:latest

如果镜像里没有 shell(比如 alpine 精简镜像可能没 bash,但有 sh),就换个思路:用docker exec进到正在跑的容器,或者临时起一个带 shell 的基础镜像挂载同样的卷来观察。这一步的目的是判断问题出在“镜像本身”还是“运行参数”。

第四步:看宿主机层面。

dmesg | tail -50 # 内核有没有 OOM 或磁盘错误 journalctl -u docker -n 100 # 守护进程日志 df -h && df -i # 磁盘和 inode free -h # 内存余量

到这一步还没解决,问题往往不在 Docker,而在系统层面:磁盘 inode 满了、内核参数限制、文件句柄数不够等等。

6.3 几条踩出来的避坑心得

不要在容器里跑后台服务管理工具。有人习惯在 Dockerfile 里写service nginx start,结果容器起来就退出。容器需要一个前台进程作为 PID 1,nginx 官方镜像用的是nginx -g 'daemon off;'就是这个道理。你要么用官方镜像的正确启动方式,要么自己写CMD时确保进程在前台。

docker system prune别无条件执行。这条命令会清掉所有未被使用的镜像、停止的容器、悬空网络。有次我手快执行了,把几个停着备用但没删的容器一起清了。用之前先docker system df看占用,用docker image prune/docker container prune分开清更安全。

卷的命名要清楚。Compose 里用相对路径挂载(bind mount)的数据在项目目录下,看得见摸得着;用命名卷(named volume)的数据在/var/lib/docker/volumes/下,路径是一串随机 ID。前者好备份,后者性能在某些场景更好(尤其 macOS 上,Linux 上差别不大)。我在 Linux 服务器上一律用相对路径挂载,备份的时候tar一下目录就完事,不用去翻 volumes 目录。

改完daemon.json一定先systemctl daemon-reload。直接 restart 有时不生效,因为 systemd 缓存了旧的 unit 配置。这个细节我吃了好几次亏才记住。

批量重启要注意依赖顺序。数据库容器和依赖它的应用容器如果同时重启,应用启动时数据库还没 ready,会一直报连接失败然后退出(虽然有restart策略会重试,但日志会很脏)。Compose 的depends_on只能控制启动顺序,不能控制“就绪”,真正可靠的做法是应用侧做重试容忍,或者用healthcheck+ 脚本等待。

7. 日常维护:备份、升级与监控

机器搭好只是开始,长期跑下去要面对的是“怎么不丢数据”和“怎么知道它挂了”。这一章讲我的日常维护动作,都是花不了多少时间但能避免大麻烦的事。

7.1 备份:只备份三样东西

Docker 环境里真正需要备份的就是三样:卷数据(或 bind mount 的数据目录)、Compose 文件与配置、.env密钥文件。镜像不需要备份,随时能重新拉;容器也不需要,靠 Compose 重建就行。

用一个简单的脚本,定时打包到独立目录(最好是另一块盘或者对象存储):

#!/bin/bash # /opt/backup/backup.sh set -e BACKUP_DIR=/data/backup DATE=$(date +%Y%m%d_%H%M) KEEP_DAYS=7 mkdir -p "$BACKUP_DIR" # 打包项目目录(排除日志和不需要的缓存) tar -czf "$BACKUP_DIR/docker-projects-$DATE.tar.gz" \ --exclude='*/logs/*' \ --exclude='*/cache/*' \ /opt/docker # 清理 7 天前的备份 find "$BACKUP_DIR" -name "docker-projects-*.tar.gz" -mtime +$KEEP_DAYS -delete echo "backup done: $BACKUP_DIR/docker-projects-$DATE.tar.gz"

加进 crontab 每天凌晨跑:

# crontab -e 0 3 * * * /bin/bash /opt/backup/backup.sh >> /var/log/backup.log 2>&1

备份不验证等于没备份。我每季度会随机挑一个备份包,解压到临时目录,看看文件是不是完整的、数据库导出文件能不能打开。有一次发现某个服务的 bind mount 路径写错了,备份包里全是空目录,要不是验证过,等真出事就晚了。

数据库容器的备份要额外处理,不能直接 tar 数据目录(运行中的数据库文件拷贝出来可能是不一致的)。正确做法是用docker exec调用数据库自带的备份工具:

# PostgreSQL docker exec -t db pg_dump -U postgres appdb > /data/backup/appdb-$(date +%F).sql.gz # MySQL / MariaDB docker exec db mysqldump -uroot -p"$DB_PASSWORD" --single-transaction appdb > /data/backup/appdb-$(date +%F).sql

--single-transaction这个参数对 InnoDB 很重要,能保证备份期间不锁表、导出的数据是一致的。

7.2 升级 Docker 和镜像的策略

Docker 引擎本身:不要追最新的。我的习惯是等新版本发布一到两个月,社区反馈稳定了再升,用apt upgrade docker-ce docker-ce-cli containerd.io。升级前先看一眼 release notes 有没有不兼容改动,尤其是涉及 cgroup、存储驱动、网络的部分。生产机器升级前先快照,这个不用犹豫。

业务镜像:固定小版本的标签,按需升级。升级前先在临时目录起一份新版本容器,把关键接口跑一遍,确认没问题再切。用docker compose pull && docker compose up -d一键完成,回滚就是改回旧版本号再 up。

系统内核和 Docker 的关系:内核升级后建议重启一次,某些网络和存储特性的变更要重启才生效。重启前确认所有服务的restart策略是对的,容器能自动回来。

7.3 最低成本的健康监控

不需要一上来就搭一整套监控系统,先做两件事就能覆盖大部分故障:

第一件,容器的 healthcheck + 自动重启。在 Compose 里给关键服务加 healthcheck,然后配合一个简单的定时检查脚本:

#!/bin/bash # 每小时检查一次,容器不健康就记录并重建 UNHEALTHY=$(docker ps --filter health=unhealthy --format '{{.Names}}') if [ -n "$UNHEALTHY" ]; then echo "$(date) unhealthy: $UNHEALTHY" >> /var/log/health-check.log # 按需处理,比如重启对应服务 # docker restart $UNHEALTHY fi

第二件,磁盘和内存水位检查。磁盘用到 80% 就该处理了,等到 100% 就来不及了:

#!/bin/bash THRESHOLD=80 USAGE=$(df -h / | awk 'NR==2 {gsub("%","",$5); print $5}') if [ "$USAGE" -gt "$THRESHOLD" ]; then echo "$(date) disk usage ${USAGE}% on /" >> /var/log/disk-alert.log docker system df >> /var/log/disk-alert.log fi

这两段脚本很土,但管用。真正的监控体系可以后补,先保证“出问题时你知道”,比什么都强。

最后分享一个我用了很多年的检查习惯:每次改完配置、重启完服务,别急着关终端,跑一遍docker compose ps加docker compose logs --tail=50,确认所有容器都是 healthy 或者至少是 running,日志里没有刺眼的 ERROR。三分钟的事,能挡住绝大多数“当时看着没问题、过两天突然挂了”的情况。云服务器上的 Docker 环境,稳不稳不在于装得多漂亮,而在于你有没有给每个容器套上限制、给每份数据留好退路、给每次变更留下验证。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进