科技与工作流 · 2026-07-05

Docker部署小项目常见坑有哪些

Docker 能把运行环境打包得更清楚,但小项目上线仍要认真处理镜像构建、配置密钥、端口网络、数据持久化、启动顺序、日志和排障证据。

很多个人项目第一次用 Docker 上线时,感觉像是终于找到了一种更省心的部署方式:本地能跑,写个 Dockerfile,再配一个 compose.yaml,服务器上 docker compose up -d,页面就出现了。这个体验确实顺手,也确实是 Docker 的价值之一。它让运行时依赖、启动命令、端口暴露和服务组合变得更可描述,减少了“我电脑上可以”的混乱。

但 Docker 不是把部署问题抹掉,而是把问题重新分层。镜像负责装什么,容器负责怎么跑,Compose 负责多个服务怎么组织,宿主机仍然负责磁盘、网络、防火墙、证书、备份和资源。小项目容易踩坑,往往不是因为 Docker 太复杂,而是因为把这些边界混在一起:把配置写进镜像,把数据库数据留在容器临时层,把端口映射理解错,把服务启动顺序当成可用性,把日志留到磁盘被写满。

如果只记一句话:Docker 部署要问的不是“容器起来了吗”,而是“镜像能复现吗、配置可替换吗、数据能保住吗、网络路径说得清吗、出错时能找到证据吗”。这篇文章不做复杂教程,只把小项目上线最常见的坑拆成几类,帮你在部署前多看一眼。

容器结构图

坑一:把镜像当成项目压缩包

第一个常见错误,是一次性把整个项目目录送进构建上下文。Docker 的 build context 会把构建所需文件发送给 builder;官方文档建议用 .dockerignore 排除不相关文件,避免把 node_modules、日志、临时产物、测试截图、.git 目录甚至本地配置一起带进去。对小项目来说,这个坑很隐蔽:镜像能构建成功,但构建慢、镜像大、缓存经常失效,还可能把不该进入镜像的文件带进发布物。

更稳的做法,是把 Dockerfile 写成“运行所需文件清单”。例如 Node 项目先复制 package.json 和锁文件安装依赖,再复制源码;Python 项目先处理依赖文件,再复制应用代码;前端项目用多阶段构建,只把打包后的静态产物放入最终镜像。Docker 官方构建最佳实践也强调 multi-stage builds、选择合适基础镜像、排除不相关文件、不要安装不必要包、利用 build cache、在 CI 中构建和测试镜像。

小项目不一定要追求极致镜像大小,但至少要避免三个信号:镜像里有源代码之外的大量缓存,构建日志里反复上传很大的 context,改一行业务代码就导致依赖层全部重建。它们说明镜像还没有被当作可复现的运行单元,而只是被当成了一份“能跑的目录备份”。

坑二:把配置和密钥写进镜像

第二个坑,是把数据库密码、API Key、后台口令、生产域名等配置写进 Dockerfile 或源码。这样做短期省事,长期会让镜像无法在不同环境复用,也会让敏感信息进入镜像历史、代码仓库或构建日志。Docker Compose 官方文档明确提醒,不要用环境变量传递密码等敏感信息,应该使用 secrets;Compose secrets 会以文件形式挂载到容器内 /run/secrets/<secret_name>,并按服务授权访问。

环境变量本身并不是坏东西。Compose 支持 environmentenv_file.env 插值和命令行临时覆盖,也有明确的优先级规则。问题在于把“普通配置”和“敏感凭据”混在一起,把 .env 当作可以随便提交的项目文件,或者不知道最终进入容器的值来自哪里。小项目常见现象是:本地 .env 能跑,服务器上没有同名变量,Compose 插值后变成空字符串;或者临时在 shell 里设过变量,结果覆盖了 .env,线上行为和文件里看到的不一致。

部署前可以做一个简单分流:端口、运行模式、公开域名、功能开关属于普通配置,可以放入环境变量或环境文件;数据库密码、令牌、私钥、证书口令属于敏感凭据,应进入 secrets、专用密钥管理或服务器受控文件。无论哪种方式,都要能回答“谁设置了这个值、上线时怎么替换、日志里会不会出现”。

坑三:误解端口、服务名和访问路径

第三个坑,是网络路径想不清。容器内部监听的端口、Compose 网络里的服务名、宿主机暴露的端口、反向代理访问的端口,是四个不同层次。Docker 网络文档说明,容器在 bridge 网络中默认可被同网络内的容器和宿主机访问,但要让宿主机外部访问,需要通过 --publish-p 发布端口。Compose 默认会为应用创建网络,同一网络内的服务可以用服务名互相发现,例如 Web 服务连接数据库时用 db:5432,宿主机访问同一个服务可能要走 localhost:映射端口

很多小项目的错误来自把这几层写反。应用在容器内监听 127.0.0.1,外部代理怎么都连不上;数据库连接串在容器里写 localhost,结果连到自己而不是数据库服务;把容器端口和宿主机端口记混,导致防火墙开错;用固定 IP 连接另一个容器,更新后 IP 变了,服务名却一直可用。

更好的习惯,是画一张访问路径图:浏览器到 Nginx,Nginx 到宿主机端口,宿主机端口映射到容器端口,应用再通过 Compose 服务名访问数据库或缓存。只要图画不清,线上排障时就会在“端口没开、服务没起、代理错了、应用监听错了”之间来回猜。

网络和数据边界图

坑四:把数据留在容器可写层

