Docker 容器化部署:从手动运维到一键发布,我踩过的 7 个坑
还在手动 SSH 登录服务器部署项目?Out 了!本文分享 Docker 容器化部署的完整实战经验,包括镜像构建优化、多阶段构建技巧、容器网络配置、数据持久化方案和 CI/CD 集成。每个坑都是真金白银换来的,帮你从"部署两小时"变成"发布五分钟"。工具只是手段,稳定高效才是目的。Docker 不是银弹,它解决了一些问题,也带来了一些新问题。但总体来说,从手动运维到容器化,效率提升是实实在在的。从
摘要
还在手动 SSH 登录服务器部署项目?Out 了!本文分享 Docker 容器化部署的完整实战经验,包括镜像构建优化、多阶段构建技巧、容器网络配置、数据持久化方案和 CI/CD 集成。每个坑都是真金白银换来的,帮你从"部署两小时"变成"发布五分钟"。

开篇引入
说实话,三年前我第一次接触 Docker 的时候,内心是拒绝的。
"不就是个打包工具吗?能有多复杂?"
结果第一个生产环境部署,我就栽了个大跟头。凌晨两点,容器起不来,日志全是报错,用户访问 502,老板在群里@我:"什么时候能好?"
那一晚我明白了:容器化不是把代码塞进镜像就完事了,这里面的门道,够写一本书。
三年过去,我从手动 SSH 部署进化到一键发布,从单容器到 Kubernetes 集群,踩过的坑没有 100 也有 80。今天把最痛的 7 个坑整理出来,希望能帮你少走点弯路。

