Docker 实战踩坑总结:从镜像构建到生产部署的配置清单
Docker 使用避坑指南|附配置文件
Docker 作为当前最常用的容器化工具之一,已经广泛应用于本地开发、测试环境、CI/CD、微服务部署以及生产运维等场景。它能够帮助我们将应用及其依赖打包成统一的镜像,实现“一次构建,到处运行”。
但在实际使用 Docker 的过程中,很多问题并不会出现在入门教程里,而是在项目上线、多人协作、服务器迁移、持续集成、生产环境运行时逐渐暴露出来。例如:镜像越构建越大、容器数据丢失、端口冲突、权限异常、网络不通、日志撑爆磁盘、容器重启失败、配置写死、Dockerfile 缓存失效等。
本文将围绕 Docker 的常见使用误区进行系统梳理,并提供可直接参考的 Dockerfile、docker-compose.yml、.dockerignore、Nginx 配置等示例文件,帮助你在日常开发和生产部署中少踩坑。
一、不要把 Docker 当成虚拟机使用
很多初学者容易把 Docker 容器理解成一台“轻量级虚拟机”,然后在容器中安装各种工具、启动多个服务、手动修改文件、进入容器排查后直接改配置。这种使用方式并不推荐。
Docker 容器的核心思想是:
一个容器只做一件事,容器本身应当是可销毁、可重建、可替换的。
常见错误做法
docker exec -it app bash
apt update
apt install vim -y
vim /etc/nginx/nginx.conf
service nginx restart
这种方式虽然临时有效,但问题很明显:
- 修改不会体现在镜像中;
- 容器删除后改动全部丢失;
- 其他环境无法复现;
- 运维和开发无法追踪配置变更;
- 不利于自动化部署。
正确做法
应该将依赖安装、配置文件、启动命令全部写入 Dockerfile 或 docker-compose.yml 中,通过镜像和配置文件统一管理。
例如:
FROM nginx:1.25-alpine
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY ./dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
这样容器即使被删除,也可以通过镜像快速重建,环境保持一致。
二、Dockerfile 编写避坑
Dockerfile 是构建镜像的核心文件。写得好,镜像小、构建快、安全性高;写得差,则会导致镜像臃肿、缓存失效、构建缓慢,甚至泄露敏感信息。
1. 不要使用 latest 标签
很多教程中会看到类似写法:
FROM node:latest
这在学习阶段问题不大,但在生产环境中非常危险。latest 并不代表稳定版本,它只是一个标签,可能随着官方镜像更新而指向新的版本。
如果某次构建时基础镜像发生变化,可能导致:
- Node、Java、Python 等运行时版本变化;
- 系统依赖变化;
- 构建结果不可预测;
- 本地正常,线上异常;
- 回滚困难。
推荐写法
FROM node:20.11.1-alpine
或:
FROM python:3.11.7-slim
固定版本能够保证构建过程更加稳定。
2. 合理利用 Docker 构建缓存
Dockerfile 每一行指令都会生成一层镜像缓存。如果上层发生变化,下层缓存也会失效。
以 Node.js 项目为例,很多人会这样写:
FROM node:20-alpine
WORKDIR /app
COPY . .
RUN npm install
CMD ["npm", "run", "start"]
这种写法的问题是:只要任意代码文件发生变化,COPY . . 就会导致后续 npm install 缓存失效,每次都重新安装依赖。
推荐写法
FROM node:20.11.1-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --registry=https://registry.npmmirror.com
COPY . .
EXPOSE 3000
CMD ["npm", "run", "start"]
这样只有 package.json 或 package-lock.json 变化时,才会重新安装依赖,构建速度会明显提升。
3. 使用多阶段构建减少镜像体积
很多前端或后端项目在构建阶段需要大量依赖,但运行阶段并不需要。例如前端项目需要 Node.js 构建静态文件,但最终只需要 Nginx 提供静态资源服务。
前端项目 Dockerfile 示例
# 第一阶段:构建阶段
FROM node:20.11.1-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --registry=https://registry.npmmirror.com
COPY . .
RUN npm run build
# 第二阶段:运行阶段
FROM nginx:1.25-alpine
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
这种方式的好处是:
- 最终镜像不包含 Node.js;
- 不包含源代码和构建依赖;
- 镜像体积更小;
- 安全性更高;
- 启动速度更快。
4. 不要在镜像中写入敏感信息
错误示例:
ENV MYSQL_PASSWORD=123456
ENV JWT_SECRET=my_secret_key
ENV ACCESS_KEY=xxxxxxxx
镜像一旦构建完成,环境变量、构建历史等信息可能被查看。如果镜像上传到公共仓库,风险更大。
可以通过以下命令查看镜像历史:
docker history image_name
推荐方式
敏感信息应通过以下方式传入:
- docker-compose 的 env_file;
- Kubernetes Secret;
- CI/CD Secret;
- 云厂商密钥管理服务;
- Docker Swarm Secret。
三、.dockerignore 文件不能忽略
很多人会忽略 .dockerignore 文件,导致构建上下文过大,镜像构建缓慢,甚至把本地敏感文件打进镜像。
比如项目目录中可能包含:
- node_modules;
- .git;
- dist;
- logs;
- .env;
- IDE 配置;
- 临时文件;
- 本地缓存。
如果没有 .dockerignore,执行:
docker build -t my-app .
Docker 会将整个目录发送给 Docker daemon,目录越大,构建越慢。
推荐 .dockerignore
.git
.gitignore
node_modules
npm-debug.log
yarn-error.log
pnpm-lock.yaml
dist
build
coverage
.env
.env.*
*.local
logs
*.log
.DS_Store
.idea
.vscode
docker-compose.override.yml
README.md
需要注意的是,如果你的构建依赖 dist 或 .env.example,就不要盲目忽略,应根据项目实际情况调整。
四、容器数据不要只放在容器内部
Docker 容器默认是临时的。容器删除后,容器内部新增或修改的数据也会随之丢失。
错误示例:
docker run -d --name mysql mysql:8
此时 MySQL 数据存储在容器内部。如果容器被删除,数据也可能丢失。
推荐使用 volume
docker volume create mysql_data
docker run -d \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql_data:/var/lib/mysql \
-p 3306:3306 \
mysql:8.0
docker-compose 示例
version: "3.9"
services:
mysql:
image: mysql:8.0.36
container_name: mysql
restart: always
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: app_db
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/conf.d:/etc/mysql/conf.d
- ./mysql/init:/docker-entrypoint-initdb.d
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
volumes:
mysql_data:
使用 volume 可以确保数据生命周期独立于容器生命周期。
五、端口映射要规划清楚
Docker 端口映射格式为:
宿主机端口:容器端口
例如:
docker run -p 8080:80 nginx
表示访问宿主机的 8080 端口,会转发到容器内的 80 端口。
常见坑点
1. 端口被占用
docker: Error response from daemon: driver failed programming external connectivity
可以使用以下命令查看端口占用:
lsof -i :8080
或:
netstat -tunlp | grep 8080
2. 宿主机端口和容器端口混淆
如果应用在容器内监听的是 3000,你写成:
ports:
- "8080:80"
就会访问失败。应该写成:
ports:
- "8080:3000"
3. 应用只监听 127.0.0.1
很多开发框架默认监听 localhost,在容器中会导致外部访问不到。
错误示例:
app.listen(3000, "127.0.0.1")
推荐:
app.listen(3000, "0.0.0.0")
容器内服务需要监听 0.0.0.0,才能被容器网络或端口映射访问。
六、容器日志一定要限制大小
Docker 默认日志驱动通常是 json-file。如果不做限制,容器日志可能持续增长,最终撑满磁盘。
可以通过以下命令查看容器日志位置:
docker inspect 容器ID | grep LogPath
单个服务配置日志限制
services:
app:
image: my-app:1.0.0
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
这表示单个日志文件最大 100MB,最多保留 3 个文件。
Docker daemon 全局配置
创建或修改:
sudo vim /etc/docker/daemon.json
配置如下:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
重启 Docker:
sudo systemctl daemon-reload
sudo systemctl restart docker
注意:全局配置通常只对新创建的容器生效,已有容器需要重新创建。
七、restart 策略不要乱用
Docker 提供了多种重启策略:
restart: "no"
restart: always
restart: unless-stopped
restart: on-failure
常用说明
| 策略 | 说明 |
|---|---|
| no | 不自动重启 |
| always | 无论退出原因,都会自动重启 |
| unless-stopped | 除非手动停止,否则自动重启 |
| on-failure | 只有非 0 状态退出才重启 |
推荐用法
生产环境中通常推荐:
restart: unless-stopped
如果是任务型容器,如数据库迁移、一次性脚本,不应使用 always,否则任务执行完后可能不断重启。
八、容器时间及时区问题
很多镜像默认使用 UTC 时间,导致日志时间与本地时间相差 8 小时。常见解决方式有两种。
方式一:设置环境变量
environment:
TZ: Asia/Shanghai
方式二:挂载宿主机时区文件
volumes:
- /etc/localtime:/etc/localtime:ro
完整示例:
services:
app:
image: my-app:1.0.0
environment:
TZ: Asia/Shanghai
volumes:
- /etc/localtime:/etc/localtime:ro
一般情况下,设置 TZ 已经足够。如果镜像不支持,则考虑挂载时区文件。
九、docker-compose 使用避坑
docker-compose 非常适合管理多容器应用,如 Web 服务、数据库、Redis、Nginx、消息队列等。但如果配置不规范,后期维护成本会很高。
1. 不要把所有配置写死
错误示例:
environment:
MYSQL_ROOT_PASSWORD: 123456
REDIS_PASSWORD: abc123
推荐使用 .env 文件:
MYSQL_ROOT_PASSWORD=your_mysql_password
REDIS_PASSWORD=your_redis_password
APP_PORT=3000
docker-compose.yml:
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
这样可以实现不同环境使用不同配置。
2. depends_on 不等于服务可用
很多人认为:
depends_on:
- mysql
表示 MySQL 准备好了,应用才启动。实际上,depends_on 只保证容器启动顺序,不保证服务已经可连接。
应用如果启动时立即连接数据库,可能会报错。
解决方式
- 应用自身增加重试机制;
- 使用 healthcheck;
- 使用 wait-for-it、dockerize 等等待脚本。
healthcheck 示例
services:
mysql:
image: mysql:8.0.36
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-p${MYSQL_ROOT_PASSWORD}"]
interval: 10s
timeout: 5s
retries: 5
应用服务:
services:
app:
build: .
depends_on:
mysql:
condition: service_healthy
注意:不同版本的 Compose 对 condition 支持情况可能不同,使用前应确认本地 Docker Compose 版本。
十、完整 docker-compose.yml 示例
下面是一个较完整的后端服务部署示例,包含应用、MySQL、Redis 和 Nginx。
version: "3.9"
services:
app:
build:
context: .
dockerfile: Dockerfile
image: my-app:1.0.0
container_name: my-app
restart: unless-stopped
env_file:
- .env
environment:
TZ: Asia/Shanghai
NODE_ENV: production
ports:
- "${APP_PORT}:3000"
volumes:
- ./logs:/app/logs
depends_on:
- mysql
- redis
networks:
- app_net
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
mysql:
image: mysql:8.0.36
container_name: my-mysql
restart: unless-stopped
env_file:
- .env
environment:
TZ: Asia/Shanghai
MYSQL_DATABASE: app_db
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
ports:
- "3306:3306"
volumes:
- mysql_data:/var/lib/mysql
- ./mysql/conf.d:/etc/mysql/conf.d
- ./mysql/init:/docker-entrypoint-initdb.d
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
networks:
- app_net
logging:
driver: "json-file"
options:
max-size: "100m"
max-file: "3"
redis:
image: redis:7.2-alpine
container_name: my-redis
restart: unless-stopped
command: redis-server --requirepass ${REDIS_PASSWORD} --appendonly yes
environment:
TZ: Asia/Shanghai
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- app_net
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
nginx:
image: nginx:1.25-alpine
container_name: my-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- ./nginx/logs:/var/log/nginx
depends_on:
- app
networks:
- app_net
logging:
driver: "json-file"
options:
max-size: "50m"
max-file: "3"
networks:
app_net:
driver: bridge
volumes:
mysql_data:
redis_data:
十一、Nginx 反向代理配置示例
./nginx/default.conf:
server {
listen 80;
server_name example.com;
client_max_body_size 50m;
location / {
proxy_pass http://app:3000;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
}
}
在 Docker Compose 的同一个网络中,Nginx 可以直接通过服务名 app 访问后端容器,而不需要写容器 IP。容器 IP 可能变化,服务名更加稳定。
十二、生产环境安全建议
Docker 并不是天然安全的,生产环境使用时需要注意以下几点。
1. 尽量不要使用 root 用户运行应用
很多官方镜像默认使用 root 用户。应用如果被攻击,攻击者可能获得容器内 root 权限,进一步增加逃逸风险。
Node.js 示例:
FROM node:20.11.1-alpine
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --only=production
COPY . .
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser
EXPOSE 3000
CMD ["node", "server.js"]
2. 最小化镜像
优先选择:
alpine
slim
distroless
但也要注意 Alpine 使用 musl libc,某些依赖可能存在兼容问题。如果遇到奇怪的编译或运行错误,可以尝试 slim 版本。
3. 不暴露不必要端口
数据库和 Redis 如果只给内部应用使用,可以不映射到宿主机端口:
redis:
image: redis:7.2-alpine
expose:
- "6379"
expose 只在 Docker 网络内部暴露,不会映射到宿主机。
4. 定期清理无用资源
Docker 长期运行后会积累大量无用镜像、容器、网络、构建缓存。
查看磁盘占用:
docker system df
清理无用资源:
docker system prune
清理更彻底:
docker system prune -a
注意:-a 会删除所有未被容器使用的镜像,执行前务必确认。
十三、常用排查命令
查看容器状态
docker ps
docker ps -a
查看容器日志
docker logs -f --tail=200 容器名
进入容器
docker exec -it 容器名 sh
如果镜像中有 bash:
docker exec -it 容器名 bash
查看容器详细信息
docker inspect 容器名
查看网络
docker network ls
docker network inspect 网络名
查看资源占用
docker stats
查看镜像
docker images
删除停止的容器
docker container prune
十四、推荐项目目录结构
一个较规范的 Docker 项目目录可以参考如下:
project
├── Dockerfile
├── docker-compose.yml
├── .dockerignore
├── .env
├── package.json
├── package-lock.json
├── src
│ └── ...
├── logs
├── nginx
│ └── default.conf
├── mysql
│ ├── conf.d
│ │ └── my.cnf
│ └── init
│ └── init.sql
└── README.md
MySQL 配置示例 mysql/conf.d/my.cnf:
[mysqld]
default-time-zone = '+08:00'
character-set-server = utf8mb4
collation-server = utf8mb4_unicode_ci
max_connections = 500
[client]
default-character-set = utf8mb4
十五、上线前检查清单
在将 Docker 服务部署到生产环境前,建议按照以下清单检查:
- [ ] 基础镜像是否固定版本,避免使用
latest; - [ ] 是否编写
.dockerignore; - [ ] 是否使用多阶段构建;
- [ ] 是否避免将密钥写入镜像;
- [ ] 是否配置日志大小限制;
- [ ] 数据库、Redis 是否使用 volume 持久化;
- [ ] 是否设置合适的 restart 策略;
- [ ] 应用是否监听
0.0.0.0; - [ ] 容器时区是否正确;
- [ ] 不必要的端口是否未暴露到公网;
- [ ] 是否配置健康检查;
- [ ] 是否设置资源限制;
- [ ] 是否准备备份和恢复方案;
- [ ] 是否验证过容器重启、宿主机重启后的恢复情况。
总结
Docker 能够显著提升应用交付效率,但它并不是简单地把应用“塞进容器”就万事大吉。真正稳定的 Docker 使用方式,需要关注镜像构建、配置管理、数据持久化、网络通信、日志控制、安全权限、服务编排和故障排查等多个方面。
对于个人项目,Docker 可以让环境搭建更简单;对于团队项目,Docker 可以让开发、测试、生产环境更加一致;对于生产系统,Docker 则需要配合规范的配置文件、监控、日志、备份和安全策略一起使用。
记住几个核心原则:
- 容器是可销毁的,数据必须持久化;
- 镜像应当可重复构建,版本必须可追踪;
- 配置不要写死,敏感信息不要进镜像;
- 一个容器尽量只做一件事;
- 日志、端口、权限、网络都要提前规划;
- 生产环境不要依赖手工进入容器修改配置。
只要遵循这些原则,Docker 不仅能提升部署效率,也能让系统更加稳定、可维护、易迁移。