第四个坑,是以为容器里的数据会自然保留。Docker 官方 volumes 文档把 volume 定义为由 Docker 创建和管理的持久数据存储,并说明 volumes 是持久化 Docker 容器生成和使用的数据的优选机制。它还提醒,volume 比直接写入容器可写层更适合持久数据,因为容器可写层会受存储驱动管理,重建容器时也不适合作为数据归宿。

小项目最容易丢的不是代码,而是上传文件、SQLite 数据库、用户头像、导出的报表、缓存索引、搜索数据、任务队列状态。第一次部署时它们都在容器里,看起来没问题;等你更新镜像、重建容器、清理旧资源,才发现数据跟着旧容器一起没了。更麻烦的是,有些数据不是完全丢失,而是散落在多个旧容器层或宿主机路径里,恢复起来很难。

区分 volume 和 bind mount 也重要。volume 由 Docker 管理,更适合数据库、上传目录等运行数据;bind mount 直接把宿主机路径挂进容器,适合开发时同步源码,或确实需要宿主机和容器同时访问某个目录的场景。小项目上线可以简单一点:数据库和上传目录使用命名 volume,并写清备份命令;需要人工查看的导出文件再考虑 bind mount。不要把“容器没删就还在”当作持久化策略。

坑五:把启动顺序当作服务可用

第五个坑,是认为 depends_on 写了顺序,应用就一定能连上数据库。Compose 官方启动顺序文档说明,Compose 会按依赖顺序创建和停止服务;如果依赖标记了 service_healthy,会等待对应 healthcheck 通过后再创建依赖它的服务。这里的差别需要分清:一个数据库容器被创建,不等于数据库已经能接受连接;一个缓存服务进程启动,不等于它已经加载完数据。

小项目常见现象是服务器重启后 Web 容器先起来,数据库还在初始化,应用连接失败后退出;或者数据库服务一时不可用,应用没有重试,Compose 又不断重启,日志看起来像随机故障。更稳的做法是:给数据库、缓存、后端服务写合适的 healthcheck;应用启动时做有限重试;依赖关系只表达启动顺序,不替代应用自己的连接容错。

这里也要避免过度设计。个人项目不一定需要复杂编排系统,但至少应该能回答:服务刚启动时谁先准备好,失败后谁负责重试,容器重启策略是什么,健康状态在哪里看。否则“容器状态是 running”会给人一种已经可用的错觉。

坑六:日志没有出口,也没有边界

第六个坑,是上线后只会看 docker logs,但没有规划日志容量。Docker 日志文档说明,Docker 有多种 logging driver;默认常见配置下会使用 json-file 记录容器日志。官方配置页还提醒,在没有日志轮转时,默认 JSON 日志可能占用大量磁盘空间;在其他场景中,local 日志驱动可用于减少磁盘耗尽风险,并默认进行轮转。

对小项目来说,日志规划不需要很重,但不能没有。至少要知道:应用日志是写到 stdout/stderr,还是写进容器里的某个文件;容器日志由哪个 driver 接管;磁盘满之前有没有轮转;错误日志能不能定位到请求、任务或用户操作;更新后旧容器日志是否还需要保留。很多“突然打不开”的事故,最后不是代码坏了,而是磁盘被日志写满、数据库无法继续写入、证书更新任务失败。

更实用的排障顺序是:先看 docker compose ps 确认服务状态,再看 docker compose logs --tail 定位错误,再看端口映射和网络连通,再看 volume 是否挂载到预期路径,最后看宿主机磁盘、内存和防火墙。把这个顺序写下来,比上线后临时翻命令更可靠。

排障流程图

坑七:没有把发布动作变成清单

最后一个坑,是每次上线都靠记忆。小项目变化快,作者也常常一个人负责开发、部署、域名、证书、数据库、备份和监控。靠记忆上线,最容易漏掉“构建新镜像、拉取新镜像、确认 env、迁移数据库、备份 volume、检查日志、验证页面、保留恢复路径”这些细节。

一个轻量清单就够用:构建前检查 .dockerignore,确认镜像标签不是随手的 latest;启动前运行 docker compose config 看最终配置;替换配置前确认敏感凭据不进仓库;更新服务前备份数据库和上传目录;启动后检查 healthcheck、日志、端口和页面;出现问题时保留旧镜像和旧 volume,不要先清理证据。这里的目标不是流程繁琐,而是让每次上线留下可追溯的判断。

Docker 适合小项目,不是因为它让部署变得没有风险,而是因为它能把很多风险写成文件、命令和检查点。只要你把镜像、配置、网络、数据、启动、日志这六件事分开看,很多上线猜测就会变成可以逐项排查的工程问题。

上线清单

结语:容器是边界,不是保险

Docker 最适合帮小项目建立边界:应用和运行时依赖的边界,服务之间的网络边界,临时文件和持久数据的边界,普通配置和敏感凭据的边界,启动状态和可用状态的边界。边界越清楚,部署越容易复盘;边界越混乱,问题越容易在“容器已经起来了”之后才出现。

所以下次小项目准备上线时,不妨先做一次十分钟检查:镜像能不能从干净环境构建,.dockerignore 是否排除了无关文件,配置和凭据是否分开,端口链路是否画得出来,数据是否进了 volume,启动是否有健康检查,日志是否会轮转,出现问题能否按证据恢复服务。能回答这些问题,Docker 才不只是一个启动命令,而会变成小项目长期维护的一部分。