资讯动态

Docker Compose 容器化部署上线:从镜像构建到排错维护

发布时间:2026/10/23 3:38:11 来源:尧图企业网站定制
1. 先把上线这件事拆开看Docker Compose 到底替我们省掉了什么很多人第一次把本地项目往公网服务器上搬脑子里想的是一条直线买机器、装环境、传代码、跑起来。真动手才发现这条线上全是岔路——运行时装哪个版本、依赖装不装得全、端口开没开、数据库连不连得上、进程挂了谁来拉起、重启之后配置还在不在。Docker Compose 的价值不是让你少敲几条命令而是把这堆岔路收敛成一份可以版本管理的声明式文件。我在带人做部署的时候有个习惯先问对方你的服务由几个部件组成。如果答案是一个 Web 应用加一个数据库那 Compose 基本就是最优解如果答案是十几个微服务加消息队列再加网关那要么上编排系统要么老老实实按域拆成几份 Compose 文件分机部署。工具选型的第一步永远是先数清楚自己的部件数量而不是先去找教程抄配置。Compose 干的事情可以类比成一份装修施工单。以前你是拿着口头描述让工人服务器现场发挥出了问题谁也说不清当初是怎么定的现在你把每个房间用什么材料、走几路线、接口留在哪全写在一张单子上工人照着干下次换一批工人照着同一张单子还能干出一样的结果。这份单子就是docker-compose.yml它带来的核心收益是环境一致性和可复现性而不是单纯图省事。还有一层常被忽略的价值Compose 天然定义了服务之间的网络拓扑。同一个项目里的容器默认落在同一个自定义网络上可以直接用服务名互相访问不需要记 IP、不需要写死地址。这一点在本地能跑、服务器上连不上数据库这类经典问题里往往是关键差异所在——本地你可能用了localhost而容器里的localhost指的是容器自己不是宿主机。我见过太多人卡在这一步代码里数据库地址写的是127.0.0.1本地跑得好好的部署到服务器上容器一启动就报连接拒绝然后开始怀疑防火墙、怀疑云厂商、怀疑人生。其实原因简单得可笑——容器内的回环地址指向容器自身数据库在另一个容器里当然连不上。理解 Compose 的网络模型比背会十条命令更重要这也是本篇后面会反复回到的一个点。所以这份指南的定位很明确给那些项目已经能在本地跑起来、但还没真正经历过公网环境考验的人。不管你用的是哪种语言栈、哪种数据库只要你的服务可以被容器化下面的路径基本通用。我尽量不写复制粘贴就能跑的空壳配置而是把每一行为什么这么写讲清楚因为线上出问题时能救你的是理解不是复制。2. 上线前的容器化改造镜像怎么打才不给未来埋雷2.1 从能跑到跑得住Dockerfile 的三个关键决策把项目塞进容器第一步是写 Dockerfile。新手最常见的写法是把整个项目目录COPY进去、装依赖、启动命令一写了事本地测试通过就以为万事大吉。实测下来这种镜像往往有几个隐患体积巨大、构建极慢、以 root 身份运行、没有健康检查。它们在演示环境里毫无症状一到公网服务器上就开始暴露。第一个决策是基础镜像的选择。以常见运行时为例官方镜像通常提供完整版和精简版两条线前者自带编译工具链后者只保留运行所需的最小集合。我的建议是分阶段构建阶段用完整版装依赖、编译产物运行阶段用精简版只搬运最终产物。这样做的好处不只是体积小更关键的是攻击面小——线上镜像里不该存在编译器、包管理器这类工具它们对运行毫无帮助只会成为隐患。第二个决策是依赖安装与源码拷贝的顺序。Docker 的构建缓存是按层命中的一旦某一层的内容变了它之后的所有层都要重建。如果把COPY . .放在装依赖之前那么你每改一行业务代码依赖就要重装一遍构建时间从几十秒涨到几分钟。正确顺序是先拷贝依赖清单文件、执行安装再拷贝源码。这样只要依赖清单没变装依赖那层就一直命中缓存。第三个决策是运行用户。容器默认以 root 运行这在单机玩具项目里无所谓但只要端口暴露在公网就属于不必要的风险放大。创建一个普通用户、切换过去再启动进程成本极低收益是把潜在的越权影响限制在一个低权限身份内。这一条我强烈建议所有要上公网的项目都做。FROM node:20-alpine AS builder WORKDIR /app COPY package*.json ./ RUN npm ci --omitdev COPY . . FROM node:20-alpine WORKDIR /app RUN addgroup -S app adduser -S app -G app COPY --frombuilder /app /app USER app EXPOSE 3000 CMD [node, server.js]上面这份示例体现的就是前面说的三点分阶段构建、依赖先行、非 root 运行。注意npm ci而不是npm install前者严格按锁文件安装保证每次构建的依赖树一致这是环境可复现的基本要求。2.2 别把密钥烤进镜像配置与代码的分离比镜像体积更容易出事的是配置管理。我见过有人把数据库密码直接写在代码里然后把代码打进镜像推到公共仓库等到发现时已经晚了。镜像一旦构建完成里面的内容就是可以被任何人拉取解包的把密钥写进镜像等于公开发布。正确做法是通过环境变量注入Compose 里用env_file指向一个服务器本地文件或者用environment声明。这个文件不进版本库只存在于服务器上权限设为仅属主可读。项目里留一份.env.example说明需要哪些变量、格式是什么新人照着填即可。这条规矩看起来是老生常谈但每次复盘线上事故总能看到它的影子。services: app: image: myapp:latest env_file: - .env.production environment: TZ: Asia/ShanghaiTZ这一行值得单独说。容器默认时区通常是 UTC你的日志时间、定时任务触发时间、数据库写入的时间戳全都偏了八小时。排查问题时对不上日志时间能把人逼疯。显式设一次时区一劳永逸。2.3 健康检查让容器在跑和服务可用变成两件事docker ps显示Up不代表你的服务真的能响应请求。进程可能已经死循环、可能卡在初始化、可能连不上数据库但还没退出。Compose 支持healthcheck定期执行一条探测命令根据结果标记容器健康状态。healthcheck: test: [CMD, wget, -qO-, http://localhost:3000/health] interval: 30s timeout: 5s retries: 3 start_period: 40sstart_period这个参数很多人不写导致应用启动慢的时候被误判为不健康。它的含义是给容器一段宽限期这段时间内探测失败不计入重试次数。有初始化逻辑的服务一定要设否则你会看到容器在启动阶段被反复重启。健康检查真正的价值在于依赖编排。数据库容器如果不是健康的应用容器就不该急着启动。用depends_on配合condition: service_healthy可以让启动顺序有实际意义而不是先启动但连不上。3. 服务器侧的准备工作比配置文件更容易出错的地方3.1 系统层面的三件小事时区、防火墙、磁盘拿到一台干净的服务器别急着装 Docker。先把三件事理清楚。时区。宿主机时区和容器时区最好一致否则日志交叉比对时会怀疑自己。系统级设一次容器里再设一次双重保险。防火墙。云服务器的安全组和系统防火墙是两层很多人只改了其中一层。安全组的规则在控制台配置系统防火墙用系统的工具管理。我的习惯是只对外开放必要端口通常是 SSH 端口加 Web 服务端口数据库端口绝对不要对公网开放容器之间在内部网络通信就够了。如果确实需要本地连接数据库调试走 SSH 隧道别图省事直接开端口。磁盘。容器日志默认会无限增长跑上几个月把磁盘写满然后整台机器上的服务一起挂掉。这是极其常见的线上事故。在 Docker 的守护进程配置里给日志设个上限是部署前就该做的动作。{ log-driver: json-file, log-opts: { max-size: 50m, max-file: 3 } }这段配置写进 Docker 的配置文件后重启服务生效含义是单个日志文件最大 50MB最多保留 3 个。总量可控排查时也够用。3.2 装 Docker 时最容易忽略的一步装 Docker 本身没什么难度按官方文档走即可。真正容易忽略的是把当前用户加入 docker 用户组否则每条命令都要加sudo用着别扭脚本里也容易出权限问题。sudo usermod -aG docker $USER执行完要重新登录才生效。这一步做完后面的操作都不需要提权Compose 文件里的相对路径、挂载目录的权限问题也会少很多。还有一个细节Docker 的数据目录默认在系统盘。镜像、容器层、数据卷都堆在那里项目跑久了系统盘告警是常态。如果服务器有独立的数据盘把 Docker 的根目录迁移过去。迁移步骤不复杂停服务、改配置里的>services: app: build: . image: myapp:latest restart: unless-stopped ports: - 127.0.0.1:3000:3000 env_file: - .env.production depends_on: db: condition: service_healthy db: image: postgres:16-alpine restart: unless-stopped environment: POSTGRES_PASSWORD: ${DB_PASSWORD} volumes: - db_data:/var/lib/postgresql/data healthcheck: test: [CMD-SHELL, pg_isready -U postgres] interval: 10s retries: 5 volumes: db_data:逐行看几个关键点。restart: unless-stopped表示除非人为停止否则容器退出就自动拉起。这个策略适合长期运行的服务。为什么要用unless-stopped而不是always因为你在调试时手动stop了一个容器always会在 Docker 服务重启后又把它拉起来而unless-stopped尊重你的手动操作。调试体验差别很大。ports写成127.0.0.1:3000:3000而不是3000:3000这是个非常重要的安全习惯。前者表示只监听本机回环地址外网访问不到后者表示监听所有网卡等于直接对公网开放。通常我们会在这台机器上再放一个反向代理由它对外提供 80/443应用只在内网被代理访问。这样应用端口不直接暴露安全边界清晰。4.2 数据卷容器可以删数据不能丢volumes这一块是新手最容易轻视、出事后最肉疼的部分。容器本身是临时的删了重建里面写入的文件全部消失。数据库的数据目录必须挂到卷上卷的生命周期独立于容器。docker compose down # 容器删除卷保留 docker compose down -v # 容器和卷一起删除数据没了这两个命令的区别我建议你在测试环境亲手试一次感受一下-v的威力。生产环境执行down -v之前务必确认有可用备份。我见过有人在生产机上顺手敲了down -v数据库直接归零恢复靠的是几小时前的备份丢了一段业务数据。除了数据库用户上传的文件、应用生成的缓存只要是需要持久化的都要挂卷或者挂宿主机目录。判断标准很简单这个目录里的东西容器重建后还需要吗需要就挂出去。挂宿主机目录时要注意权限。容器内进程以非 root 用户运行宿主机目录如果属于 root 且权限严格容器写入会报权限拒绝。解决方案是提前把宿主机目录的属主改成对应的用户 ID或者在 Compose 里显式指定user。这个问题在数据库容器上尤其常见因为官方数据库镜像会检查数据目录的属主。4.3 反向代理把端口收敛到一个入口前面提到应用端口只监听回环地址那外部流量怎么进来答案是反向代理。常见选择有 Nginx、Caddy、Traefik选哪个看你的需求。Caddy 的优势是自动申请和续期证书配置极简Nginx 胜在生态成熟、资料多、可控性强。我倾向于把反向代理也放进 Compose作为一个服务统一管理。proxy: image: nginx:alpine restart: unless-stopped ports: - 80:80 - 443:443 volumes: - ./nginx/conf.d:/etc/nginx/conf.d:ro - ./nginx/certs:/etc/nginx/certs:ro depends_on: - app代理转发到应用时目标地址写服务名加容器端口比如proxy_pass http://app:3000;。因为它们在同一个 Compose 网络里服务名会被解析成容器地址。这就是前面说的服务名互访的实际用处配置文件里永远不出现 IP。代理配置里还有几个转发头要带上Host、X-Real-IP、X-Forwarded-For、X-Forwarded-Proto。不带头信息应用拿到的客户端 IP 全是代理的 IP限流、审计、日志全都失真不传X-Forwarded-Proto应用不知道自己被 HTTPS 访问可能生成错误的跳转地址导致重定向循环。4.4 多环境配置的复用思路项目往往有测试环境和生产环境两份 Compose 文件如果完全复制改一处要改两遍迟早不一致。Compose 支持多文件叠加基础文件写公共部分环境文件写差异部分。docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d差异通常包括镜像标签、端口映射、环境变量文件、资源限制。这样公共结构只维护一份环境差异集中在一处复盘时一目了然。这是工程化上很划算的一笔投入。5. 首次部署的完整链路一步一步来别跳步5.1 从拉代码到服务可访问的实操顺序把前面的准备串起来一次完整的首次部署大致是这样一条链路。我按实际操作顺序写并且标注每步的验证方法因为部署最怕的就是执行了但不知道成没成。第一步登录服务器进入项目目录从版本库拉取代码。验证方式确认目录结构和本地一致关键配置文件都在。第二步准备环境变量文件。从示例文件复制一份填入真实值设置权限。cp .env.example .env.production chmod 600 .env.production第三步构建镜像。这一步在服务器上执行耗时取决于项目大小和网络。验证方式看构建日志末尾是否成功然后docker images确认镜像存在且标签正确。docker compose build第四步启动服务。docker compose up -d第五步观察状态。docker compose ps看所有服务是否处于 running 且 healthy。如果某个服务反复重启直接看日志。docker compose logs -f --tail100 app第六步本机验证。在服务器上直接请求应用端口。curl -I http://127.0.0.1:3000/health第七步外部验证。从本地浏览器或命令行访问公网地址确认代理链路通畅。这个顺序的关键在于分层验证先确认容器起来了再确认应用在本机可达最后确认外网可达。三层分开查出问题时能立刻定位到是哪一层的问题。一上来就从外网访问失败了根本不知道是应用没起、端口没开、还是代理配错。5.2 第一次启动数据库的初始化坑数据库容器第一次启动时会执行初始化创建一个空的数据目录设置初始用户和密码。这一步有几个坑值得提前说。第一初始化只在数据目录为空时执行。如果你改了密码环境变量然后重启容器新密码不会生效因为数据目录已经存在初始化逻辑被跳过。这时候要么进容器手动改密码要么清空数据目录重新初始化仅限开发环境。生产环境遇到这种情况正确做法是用数据库自身的工具改密码。第二初始化需要时间尤其是有初始化脚本的时候。应用如果启动太快会在数据库还没准备好时尝试连接然后失败退出。这就是前面强调健康检查和depends_on的原因。但要注意depends_on只保证启动顺序不保证应用自身的重连逻辑。应用侧最好也加上连接重试双保险。第三编码和排序规则。初始化时如果不指定可能拿到默认的字符集后续存中文、emoji 出问题。建库时显式指定 UTF-8 相关配置省得以后迁移数据。5.3 上线后立刻要做的几项加固服务能访问了不等于可以收工。下面这几件事我建议在首次上线当天就做完。改掉默认凭据。数据库的超级用户密码、应用的初始管理员账号全部换成强密码。用密码管理器生成别用能猜到的组合。关闭不必要的端口。回头检查安全组和系统防火墙确认只有 80、443 和运维端口对外开放。数据库端口、应用端口都应该只在内网可见。验证备份可用。备份这事没验证过等于没有。手动导一次数据库恢复到另一个库里确认数据完整。这个过程可能只要十分钟但能在真正出事时救命。配置日志轮转。前面在 Docker 层面设了日志上限应用自身的日志文件也要管。应用写到挂载目录里的日志同样需要轮转策略否则一样会把磁盘写满。6. 上线之后更新、排错与长期维护6.1 一次平滑更新的标准动作项目上线只是开始后面会有无数次更新。我用的更新流程固定为四步减少手滑空间。# 1. 拉取最新代码 git pull # 2. 重新构建镜像 docker compose build app # 3. 只重建变更的服务其他不动 docker compose up -d app # 4. 观察日志和健康状态 docker compose logs -f --tail50 app第 3 步只指定app意味着数据库和代理不受影响重建范围最小。Compose 会对比配置和镜像只重建有变化的容器这是它的内置能力不需要你手动停机。更新前有个习惯动作先备份数据库。涉及数据库结构变更的更新备份是底线。另外如果新版本有问题需要回滚镜像标签要能对应到版本号别全用latest。用latest的后果是回滚时你根本不知道该回哪个因为标签永远指向最新。给镜像打上版本号或提交短哈希回滚就是改一行标签重新up。6.2 常见故障的排查链路线上出问题最忌讳的是瞎改。我习惯按固定链路排查从外到内逐层排除。第一步确认现象范围。是全部不可访问还是某个接口报错是刚部署完就出问题还是运行一段时间后出问题范围决定了排查方向。第二步看代理层。代理容器是否健康配置是否加载成功上游地址是否可达。代理日志里通常能看到明确的错误比如上游超时、连接拒绝。第三步看应用容器状态。docker compose ps看是否在反复重启。反复重启基本等于启动即崩溃直接看日志找报错。第四步看应用日志。从启动日志开始看找第一个报错而不是最后一个。很多人习惯看最新的报错但根因往往在前面。第五步看依赖服务。数据库是否健康连接数是否打满慢查询是否堆积。连接池耗尽是个高频问题表现为应用卡住但不报错。第六步看宿主机资源。CPU、内存、磁盘。磁盘满、内存耗尽触发 OOM都会让服务突然不可用而且日志里可能什么都没有。下面这张表是我整理的常见现象与对应方向的对照实战中能省不少时间。现象优先排查方向典型原因外网完全打不开本机 curl 正常代理、防火墙、安全组端口未放行、代理配置未生效容器反复重启应用启动日志配置缺失、依赖连不上、端口冲突接口偶发超时数据库连接池、资源连接泄漏、慢查询、内存不足日志时间对不上时区配置容器未设时区磁盘突然告警日志、临时文件日志未轮转、临时文件未清理更新后功能异常镜像版本、环境变量标签不对、配置遗漏6.3 几个我用血换来的经验不要在服务器上直接调试业务逻辑。有人喜欢在服务器上改代码试错改完忘了同步回本地下次部署全丢。所有代码改动在本地完成走构建流程。环境变量的名字要统一。代码里读DB_HOSTCompose 里写DATABASE_HOST这种低级不一致排查起来极其折磨。项目初期就定好命名规范写进文档。容器内不要存重要文件。任何写在容器文件系统里的东西容器一重建就没了。养成习惯写之前先问一句这个目录挂出去了吗。时区、编码、权限这三样出问题时都表现得极其诡异。时间错八小时、中文变乱码、莫名其妙权限拒绝症状看起来都和根因不沾边。部署前一次性配好比事后救火划算得多。保留一份从零重建的文档。假设服务器明天挂了换一台新机器你需要多久能恢复把完整步骤写下来每隔一段时间照着走一遍验证。这份文档的价值在你真正需要它的那天会体现得淋漓尽致。6.4 用起来之后才会想到的扩展方向跑顺了之后有些东西可以逐步加上。比如资源限制给每个服务设 CPU 和内存上限防止某个服务失控拖垮整台机器。比如定时备份用系统定时任务定期导出数据库并清理过期备份。比如基础的监控哪怕只是定期探测健康接口、异常时发个通知也比出事后靠用户反馈强。这些都不是上线的前提但它们是从能用到好用的分界线。我的建议是不要一上来全加上先让核心链路稳定跑起来再按实际遇到的问题逐个补。部署这件事过度设计的成本往往比欠设计还高——你没遇到过的问题提前防也防不到点子上。回到最开始那个问题Docker Compose 到底替我们省掉了什么我的答案是它把这台服务器上现在跑着什么、怎么配的、怎么重建从口头记忆变成了一份可审查的文件。真正的门槛从来不在命令本身而在于你是否想清楚了每个服务要什么、依赖谁、数据放哪、出问题怎么查。把这些想明白写配置只是几分钟的事想不明白配置抄得再像线上照样会给你上课。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