Docker 部署少踩坑:从容器配置到 Compose 一键上线
Docker 使用避坑指南|一键部署
在现代软件开发与运维体系中,Docker 已经成为非常重要的基础工具。无论是个人开发者搭建本地环境,还是企业团队进行微服务部署、持续集成、持续交付,Docker 都能显著提升环境一致性与部署效率。
不过,Docker 虽然上手简单,但真正用好并不容易。很多人在使用过程中会遇到各种“坑”:镜像越来越大、容器数据丢失、端口冲突、权限异常、网络不通、服务重启后配置失效、生产环境直接使用 latest 标签导致版本不可控等。
本文将围绕 Docker 使用避坑指南 和 一键部署实践 展开,帮助你更稳妥地使用 Docker,避免常见错误,并掌握一套适合日常开发和生产部署的思路。
一、为什么要使用 Docker?
在 Docker 出现之前,部署一个应用通常需要在服务器上手动安装各种依赖。例如:
- 安装指定版本的 Java、Node.js、Python、Go 运行环境;
- 配置 Nginx、MySQL、Redis 等基础服务;
- 修改系统环境变量;
- 处理不同操作系统之间的兼容问题;
- 解决“我本地能跑,服务器跑不了”的问题。
Docker 的出现很好地解决了这些痛点。
Docker 的核心思想是:将应用及其依赖打包到一个镜像中,然后以容器的形式运行。
这样一来,只要目标机器安装了 Docker,就可以快速启动相同的应用环境。
Docker 带来的主要好处包括:
-
环境一致性强
开发、测试、生产环境可以使用同一个镜像,减少环境差异。 -
部署速度快
容器启动速度通常远快于传统虚拟机。 -
资源占用低
Docker 容器共享宿主机内核,相比虚拟机更轻量。 -
便于扩缩容
可以快速启动多个容器实例,配合负载均衡实现水平扩展。 -
方便回滚
镜像版本可控,出现问题时可以快速回退到旧版本。
二、Docker 基础概念不要混淆
很多 Docker 使用问题,本质上是对几个基础概念理解不清导致的。
1. 镜像 Image
镜像可以理解为应用运行环境的模板。它包含了应用程序、运行时、依赖库、环境变量、配置文件等内容。
例如:
docker pull nginx:1.25
这条命令会拉取一个 Nginx 镜像。
镜像本身是静态的,不会运行。只有基于镜像创建容器之后,应用才会真正启动。
2. 容器 Container
容器是镜像运行起来之后的实例。
例如:
docker run -d --name my-nginx nginx:1.25
这条命令会基于 nginx:1.25 镜像启动一个名为 my-nginx 的容器。
需要注意的是:
容器不是虚拟机,容器的生命周期可能很短,不能把容器内部当成长期保存数据的地方。
这是 Docker 使用中非常重要的避坑点。
3. 数据卷 Volume
数据卷用于持久化容器中的数据。
例如 MySQL 容器中的数据库文件,如果只保存在容器内部,那么删除容器后数据也会丢失。因此,必须将数据目录挂载到宿主机或 Docker Volume 中。
示例:
docker run -d \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql_data:/var/lib/mysql \
mysql:8.0
这里的 mysql_data 就是一个 Docker Volume,用于持久化 MySQL 数据。
4. 网络 Network
Docker 容器之间可以通过网络通信。默认情况下,Docker 会创建一个 bridge 网络,但在实际项目中,建议为每个项目创建独立网络。
例如:
docker network create app-network
然后启动容器时指定网络:
docker run -d --name redis --network app-network redis:7
同一个自定义网络中的容器,可以通过容器名互相访问。
三、Docker 使用常见避坑指南
下面是 Docker 使用过程中最常见、也最容易踩的坑。
1. 不要在生产环境使用 latest 标签
很多初学者习惯这样启动容器:
docker run -d nginx:latest
这在本地测试时问题不大,但在生产环境非常危险。
latest 并不代表最新稳定版,它只是镜像维护者默认打的一个标签。今天的 latest 和明天的 latest 可能不是同一个版本。
如果你在生产环境中使用 latest,可能会出现以下问题:
- 重新部署后应用版本变化;
- 依赖行为不一致;
- 配置兼容性问题;
- 回滚困难;
- 排查问题时无法确认准确版本。
正确做法是指定明确版本:
docker run -d nginx:1.25.4
对于自己构建的业务镜像,也应使用明确版本号,例如:
my-app:v1.0.0
my-app:v1.0.1
my-app:2024-01-15
如果团队有 CI/CD 流程,也可以使用 Git Commit ID 作为镜像标签:
my-app:8f3a2c1
2. 不要把重要数据只放在容器内部
这是最常见的 Docker 事故之一。
很多人启动 MySQL 容器后,发现服务运行正常,于是直接在里面创建数据库、写入业务数据。过了一段时间,容器出现问题,执行了:
docker rm -f mysql
结果数据库数据全部没了。
原因很简单:容器删除后,容器内部的可写层也会被删除。
正确做法是使用数据卷或目录挂载。
使用 Docker Volume
docker volume create mysql_data
docker run -d \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-v mysql_data:/var/lib/mysql \
mysql:8.0
使用宿主机目录挂载
docker run -d \
--name mysql \
-e MYSQL_ROOT_PASSWORD=123456 \
-v /data/mysql:/var/lib/mysql \
mysql:8.0
两种方式都可以,但在生产环境中,更建议明确规划数据目录,例如:
/data/docker/mysql
/data/docker/redis
/data/docker/nginx
/data/docker/app
这样备份、迁移、排查问题都更加方便。
3. 端口映射要规划清楚
Docker 容器内部端口和宿主机端口不是一回事。
例如:
docker run -d -p 8080:80 nginx:1.25
含义是:
宿主机端口 8080 -> 容器端口 80
访问宿主机的 8080 端口时,请求会转发到容器内部的 80 端口。
常见错误包括:
- 把端口顺序写反;
- 多个容器映射同一个宿主机端口;
- 容器内部服务监听地址不是
0.0.0.0; - 防火墙或云服务器安全组未放行端口。
如果启动容器时报错:
port is already allocated
说明宿主机端口已经被占用。
可以使用以下命令查看端口占用:
ss -tunlp | grep 8080
或者:
lsof -i:8080
4. 容器之间通信不要依赖宿主机 IP
很多人在 Docker 中部署多个服务,例如应用、MySQL、Redis,然后在应用配置中写:
mysql_host=127.0.0.1
redis_host=127.0.0.1
这是错误的。
在容器内部,127.0.0.1 指的是容器自身,不是宿主机,也不是其他容器。
正确方式是使用 Docker 自定义网络,并通过容器名访问服务。
示例:
docker network create app-net
启动 MySQL:
docker run -d \
--name mysql \
--network app-net \
-e MYSQL_ROOT_PASSWORD=123456 \
mysql:8.0
启动应用:
docker run -d \
--name app \
--network app-net \
-e DB_HOST=mysql \
my-app:v1.0.0
此时应用容器中可以通过 mysql:3306 访问 MySQL。
这种方式比写宿主机 IP 更稳定,也更适合迁移和扩展。
5. Dockerfile 不要随便写
Dockerfile 是构建镜像的核心文件。一个糟糕的 Dockerfile 会导致镜像体积巨大、构建速度慢、安全风险高。
避免使用过大的基础镜像
例如:
FROM ubuntu:latest
如果只是运行一个简单的 Node.js 或 Go 应用,使用完整 Ubuntu 镜像往往没有必要。
可以优先选择官方轻量镜像:
FROM node:20-alpine
或:
FROM nginx:1.25-alpine
Alpine 镜像体积小,但也要注意某些依赖兼容问题。如果应用依赖较复杂,可以选择 Debian slim 版本:
FROM node:20-slim
合理利用缓存
Docker 构建镜像时会使用缓存。如果 Dockerfile 顺序不合理,就会导致每次构建都重新安装依赖。
以 Node.js 项目为例,不推荐这样写:
COPY . .
RUN npm install
因为只要任意代码文件变化,npm install 就会重新执行。
更好的写法是:
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
这样只有依赖文件变化时,才会重新安装依赖。
使用多阶段构建
多阶段构建可以显著减小镜像体积。
以 Go 项目为例:
FROM golang:1.22 AS builder
WORKDIR /app
COPY . .
RUN go build -o server .
FROM debian:bookworm-slim
WORKDIR /app
COPY --from=builder /app/server /app/server
EXPOSE 8080
CMD ["./server"]
编译环境只存在于构建阶段,最终镜像中只保留运行所需文件。
6. 不要忽视 .dockerignore
很多人写了 Dockerfile,却忘记写 .dockerignore。
如果没有 .dockerignore,构建镜像时可能会把以下内容一起发送给 Docker:
.git目录;node_modules;- 日志文件;
- 本地缓存;
- 临时文件;
- 测试数据;
- 密钥文件;
- IDE 配置。
这会导致构建变慢、镜像变大,甚至泄露敏感信息。
示例 .dockerignore:
.git
node_modules
dist
logs
*.log
.env
.idea
.vscode
.DS_Store
特别注意:不要把 .env、私钥、证书等敏感文件打进镜像。
7. 环境变量不要硬编码在镜像中
很多人会在 Dockerfile 中直接写:
ENV MYSQL_PASSWORD=123456
这种方式非常不安全。镜像一旦被推送到仓库,环境变量信息可能被其他人查看到。
更推荐在运行容器时注入环境变量:
docker run -d \
-e MYSQL_PASSWORD='your_password' \
my-app:v1.0.0
如果使用 Docker Compose,可以写在 .env 文件中,并确保 .env 不提交到 Git 仓库。
8. 容器日志要限制大小
Docker 默认会使用 json-file 日志驱动。如果不限制日志大小,容器持续运行后日志文件可能占满磁盘。
这是生产环境中非常常见的问题。
可以在启动容器时指定日志限制:
docker run -d \
--name app \
--log-driver json-file \
--log-opt max-size=100m \
--log-opt max-file=3 \
my-app:v1.0.0
也可以在 Docker daemon 配置中统一设置:
{
"log-driver": "json-file",
"log-opts": {
"max-size": "100m",
"max-file": "3"
}
}
配置文件通常位于:
/etc/docker/daemon.json
修改后重启 Docker:
systemctl restart docker
9. 容器重启策略要设置
如果容器因为异常退出,默认情况下不会自动重启。生产环境中建议设置重启策略。
常用策略包括:
--restart=always
或:
--restart=unless-stopped
示例:
docker run -d \
--name nginx \
--restart=unless-stopped \
-p 80:80 \
nginx:1.25
两者区别:
always:无论什么情况,只要 Docker 启动,就尝试启动容器;unless-stopped:除非你手动停止容器,否则 Docker 启动时会自动启动容器。
一般更推荐使用 unless-stopped。
10. 不要长期不清理无用镜像和容器
Docker 使用久了之后,系统中可能会积累大量无用资源:
- 停止的容器;
- 未使用的镜像;
- 悬空镜像;
- 无用网络;
- 构建缓存;
- 未使用的数据卷。
查看磁盘占用:
docker system df
清理无用资源:
docker system prune
如果要清理未使用镜像:
docker image prune -a
如果要清理未使用数据卷:
docker volume prune
但请务必注意:
清理数据卷前一定要确认是否还有重要数据,否则可能造成不可恢复的数据丢失。
生产环境建议定期巡检,而不是盲目执行清理命令。
四、Docker Compose:更适合一键部署
单独使用 docker run 可以启动容器,但当项目包含多个服务时,命令会变得复杂。
例如一个常见 Web 项目可能包括:
- 后端应用;
- MySQL;
- Redis;
- Nginx;
- 消息队列;
- 定时任务。
如果每个服务都手写 docker run,维护成本很高。
这时推荐使用 Docker Compose。
Docker Compose 可以通过一个 docker-compose.yml 文件统一定义多个服务,然后使用一条命令启动整个项目:
docker compose up -d
这就是我们常说的“一键部署”。
五、一键部署示例:Nginx + 应用 + MySQL + Redis
下面给出一个较完整的 Docker Compose 示例,适合中小型项目参考。
目录结构建议如下:
project/
├── docker-compose.yml
├── .env
├── app/
│ ├── Dockerfile
│ └── ...
├── nginx/
│ └── nginx.conf
└── data/
├── mysql/
└── redis/
1. .env 示例
MYSQL_ROOT_PASSWORD=change_this_root_password
MYSQL_DATABASE=app_db
MYSQL_USER=app_user
MYSQL_PASSWORD=change_this_app_password
APP_PORT=8080
注意:.env 文件不要提交到公开仓库。
2. docker-compose.yml 示例
services:
nginx:
image: nginx:1.25-alpine
container_name: demo-nginx
restart: unless-stopped
ports:
- "80:80"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
depends_on:
- app
networks:
- demo-net
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
app:
build:
context: ./app
dockerfile: Dockerfile
image: demo-app:v1.0.0
container_name: demo-app
restart: unless-stopped
environment:
DB_HOST: mysql
DB_PORT: 3306
DB_NAME: ${MYSQL_DATABASE}
DB_USER: ${MYSQL_USER}
DB_PASSWORD: ${MYSQL_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
expose:
- "8080"
depends_on:
- mysql
- redis
networks:
- demo-net
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
mysql:
image: mysql:8.0
container_name: demo-mysql
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
volumes:
- ./data/mysql:/var/lib/mysql
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
networks:
- demo-net
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
redis:
image: redis:7-alpine
container_name: demo-redis
restart: unless-stopped
volumes:
- ./data/redis:/data
command: redis-server --appendonly yes
networks:
- demo-net
logging:
driver: json-file
options:
max-size: "100m"
max-file: "3"
networks:
demo-net:
driver: bridge
3. 一键启动
进入项目目录:
cd project
启动所有服务:
docker compose up -d
查看容器状态:
docker compose ps
查看日志:
docker compose logs -f
停止服务:
docker compose down
如果只想重新构建并启动应用:
docker compose up -d --build app
六、生产环境部署建议
Docker 可以简化部署,但生产环境仍然需要遵守基本规范。
1. 镜像版本要可追踪
业务镜像应使用明确版本号,不建议只使用:
app:latest
推荐:
app:v1.2.3
app:20240115
app:git-commit-id
这样可以快速定位线上运行的代码版本。
2. 配置和代码分离
镜像中尽量只包含代码和运行环境,配置通过环境变量或配置文件挂载注入。
这样可以让同一个镜像运行在不同环境:
- 开发环境;
- 测试环境;
- 预发布环境;
- 生产环境。
3. 数据必须备份
无论使用宿主机目录挂载,还是 Docker Volume,都必须制定备份策略。
例如 MySQL 可以定期执行:
docker exec demo-mysql mysqldump -u root -p app_db > backup.sql
也可以使用定时任务将备份文件同步到远程存储。
需要注意的是,备份不是目的,可恢复才是目的。因此必须定期测试备份文件能否正常恢复。
4. 监控磁盘和日志
Docker 容器日志、镜像缓存、数据库数据都可能占用大量磁盘空间。
建议监控以下指标:
- 磁盘使用率;
- Docker 日志大小;
- 容器重启次数;
- CPU 和内存使用率;
- 数据库连接数;
- 应用错误日志。
常用命令:
docker stats
查看容器资源占用。
5. 不要直接进入容器修改文件
有些人习惯这样操作:
docker exec -it app sh
然后在容器里手动修改配置文件或代码。
这是一种非常不推荐的做法。
原因包括:
- 容器重建后修改丢失;
- 无法追踪修改历史;
- 多实例环境无法保持一致;
- 容易造成线上环境和镜像版本不一致。
正确做法是:
- 修改代码或配置;
- 重新构建镜像;
- 使用 Compose 或 CI/CD 重新部署。
七、常用排查命令
Docker 出问题时,可以按以下顺序排查。
查看容器列表
docker ps
docker ps -a
查看容器日志
docker logs -f 容器名
例如:
docker logs -f demo-app
进入容器
docker exec -it 容器名 sh
如果容器中有 bash:
docker exec -it 容器名 bash
查看容器详细信息
docker inspect 容器名
查看网络
docker network ls
docker network inspect 网络名
查看镜像
docker images
查看数据卷
docker volume ls
查看资源占用
docker stats
八、Docker 一键部署检查清单
在正式部署前,可以按照下面的清单逐项检查。
镜像相关
- 是否使用了明确版本号?
- 是否避免使用
latest? - Dockerfile 是否足够精简?
- 是否使用了
.dockerignore? - 是否避免把敏感信息打进镜像?
数据相关
- 数据是否挂载到宿主机或 Volume?
- 数据目录权限是否正确?
- 是否有备份策略?
- 是否测试过数据恢复?
网络相关
- 服务是否在同一个 Docker 网络中?
- 容器之间是否通过服务名访问?
- 宿主机端口是否冲突?
- 防火墙和安全组是否放行?
安全相关
- 是否避免使用弱密码?
- 是否只暴露必要端口?
- 是否限制容器日志大小?
- 是否避免把密钥写入镜像?
- 是否控制镜像仓库权限?
运维相关
- 是否设置
restart策略? - 是否配置日志轮转?
- 是否能快速回滚?
- 是否有监控和告警?
- 是否记录部署版本?
九、总结
Docker 的价值不仅仅是“能把服务跑起来”,更重要的是让应用具备更好的可移植性、可维护性和可交付能力。
如果只是简单执行几条 docker run 命令,确实可以快速启动服务;但如果想在生产环境中稳定运行,就必须重视版本管理、数据持久化、网络规划、日志控制、安全配置和备份恢复。
本文总结的核心避坑原则可以归纳为以下几点:
- 生产环境不要使用
latest标签; - 重要数据必须挂载并定期备份;
- 容器之间通信优先使用自定义网络和服务名;
- Dockerfile 要精简,并合理利用缓存;
- 必须编写
.dockerignore; - 敏感信息不要写入镜像;
- 容器日志要限制大小;
- 生产服务要设置重启策略;
- 不要在容器内部手动修改线上文件;
- 推荐使用 Docker Compose 实现一键部署。
掌握这些原则后,Docker 不仅能帮助你快速部署项目,也能让你的系统在长期运行中更加稳定、清晰、可控。对于个人开发者来说,它可以节省大量环境配置时间;对于团队来说,它则是构建标准化交付流程的重要基础。