
开头部分不需要主标题直接进入内容。云服务器上搭 Docker 这件事做一遍只要十分钟但要搭到能长期稳定扛业务踩的坑比想象中多。装个包、跑个 hello-world、截个图这只是开始。真正上手之后日志把磁盘写满、容器端口在外网死活连不上、镜像拉取卡在转圈、机器重启后容器没自己起来这些问题一个接一个。它们有个共同点都不是 Docker 本身的 bug而是安装方式、目录规划、daemon 配置、网络与防火墙这几处基础没打对。这篇内容把我自己在云服务器上从零搭 Docker 环境、再跑到能长期稳定用的一整套过程捋清楚包括机器怎么选、系统怎么定、装哪种包、配置怎么写、出问题从哪一层下手。刚接触容器、准备在云主机上跑几个自建服务的朋友可以照着走已经在用但总觉得环境“不太干净”、想重新整理一遍的人也能从中挑到用得上的部分。全程不需要你事先懂内核参数跟着做就行。1. 动手之前先把需求和容量算清楚1.1 云主机加容器这套组合到底在解决什么先把这个组合的价值说透不然选型很容易走偏。传统做法是买一台云主机在上面直接装运行环境、装数据库、装 Web 服务所有东西挤在一个系统里。好处是简单坏处是升级一个组件可能连带把别的搞崩换台机器迁移更是噩梦。容器把“运行环境”打包成了镜像应用依赖的库、配置、启动方式全在镜像里宿主机只负责提供内核和资源。这就带来两个直接收益一是环境一致本地跑通的镜像到云上大概率也一样二是隔离两个服务打架的概率大大降低。但容器不等于虚拟机这点必须清楚。容器共享宿主机内核所以你不能在 Linux 宿主机上跑一个 Windows 容器也不能指望容器里的内核参数和宿主机完全隔离。这决定了云服务器上跑 Docker 的一些特殊注意事项内核版本要够新、宿主机的一些 sysctl 参数会影响所有容器、磁盘和 inode 是全局共享的资源。理解了这层关系后面遇到“为什么容器里的进程被系统杀掉了”“为什么磁盘满了所有容器都异常”这类问题就不会一头雾水。我在实际使用中的体会是容器真正的门槛不在“怎么装”而在“怎么规划”。装 Docker 的命令网上到处都是抄一份就能跑起来但数据目录放哪、日志留多久、端口怎么暴露、谁有权限操作 Docker这些决策一旦定错后面改起来要停机、要迁移数据代价大得多。所以这一节先讲规划下一节才动手。1.2 单机、Compose、多机编排三条路的取舍很多人一上来就问要不要上 Kubernetes我的建议是先别急。选择哪条路取决于你要跑几个服务、几个人维护、能不能接受单点故障。下面这张表是我自己总结的对照可以直接拿去对号入座。方案适用规模上手成本主要优势主要代价裸 docker run1 到 3 个独立服务临时验证极低一条命令起一个调试直观参数记不住重建靠翻历史无法版本管理Docker Compose3 到 15 个服务单机部署低一份 YAML 描述全套可纳入版本库只在一台机器无跨机调度多机编排集群方案多节点、需要弹性伸缩高自动调度、滚动更新、故障转移运维复杂度陡增小团队容易被反噬表格里最容易被低估的是最后一行。多机编排解决的是“规模”问题不是“稳定”问题。一台 4 核 8G 的云主机跑 Compose只要配置写对、备份做好撑住日均几万访问的自建服务完全够用。反过来如果团队只有一两个人上了集群之后光是排查节点状态、网络插件、存储卷绑定就能耗掉大半精力反而没时间做业务。还有个中间选项值得提一下在同一台机器上用 Compose 分多个 project 管理。比如compose -p blog up -d跑博客compose -p tools up -d跑内部工具互相独立共用同一个 Docker 守护进程。这样既能做到服务隔离和独立启停又不用引入额外组件。我自己大部分单机场景都用这种方式清洗数据、重启某个 project 都不会影响其他的。1.3 规格估算别拍脑袋买机器容量估算这一步很多人直接跳过先买最便宜的跑不动再升配。升配本身不难但磁盘扩容、数据迁移往往要停机能一次算准就一次算准。我给一套自己常用的估算方法不精确但足够用。内存先算。宿主机系统本身含 systemd、sshd、日志服务等占 500MB 到 800MBDocker 守护进程和 containerd 再占 100MB 到 200MB加起来按 1GB 打底。然后是各服务的峰值内存之和再乘 1.3 的余量系数。举个具体例子Nginx 静态站峰值 100MB一个后端服务 500MBMySQL 的 buffer pool 设 512MB 加连接开销约 800MBRedis 设 256MB 上限合计约 1.65GB乘 1.3 得 2.1GB加上打底的 1GB就是 3.1GB。这种情况买 4GB 内存够用但只剩不到 1GB 弹性跑得稍微激进一点就会触发 OOM。所以我一般会往上一档选 8GB代价是每月多几十块换来的是不用半夜起来处理容器被杀。磁盘分两块看。系统盘只放系统和 Docker 程序本身40GB 足够数据盘单独挂放镜像层、容器卷、日志。数据盘的估算公式是镜像总量 卷数据量 × 增长系数 日志预留。镜像总量按每个服务 300MB 到 1GB 估五六个服务大概 4GB卷数据按业务量估比如数据库按每天新增 50MB、保留一年算约 18GB日志按每个容器 50MB 上限乘 3 个文件、乘容器数十个容器就是 1.5GB。再留 30% 余量100GB 数据盘是很舒服的配置。CPU 反而是最不用纠结的。容器化服务的瓶颈绝大多数时候在内存和磁盘 IO2 核能跑起来的东西4 核通常不会快一倍。除非你做编译、转码、爬虫这类计算密集的活否则 2 核起步就够把钱花在内存和磁盘上更划算。带宽同理按实际出网流量估图片和视频多的站点要提前算清楚不然流量超了账单会很难看。2. 开机器与系统初始化2.1 系统镜像和内核版本怎么选系统选型上我现在的默认答案是新项目优先用较新的长期支持版本比如 Debian 12 或 Ubuntu 22.04/24.04如果团队更熟悉 RPM 系就用 Rocky Linux 9 这类。理由不是性能差异而是软件源里的 Docker 相关包版本、systemd 版本、内核版本都比较新能省掉一堆兼容性麻烦。有一条硬性底线内核版本不要低于 4.x最好 5.4 以上。Docker 的默认存储驱动是 overlay2这个驱动对内核有要求老内核上它会自动降级到别的驱动性能和稳定性都差一截而且报错信息很隐晦。你可以用uname -r一眼看出来返回 3.10 这种就要警惕了这种一般是已经停止维护的老系统。还要避开一种情况系统自带的软件源里已经有 docker 包。很多发行版会打包一个叫docker.io或者docker的包版本通常落后好几个大版本而且和官方包的组件版本对不上。我见过最典型的现象是docker compose子命令不存在因为老包只带 compose v1 的独立二进制。所以安装前先确认一下系统里有没有残留有的话先卸干净再装官方包这比装完发现版本错乱再回滚省事。注意不要在同一台机器上混装发行版自带的 docker 包和官方仓库的 docker-ce 包两者会争抢/usr/bin/docker和 systemd 单元文件症状是命令能用但守护进程起不来或者反过来。2.2 安全组、登录方式与首次登录要做的事云服务器开出来第一件事是配安全组有的平台叫防火墙或网络 ACL。默认规则一般是只开 SSH 端口这很好别图省事直接放开全部端口。我见过太多因为把 3306 或 6379 直接暴露到公网、几天后被扫到拖库的案例。正确的做法是只有确实需要对外提供服务的端口才在安全组放行数据库、缓存、内部管理接口一律不放行通过内网或者本机回环访问。SSH 登录方式建议从密码改成密钥。密码登录在公网上被爆破是必然事件只是时间问题。生成密钥对、把公钥写进服务器的~/.ssh/authorized_keys、然后在 SSH 配置里关掉密码认证这套流程做一次十分钟收益是长期的。改配置前一定要先另开一个终端验证密钥能登录成功再关密码否则很容易把自己锁在外面。# 本地生成密钥如果还没有 ssh-keygen -t ed25519 -C opsexample # 上传公钥到服务器 ssh-copy-id -i ~/.ssh/id_ed25519.pub useryour-server-ip # 服务器端确认后编辑 /etc/ssh/sshd_config # PasswordAuthentication no # PubkeyAuthentication yes sudo systemctl reload sshd日志里能看到大量爆破尝试是常态不必紧张但也别掉以轻心。如果想进一步降低风险可以把 SSH 端口从 22 改到高位端口或者只允许固定出口 IP 访问。这些措施不能替代密钥认证但能显著减少噪音日志让真正的异常更容易被发现。2.3 首次登录后的初始化清单裸系统到手我有一套固定的初始化顺序做完之后环境就比较“干净”了。第一件事是更新系统并装基础工具包括 curl、ca-certificates、gnupg、vim、htop、unzip 这些后面装 Docker 和排查问题都要用。# Debian / Ubuntu sudo apt update sudo apt -y upgrade sudo apt -y install curl ca-certificates gnupg lsb-release vim htop unzip chrony # Rocky / Alma / 其他 RPM 系 sudo dnf -y update sudo dnf -y install curl ca-certificates gnupg2 vim htop unzip chrony第二件事是确认时间同步。容器日志时间戳错乱、数据库主从延迟异常、TLS 证书校验失败追到根上经常是宿主机时间偏了。云主机一般有内建的时间同步服务但确认一下没坏处timedatectl status看 “System clock synchronized” 是不是 yes不是的话启动 chrony 或 systemd-timesyncd。第三件事是把数据盘挂好。云主机买的时候通常带一块额外的数据盘需要在系统里分区、格式化、挂载。文件系统选 ext4 还是 xfs我的习惯是 xfs尤其是数据盘上跑大量小文件镜像层就是典型的小文件场景时表现更稳。挂载点建议固定为/data并且写进/etc/fstab用 UUID 而不是设备名避免重启后设备顺序变化导致挂不上。# 查看数据盘设备名通常是 vdb / sdb注意别搞错系统盘 lsblk sudo mkfs.xfs /dev/vdb sudo mkdir -p /data echo UUID$(sudo blkid -s UUID -o value /dev/vdb) /data xfs defaults,noatime 0 2 | sudo tee -a /etc/fstab sudo mount -a df -h /datanoatime这个参数值得加上。默认的文件系统会在每次读文件时更新访问时间容器场景下文件读取极其频繁这个写入纯属浪费 IO。加上之后对大部分业务没有影响磁盘压力能小一点。第四件事如果系统默认开了防火墙比如 ufw 或 firewalld要么把需要的端口显式放行要么在确认安全组已经做好隔离的前提下关掉它。这里有一个必须提前知道的问题下一章会展开讲Docker 发布端口时会直接操作 iptables默认会绕过 ufw 的规则。也就是说你在 ufw 里写了deny 8080用-p 8080:80起的容器照样能从外面访问。这个行为让很多人误以为自己有防火墙保护实际上没有。3. 安装 Docker三种方式的对比与完整流程3.1 三种安装方式的取舍装 Docker 有三条常见路线先看清楚差异再选不要随手复制一段脚本了事。第一种是官方提供的便捷脚本一条 curl 加管道到 shell 就装完。优点是快适合临时验证、一次性环境。缺点也明显脚本行为不透明具体装了什么版本、加了哪些源、改了什么配置你并不清楚而且它不会帮你做目录规划和 daemon 配置。生产环境我不用这种方式。第二种是通过官方软件仓库安装。这是推荐做法过程透明包管理器的依赖解析、版本锁定、升级清理都能正常工作。多敲几条命令换来的是可维护性。后面升级、回滚、查看版本都走标准流程。第三种是用系统自带的包。前面说过不推荐主要问题是版本落后、组件不全、和官方文档对不上。方式适合场景版本可控可维护性我的建议便捷脚本临时测试、一次性演示弱差用完即销毁的环境可以用官方仓库生产、长期运行强好默认选择系统自带包无外网、内网离线环境弱一般仅在受限环境用且要统一版本3.2 官方仓库安装的完整命令流程下面这套流程我在多种系统上反复用过直接抄就行。先是 Debian 和 Ubuntu 系# 1. 基础依赖 sudo apt update sudo apt -y install ca-certificates curl gnupg # 2. 建 keyring 目录并导入签名密钥 sudo install -m 0755 -d /etc/apt/keyrings sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg \ -o /etc/apt/keyrings/docker.asc sudo chmod ar /etc/apt/keyrings/docker.asc # 3. 添加软件源注意 Debian 系要把 ubuntu 换成 debian echo deb [arch$(dpkg --print-architecture) signed-by/etc/apt/keyrings/docker.asc] \ https://download.docker.com/linux/ubuntu $(. /etc/os-release echo $VERSION_CODENAME) stable \ | sudo tee /etc/apt/sources.list.d/docker.list /dev/null # 4. 安装 sudo apt update sudo apt -y install docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin # 5. 开机自启并立刻启动 sudo systemctl enable --now docker这里有几个细节值得说。第二第三步用的是keyrings加signed-by的写法而不是老的apt-key add。老写法在新版本系统上会报警告而且密钥是全局信任的安全性差。新写法把密钥和源绑定只有这个源用这个密钥校验更干净。第五步里我把docker-buildx-plugin和docker-compose-plugin一起装上了。前者是做多架构镜像构建用的现在很多基础镜像同时提供 amd64 和 arm64用 buildx 一条命令就能构建双架构镜像后者提供docker compose子命令注意是空格v2 版本比老的docker-compose带横杠v1功能更全、性能更好。一次装齐省得后面用到再补。RPM 系系统的流程大同小异sudo dnf -y install dnf-plugins-core sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo sudo dnf -y install docker-ce docker-ce-cli containerd.io \ docker-buildx-plugin docker-compose-plugin sudo systemctl enable --now docker注意仓库地址里的centos路径是给整个 RPM 系共用的Rocky、Alma、CentOS Stream 都用这个源不要因为系统名不同就去找别的路径找不到的。另外 CentOS 7 已经停止维护不建议再作为新环境的基底。3.3 装完先验证再挪目录装完不要急着跑业务容器先做三件事。第一验证版本和组件是否齐全。docker version会分别打印客户端和服务端的版本两边都出来才说明守护进程正常工作只出客户端说明服务没起来去看systemctl status docker。docker compose version确认 Compose v2 装上了。docker info输出信息很多重点看三处Storage Driver 是不是 overlay2、Docker Root Dir 指向哪个路径、Cgroup Driver 是 systemd 还是 cgroupfs。Cgroup Driver 在新版本系统上应该是 systemd如果不是某些资源限制参数会不生效。第二跑一个最小的验证容器。docker run --rm hello-world能出那段欢迎文字说明镜像拉取、容器创建、网络配置这一整条链路是通的。这一步看着傻但它能在你还没配置任何东西的时候把“网络能不能通”“存储驱动正不正常”这两个大问题暴露出来。第三把数据目录挪到数据盘。Docker 默认把所有东西放在/var/lib/docker这个路径在系统盘上。镜像层、容器可写层、卷、日志全在这里跑一段时间几十 GB 很常见系统盘满了整台机器都会出问题。正确做法是在配置里把># 1. 停服务 sudo systemctl stop docker docker.socket # 2. 迁移数据用 rsync 保留属性比 cp 稳 sudo mkdir -p /data/docker sudo rsync -aHAX --infoprogress2 /var/lib/docker/ /data/docker/ # 3. 写配置下一章会展开这个文件的完整内容 sudo mkdir -p /etc/docker sudo tee /etc/docker/daemon.json /dev/null EOF { data-root: /data/docker } EOF # 4. 启动并验证 sudo systemctl start docker docker info | grep -i docker root dir docker images迁移完成后先别急着删老目录等确认新路径下镜像、容器、卷都在、业务能正常起来再删也不迟。多留几天更保险磁盘不够的时候清一下就行。我自己就干过迁移完手快删了老目录、结果发现某个卷没同步过来的蠢事数据找回来费了大劲。4. 让 Docker 真正好用配置、权限与磁盘治理4.1 daemon.json 逐项拆解/etc/docker/daemon.json是整个 Docker 环境里最值得花时间研究的文件。它控制守护进程的行为改完需要systemctl reload docker或重启才生效部分配置项支持 reload部分必须重启。下面这份是我目前的标准模板逐项说清楚为什么这么设。{ data-root: /data/docker, storage-driver: overlay2, log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 }, registry-mirrors: [https://你的镜像加速地址], live-restore: true, default-ulimits: { nofile: { Name: nofile, Hard: 65536, Soft: 65536 } } }>sudo systemctl restart docker sudo systemctl status docker --no-pager # 起不来就去看详细原因 sudo journalctl -u docker -n 100 --no-pager4.2 用户组权限与 root 等价的风险默认情况下只有 root 能用 docker 命令日常操作一直 sudo 很烦。标准解法是把用户加进 docker 组sudo usermod -aG docker $USER # 退出重新登录或者临时切一下组 newgrp docker docker ps这一步做完确实方便了但有一个必须讲清楚的安全事实docker 组的成员等价于 root。因为你可以运行一个挂载了宿主机根目录的容器然后在里面随意读写宿主机的任何文件# 这行命令的效果等于拿到了宿主机 root docker run --rm -it -v /:/host alpine chroot /host sh所以 docker 组不要随便加人。团队里如果有多人需要操作正确做法是分清权限边界日常查看日志、重启服务这类操作可以搞一个受限的封装脚本脚本内部用 sudo 执行固定命令而不是给所有人开 docker 组。或者用 rootless 模式让 Docker 守护进程以普通用户身份运行代价是网络性能和端口绑定低于 1024 的端口需要额外配置上有些限制。单机小规模场景我倾向的做法是只给自己加 docker 组其他人的操作通过脚本或简单的管理面板代理。4.3 磁盘和日志的长期监控磁盘是云服务器上最容易出事的地方而且出事的形态有两种很多人只知道第一种。第一种是空间满。表现是所有写操作失败容器日志开始报错数据库直接拒绝写入。用df -h能看出来。第二种是 inode 满。表现和空间满几乎一样但df -h显示还有大量剩余空间。原因是文件系统里的小文件文件数达到上限了df -i才能看到真相。容器场景特别容易撞上 inode 问题因为镜像层解压出来就是成千上万个小文件。df -h # 空间使用 df -i # inode 使用 docker system df -v # 各镜像、容器、卷的具体占用docker system df -v这个命令值得加进你的日常检查清单它能把“谁占了空间”列得清清楚楚包括每个镜像的大小、每个卷的实际占用、每个容器的可写层大小。有了它再去删东西就有据可依不会误删还在用的卷。清理要分层次不能一上来就docker system prune -af --volumes。带--volumes的清理会删掉所有没有被运行中容器使用的卷如果某个服务是临时停着的它的数据就没了。我一般的清理顺序是先清悬空镜像dangling也就是没有标签的中间层再清停止超过一周的容器再清没被引用的卷最后才考虑全量清理。# 安全清理只清悬空镜像 docker image prune -f # 清掉停止超过 168 小时的容器 docker container prune -f --filter until168h # 清理未被引用的卷先 dry-run 看看 docker volume ls -f danglingtrue还有一处空间经常被忽略构建缓存。如果你在这台机器上构建镜像buildkit 会缓存大量中间层占用可能远超你的镜像本身。docker builder prune可以清理加--filter until168h保留近期的缓存兼顾速度和空间。5. 用 Compose 跑一组真实服务5.1 Compose v2 的安装与验证前面装 Docker 的时候已经带上docker-compose-plugin了所以正常情况下docker compose version直接就能用。如果你是从老环境迁移过来可能系统里同时存在docker-composev1Python 实现和docker composev2Go 实现两者配置语法略有差异混用很容易出问题。检查一下which docker-compose有的话卸掉统一用 v2。v2 相比 v1 有几个实打实的改进启动速度快很多因为不再依赖 Python 运行时支持docker compose watch做开发时的文件监视热重载depends_on的condition: service_healthy在 v2 里行为更可靠。还有一个细节v2 里--project-name的默认值取自目录名所以起项目目录名要注意compose这种通用名容易冲突。5.2 一份可复用的 compose 文件拆解下面这份是我常用的骨架包含 Web 反向代理、应用服务、数据库三层把所有关键点都体现出来了。删掉业务相关的部分就是通用模板。name: demo-stack services: web: image: nginx:1.27-alpine restart: unless-stopped ports: - 127.0.0.1:8080:80 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./www:/usr/share/nginx/html:ro depends_on: app: condition: service_healthy networks: - front app: image: demo/app:1.4.2 restart: unless-stopped env_file: .env expose: - 8000 healthcheck: test: [CMD, curl, -fsS, http://127.0.0.1:8000/healthz] interval: 30s timeout: 3s retries: 3 start_period: 20s volumes: - app-data:/app/data networks: - front - back db: image: mysql:8.4 restart: unless-stopped command: - --character-set-serverutf8mb4 - --collation-serverutf8mb4_unicode_ci - --innodb-buffer-pool-size512M environment: MYSQL_DATABASE: demo env_file: .env volumes: - db-data:/var/lib/mysql networks: - back healthcheck: test: [CMD, mysqladmin, ping, -h, 127.0.0.1, --silent] interval: 10s timeout: 5s retries: 10 volumes: app-data: db-data: networks: front: back: internal: true这份文件里有几个设计决策值得展开讲都是踩过坑之后总结的。ports写的是127.0.0.1:8080:80而不是8080:80。区别在于绑定地址前者只监听回环外部访问不到必须经过宿主机上的反向代理进来后者监听所有网卡等于直接暴露到公网。这个写法配合下一章的防火墙问题是最省心的组合——端口压根不对外安全组和防火墙的配置压力都小。网络分成了front和back两个其中back标了internal: true。含义是数据库所在的网络不允许出网只能被同网络的容器访问。这样做的好处是即使数据库容器被攻破攻击者也没法从容器里直接对外发起连接横向移动的难度大很多。应用服务同时接两个网络作为桥梁。这是 Compose 里少数几个能低成本提升安全性的配置建议养成习惯。restart: unless-stopped而不是always。区别是你手动docker stop掉的容器重启宿主机之后不会自己起来。调试的时候这个行为很重要不然你停了服务想排查问题机器一重启它又活了反而干扰判断。healthcheck配合depends_on.condition: service_healthy解决的是启动顺序问题。普通depends_on只保证容器先后启动不保证前一个真的能提供服务。应用比数据库先起来、连不上直接退出的情况太常见了。加了健康检查之后Compose 会等数据库 ping 通才启动应用启动成功率明显提升。镜像标签固定成了1.27-alpine和1.4.2而不是latest。latest的问题不在于它不好而在于它的行为不可预测某天你docker compose pull一下镜像换了行为也可能变了而你完全不知道改了什么。生产环境用明确的版本号升级是一个主动的、可测试的动作。5.3 数据持久化、备份与升级的完整流程Compose 里的数据持久化有两种写法命名卷和绑定挂载。命名卷由 Docker 管理路径在/data/docker/volumes/下面好处是跨平台一致、便于备份迁移绑定挂载直接把宿主机目录映射进容器好处是路径明确、改文件方便。我的选择标准是数据库这类需要整体备份的数据用命名卷配置文件、静态资源这类需要经常编辑的用绑定挂载。上面那份文件就是这个思路。备份不能只依赖云平台的磁盘快照。快照确实方便但它有两个问题一是恢复粒度太粗你只想恢复一个表得整盘回滚二是快照和文件系统状态强相关数据库在写入过程中拍快照恢复出来可能不一致。所以逻辑备份和快照要配合用。数据库的逻辑备份这么写# 从容器里导出注意密码通过环境变量传入别写在命令行历史里 docker compose exec -T db sh -c \ exec mysqldump --single-transaction --routines --triggers \ -uroot -p$MYSQL_ROOT_PASSWORD --all-databases \ ./backup/all-$(date %F).sql # 校验导出文件不是空的小于 1KB 基本可以判断出错 ls -lh ./backup/all-$(date %F).sql--single-transaction这个参数很关键它让 InnoDB 在一致性快照里导出数据不用锁表业务可以照常跑。如果你把密码放在了.env文件里并通过env_file注入容器内的环境变量就有上面这条命令能直接用如果用的是密钥文件方式就得改成-p$(cat /run/secrets/db_root)这种形式。卷数据的备份用一次性容器最方便docker run --rm \ -v demo-stack_app-data:/data:ro \ -v $PWD/backup:/backup \ alpine tar czf /backup/app-data-$(date %F).tar.gz -C /data .备份文件不要留在同一台机器上机器整个挂掉的话备份跟着没。传到对象存储最省事加一条rclone copy或者用云平台自带的命令行工具同步出去写进定时任务每天凌晨跑一次。升级流程我固定成四步先备份再改版本号再拉镜像最后重建。关键是第三步和第四步之间要留一个观察窗口。# 1. 备份见上 # 2. 编辑 compose 文件把 image 标签改成新版本 # 3. 拉取新镜像不重建容器 docker compose pull # 4. 只重建有变化的服务 docker compose up -d # 5. 观察日志和健康状态 docker compose ps docker compose logs -f --tail100 appdocker compose pull和up -d分两步做是为了把“下载”和“切换”分开。如果拉取过程网络出问题服务还在用老版本正常跑不会受影响。切换之后如果发现异常回滚也很简单把标签改回去再up -d一次因为老镜像还在本地秒级完成。这就是为什么不要在切换后立刻docker image prune把回滚的可能性保留几天。6. 常见问题排查实录6.1 镜像拉不动从网络层往上找这是最高频的问题症状是docker pull卡在 “Pulling fs layer” 或者直接超时。排查要按层次来不要一上来就怀疑镜像仓库。第一层看 DNS。cat /etc/resolv.conf如果配置的解析服务不可达容器和宿主机都解析不了域名。用一个简单命令确认解析正常再看能不能建立连接。第二层看 MTU。这是云上最隐蔽的坑之一云平台的网络可能有额外的封装实际可用 MTU 比 1500 小通常是 1450 甚至 1400。表现出来就是小包能通、大包丢失而 TLS 握手和大层下载恰好都是大包于是现象就是“偶尔能拉到大部分时候卡住”。判断方法很简单用不同大小的包去试探# 不分片探测能通说明 MTU 够 ping -M do -s 1400 -c 3 你的镜像仓库地址或任意公网地址如果 1400 能通、1472 不通就说明路径 MTU 在 1400 出头。解决办法是在 daemon.json 里显式设一个较小的 MTU{ mtu: 1400 }改完重启 Docker之前卡住的拉取通常就顺畅了。这个参数影响的是 Docker 创建的网桥和容器接口设小一点不会有副作用只是每包有效载荷少一些对吞吐的影响可以忽略。第三层看镜像加速配置。如果加速地址填错了或者已失效表现是拉取超时但换成直连就好。可以临时注释掉registry-mirrors试一次来定位。第四层才是看 Docker 日志journalctl -u docker -n 200 --no-pager里面有连接层面的详细报错能区分是 DNS、TCP 连接、TLS 握手还是认证问题。提示镜像拉取类问题的日志里经常出现一长串重试记录看着吓人实际信息量最大的是第一条报错。从第一条开始读不要被后面的重试淹没。6.2 端口不通三层定位法“容器跑起来了但外面访问不到”这个问题的排查我固定用三层法从上往下能在两分钟内定位。第一层容器内部。确认服务真的在容器里监听而且监听地址是对的。很多应用默认只监听127.0.0.1容器外面自然访问不到。docker compose exec app sh -c ss -lntp || netstat -lntp看到127.0.0.1:8000就要改应用配置让它监听0.0.0.0。看到0.0.0.0:8000说明容器内部没问题往下走。第二层宿主机到容器。确认端口映射存在并且从宿主机本身能访问docker compose ps # 看 PORTS 列有没有映射 curl -v http://127.0.0.1:8080/ # 从宿主机直接打 ss -lntp | grep 8080 # 看宿主机谁在监听如果docker compose ps里端口映射显示的是127.0.0.1:8080-80/tcp说明只绑了回环从外网访问不到是正常的需要有反向代理或者改绑定地址。如果宿主机上curl不通问题在容器和宿主之间常见原因是应用没起完、健康检查失败、或者容器退出了。第三层外部到宿主机。这里有两个地方要查云平台的安全组入方向规则有没有放行该端口和系统的防火墙。这里必须强调前面提过的那个问题注意Docker 发布端口时会在 iptables 的 nat 表和 filter 表里直接插入规则默认走的是DOCKER链而 ufw 的规则在INPUT链上。结果是容器端口会绕过 ufw 的 deny 规则。你在 ufw 里禁止了 8080容器照样能被公网访问。发现这个行为之后处理方式有几种。最推荐的是从根上避免容器端口只绑127.0.0.1对外统一走宿主机上的反向代理由反向代理监听 80/443这样暴露面完全可控。如果确实需要直接暴露容器端口那就不要依赖 ufw直接在云平台安全组里做限制安全组的规则在网络层生效Docker 绕不过去。还有一种做法是在 iptables 的DOCKER-USER链里加规则这个链是专门留给你自定义的规则优先级高于 Docker 自动生成的规则但维护起来相对麻烦。6.3 磁盘满、内存 OOM 与容器反复重启磁盘满的判断前面讲过df -h和df -i一起看。确定是空间问题后用docker system df -v找元凶。最常见的三个来源日志没做轮转daemon.json 没配 max-size、构建缓存堆积、以及某个容器的可写层里写了大量临时文件。第三种情况特别容易被忽略因为可写层的数据不在卷里容器一删就没了但在运行期间它会一直涨。解决办法是让应用把临时文件写到挂载的卷里或者挂一个 tmpfs。内存 OOM的表现是容器毫无征兆地退出退出码 137日志里没有任何错误。确认方法# 看内核有没有杀进程的记录 sudo dmesg -T | grep -i -E out of memory|killed process # 看容器的 OOM 标记 docker inspect -f {{.State.OOMKilled}} {{.State.ExitCode}} 容器名输出true 137就实锤了。这时候不要急着加内存先搞清楚是谁吃的是应用真需要这么多还是内存泄漏还是 JVM 这类运行时的堆没设上限。Java 服务在容器里不设-Xmx会按宿主机总内存去算堆大小容器限额 1G 它可能按 8G 去申请必然被杀。设上明确的堆上限把容器内存限额留出 20% 给堆外内存问题就解决了。容器反复重启docker compose ps里能看到状态在Restarting和Up之间跳。第一步是看退出码和日志别急着改配置docker compose ps docker compose logs --tail200 服务名 docker inspect -f {{.State.ExitCode}} {{.State.Error}} 容器名退出码能快速缩小范围1 一般应用报错看日志137 是 OOM 或被杀139 是段错误143 是收到 SIGTERM通常是启动超时被停。如果日志里什么都没有说明进程是在初始化阶段就挂了检查启动命令、挂载路径权限、环境变量是不是缺了。还有一种情况是健康检查一直失败导致的反复重建这时候日志正常但状态不健康得去看健康检查的命令本身写得对不对比如镜像里根本没有 curl。6.4 高频问题速查表把上面这些整理成一张表出问题的时候先扫一眼能省不少时间。现象最可能原因快速验证处理方式拉镜像卡住或超时MTU 不匹配或加速器失效ping -M do -s 1400探测daemon.json 设 mtu检查 registry-mirrors容器端口外部不通只绑了 127.0.0.1 或安全组未放行docker compose ps看 PORTS 列改绑定地址或加安全组规则防火墙规则对容器无效Docker 绕过 ufw 直接写 iptablesufw deny 后仍能访问端口只绑回环限制放安全组做容器无故退出码 137内存超限被杀docker inspect看 OOMKilled调内存限额或应用堆参数磁盘显示有空间但写不进去inode 耗尽df -i清理小文件迁移数据盘磁盘空间被吃光日志未轮转或构建缓存docker system df -v配 max-sizeprune 缓存重启机器后容器没起来restart 策略是 no 或手动 stop 过docker inspect看 RestartPolicy设unless-stopped数据库容器数据不见># 每周清理脚本示例放在 crontab 里 #!/bin/sh docker image prune -f docker container prune -f --filter until168h docker builder prune -f --filter until168h docker system df这套东西配好之后日常维护的工作量很小大部分时间你根本不需要登录服务器。真出问题的时候因为配置是透明的、日志是受控的、备份是可恢复的排查和恢复都不会太痛苦。我自己在实际操作中的体会是云服务器上搭 Docker 这件事装的那十分钟真的不重要重要的是装完之后做的那些决定数据放哪、日志留多久、端口怎么暴露、谁来备份、多久演练一次。这些决定做对了环境能安安稳稳跑一两年不用大动做错了问题会在半年后以各种你想不到的方式冒出来。所以每次给人搭环境我都宁愿多花半小时把 daemon.json 和 compose 骨架写扎实也不愿意事后去救火。如果你现在正准备从头搭一套建议就按这个顺序来先把第 1 章的容量估算和第 3 章的目录规划做掉后面的事情会顺很多。