坑一:镜像构建又慢又大
问题场景
刚开始写 Dockerfile,我是这么干的:
FROM node:18
WORKDIR /app
COPY . .
RUN npm install
RUN npm run build
EXPOSE 3000
CMD ["node", "dist/main.js"]
看着挺简洁对吧?实际问题一大堆:
-
构建慢:每次改一行代码,
npm install重新跑一遍,10 分钟起步 -
镜像大:基础镜像 + node_modules + 构建工具,轻松 1.5GB+
-
安全隐患:生产镜像里塞满了开发依赖
解决方案:多阶段构建
后来我学乖了,用多阶段构建:
# 构建阶段
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
# 生产阶段
FROM node:18-alpine
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY --from=builder /app/package.json ./
ENV NODE_ENV=production
EXPOSE3000
CMD ["node", "dist/main.js"]
效果对比:
|
指标 |
优化前 |
优化后 |
|---|---|---|
|
构建时间 |
10 分钟 |
2 分钟 |
|
镜像大小 |
1.5GB |
180MB |
|
启动速度 |
30 秒 |
5 秒 |
关键技巧:
-
**用
npm ci代替npm install**:ci 是 clean install 的缩写,专门给 CI/CD 设计的,速度更快且保证依赖版本一致 -
先复制 package.json:利用 Docker 层缓存,依赖不变时直接跳过安装
-
用 Alpine 基础镜像:体积小、安全,但注意有些包可能需要额外编译依赖
坑二:容器网络配置混乱
问题场景
"为什么容器里访问不到数据库?"
"为什么两个容器互相 ping 不通?"
这是我被问得最多的两个问题。Docker 网络模型确实有点反直觉,我刚开始也懵。
解决方案:理解三种网络模式
1. bridge 模式(默认)
每个容器有独立 IP,通过端口映射对外暴露:
docker run -p 3000:3000 my-app
适合单机部署,简单直接。
2. host 模式
容器直接用宿主机网络,性能最好但隔离性差:
docker run --network host my-app
我一般只在性能敏感场景用这个,比如高频交易系统。
3. overlay 网络(多机通信)
多容器协作时,创建自定义网络:
docker network create app-network
docker run -d --network app-network --name db postgres
docker run -d --network app-network --name api my-api
这样 api 容器里直接用 db 做主机名就能访问数据库,不用记 IP,容器重启也不影响。
血泪教训
有一次生产事故,我把数据库容器和 API 容器放在不同网络里,死活连不上。排查两小时,最后发现是网络配置问题。
建议:用 docker network inspect <network-name> 随时检查网络拓扑,别靠猜。
坑三:数据持久化没做好
问题场景
"容器删了,数据也没了?"
对,默认情况下容器是"用完即扔"的,数据存在容器层,容器一删全没了。
解决方案:三种持久化方案
1. Volume(推荐)
Docker 管理的持久化存储:
docker volume create db-data
docker run -v db-data:/var/lib/postgresql/data postgres
优点:跨容器共享、备份方便、性能好
2. Bind Mount
直接挂载宿主机目录:
docker run -v /home/user/data:/app/data my-app
优点:开发调试方便,直接改宿主机文件
缺点:依赖宿主机路径,迁移麻烦
3. tmpfs
内存存储,重启就丢:
docker run --tmpfs /app/tmp my-app
适合存临时文件、session 数据。
我的选择
生产环境:Volume
开发环境:Bind Mount(改代码即时生效)
敏感数据:加密 Volume + 定期备份
坑四:环境变量管理混乱
问题场景
"测试环境配置怎么跑到生产了?"
"API 密钥怎么提交到 Git 了?"
环境变量管理不好,轻则配置错误,重则密钥泄露。
解决方案:分层管理
1. .env 文件(开发环境)
# .env.local
DATABASE_URL=postgres://localhost:5432/dev_db
API_KEY=dev_key_123
注意:.env.local 要进 .gitignore,别提交!
2. Docker Compose 环境变量
version: '3.8'
services:
api:
image: my-api
env_file:
- .env.production
environment:
- NODE_ENV=production
3. 密钥管理(生产环境)
用 Docker Swarm Secrets 或 Kubernetes Secrets:
echo "my_secret_password" | docker secret create db_password -
核心原则:
-
开发配置本地化,不提交代码
-
生产配置自动化,走 CI/CD 流水线
-
密钥加密存储,不写死在代码里
坑五:日志收集不到位
问题场景
"线上出问题了,去哪看日志?"
"容器重启了,之前的日志还能查到吗?"
默认情况下,Docker 日志存在容器里,容器删了就没了。
解决方案:集中式日志收集
方案一:docker logs + 日志驱动
docker run --log-driver=json-file --log-opt max-size=10m my-app
限制单个日志文件大小,避免撑爆磁盘。
方案二:ELK 栈(推荐)
version: '3.8'
services:
api:
image:my-api
logging:
driver:fluentd
options:
fluentd-address:localhost:24224
fluentd:
image:fluent/fluentd
ports:
-"24224:24224"
elasticsearch:
image:elasticsearch:8.11.0
kibana:
image:kibana:8.11.0
方案三:云服务商日志服务
阿里云 SLS、腾讯云 CLS、AWS CloudWatch,省心但贵。
我的建议
小项目:docker logs + 日志轮转
中大型项目:ELK 或云日志服务
关键指标:日志保留至少 30 天,支持关键词搜索和告警。
坑六:健康检查没配置
问题场景
"容器明明挂了,为什么 Docker 还显示 running?"
因为进程还在,只是业务逻辑崩了。Docker 不知道,以为一切正常。
解决方案:配置 HEALTHCHECK
FROM node:18-alpine
WORKDIR /app
COPY . .
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=40s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]
参数解释:
-
--interval=30s:每 30 秒检查一次 -
--timeout=3s:超时 3 秒算失败 -
--start-period=40s:启动后 40 秒内不检查(给启动时间) -
--retries=3:连续 3 次失败才标记 unhealthy
配合 Docker Compose 自动重启:
services:
api:
restart: always
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3000/health"]
interval: 30s
retries: 3
这样容器挂了会自动重启,不用半夜起来救火。
坑七:CI/CD 集成没打通
问题场景
"每次发布都要手动打包、手动上传、手动重启?"
这都 2026 年了,真没必要。
解决方案:GitHub Actions 自动化
name: Deploy
on:
push:
branches:[main]
jobs:
build-and-deploy:
runs-on:ubuntu-latest
steps:
-uses:actions/checkout@v4
-name:BuildDockerImage
run:dockerbuild-tmy-app:${{github.sha}}.
-name:PushtoRegistry
run:|
docker login -u ${{ secrets.DOCKER_USER }} -p ${{ secrets.DOCKER_PASS }}
docker push my-app:${{ github.sha }}
-name:DeploytoServer
uses:appleboy/ssh-action@master
with:
host:${{secrets.SERVER_HOST}}
username:${{secrets.SERVER_USER}}
key:${{secrets.SSH_KEY}}
script: |
docker pull my-app:${{ github.sha }}
docker stop api || true
docker rm api || true
docker run -d --name api my-app:${{ github.sha }}
效果:
-
提交代码 → 自动构建 → 自动部署
-
全程 5 分钟,不用人工干预
-
出问题了?
git revert一键回滚
总结
三年容器化实践,我最深的体会是:工具只是手段,稳定高效才是目的。
Docker 不是银弹,它解决了一些问题,也带来了一些新问题。但总体来说,从手动运维到容器化,效率提升是实实在在的。
最后给几个建议:
-
从小项目开始练手,别一上来就搞生产环境
-
多读官方文档,很多坑官方都给了最佳实践
-
监控和日志要提前配好,别等出问题了再补
-
定期更新基础镜像,安全补丁不能省
互动时间:
你在容器化部署中踩过哪些坑?欢迎在评论区分享,我挑三个最痛的坑,下期专门写文章分析解决方案。
觉得有用? 点赞 + 在看,让更多开发者少踩点坑。
关注我,下期聊《Kubernetes 入门:从 Docker 到 K8s,我的迁移实战》。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)