用 Docker Compose 把项目部署变成一条命令
Docker 实战案例分享|一键部署
在日常开发、测试和生产运维中,“环境不一致”几乎是最常见、也最让人头疼的问题之一。开发同学本地运行正常,到了测试环境出现依赖缺失;测试环境验证通过,部署到生产环境又因为系统版本、配置差异、端口冲突等问题导致服务异常。随着微服务架构、前后端分离、持续交付等实践逐渐普及,传统的手工部署方式已经越来越难以满足效率和稳定性的要求。
Docker 的出现,很大程度上解决了这个问题。它通过容器技术将应用程序、运行环境、依赖库、配置文件等内容进行统一封装,使应用能够在不同机器、不同系统环境中保持一致的运行表现。对于开发者来说,Docker 可以让项目快速启动;对于测试人员来说,Docker 可以快速复现环境;对于运维人员来说,Docker 可以降低部署成本、提高交付效率。
本文将通过一个完整的实战案例,分享如何使用 Docker 实现项目的一键部署。文章会从 Docker 的核心价值讲起,再结合一个典型 Web 项目,介绍如何编写 Dockerfile、如何使用 docker-compose 管理多服务、如何配置环境变量、如何完成一键启动,以及在实际落地过程中需要注意的关键问题。
一、为什么选择 Docker 做一键部署?
所谓“一键部署”,并不是简单地写一个启动脚本,而是让项目从源码、依赖、配置到服务启动都形成标准化流程。理想情况下,无论是在开发电脑、测试服务器,还是生产服务器,只要执行一条命令,就可以完成应用启动。
Docker 在一键部署场景中有几个明显优势。
第一,Docker 可以解决环境一致性问题。传统部署方式往往依赖服务器上已经安装好的 Java、Node.js、Python、Nginx、MySQL 等软件,一旦版本不一致,就可能出现各种不可预期的问题。而 Docker 镜像中已经包含了应用运行所需的基础环境,部署时只需要启动容器即可。
第二,Docker 可以简化依赖管理。一个项目可能依赖数据库、缓存、消息队列、对象存储等多个组件。如果手动安装这些服务,不仅耗时,而且配置复杂。通过 docker-compose,我们可以把多个服务统一编排起来,一条命令即可启动整个系统。
第三,Docker 方便迁移和扩展。镜像构建完成后,可以推送到镜像仓库,在任意服务器上拉取运行。项目从测试环境迁移到生产环境时,不需要重新安装依赖,只需要调整配置即可。
第四,Docker 有利于自动化交付。无论是 Jenkins、GitLab CI、GitHub Actions,还是其他 CI/CD 工具,都可以很方便地与 Docker 集成,实现自动构建镜像、自动推送镜像、自动部署服务。
二、实战案例背景
假设我们有一个典型的前后端分离项目,整体架构如下:
- 前端:Vue 或 React 项目,构建后由 Nginx 提供静态资源服务;
- 后端:Spring Boot 或 Node.js API 服务;
- 数据库:MySQL;
- 缓存:Redis;
- 反向代理:Nginx;
- 编排工具:Docker Compose。
最终目标是:在服务器上执行以下命令即可启动完整系统。
docker compose up -d
启动完成后,用户可以通过浏览器访问前端页面,前端请求后端接口,后端连接 MySQL 和 Redis,整个系统能够正常运行。
这个案例虽然是简化版本,但已经覆盖了大多数中小型 Web 项目的核心部署流程。实际项目中,即使增加消息队列、搜索引擎、文件服务,也可以沿用类似思路扩展。
三、项目目录设计
一个清晰的目录结构,是 Docker 化部署的基础。推荐目录如下:
project-root
├── frontend
│ ├── Dockerfile
│ ├── nginx.conf
│ └── package.json
├── backend
│ ├── Dockerfile
│ ├── app.jar
│ └── application.yml
├── mysql
│ └── init.sql
├── docker-compose.yml
└── .env
其中:
frontend存放前端项目代码和前端镜像构建文件;backend存放后端项目代码或构建产物;mysql/init.sql用于数据库初始化;docker-compose.yml用于统一编排所有服务;.env用于统一管理环境变量,例如数据库密码、端口号、镜像版本等。
这种结构的好处是职责清晰。前端、后端、数据库初始化脚本、服务编排文件彼此独立,但又可以通过 Compose 统一管理。
四、编写后端 Dockerfile
以后端服务为例,如果是 Spring Boot 项目,通常最终会打包成一个 jar 文件。后端 Dockerfile 可以这样编写:
FROM eclipse-temurin:17-jre
WORKDIR /app
COPY app.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]
这份配置非常简洁,但已经足够完成后端容器化。
FROM eclipse-temurin:17-jre 表示使用 Java 17 运行环境作为基础镜像。相比完整 JDK,JRE 镜像体积更小,更适合生产运行。
WORKDIR /app 表示容器中的工作目录。后续命令都会在该目录下执行。
COPY app.jar app.jar 表示将本地构建好的后端程序复制到镜像中。
EXPOSE 8080 用于声明应用监听端口。
ENTRYPOINT 表示容器启动时执行的命令,也就是运行 Spring Boot 应用。
在实际项目中,我们也可以使用多阶段构建,让 Docker 在构建镜像时自动完成 Maven 或 Gradle 打包。不过对于部署流程来说,先在 CI 阶段构建产物,再制作运行镜像,通常会更加稳定和高效。
五、编写前端 Dockerfile
前端项目通常需要先执行构建命令,然后将生成的静态文件交给 Nginx 托管。推荐使用多阶段构建:
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY . .
RUN npm run build
FROM nginx:1.25-alpine
COPY nginx.conf /etc/nginx/conf.d/default.conf
COPY --from=builder /app/dist /usr/share/nginx/html
EXPOSE 80
这份 Dockerfile 分为两个阶段。
第一个阶段使用 Node.js 镜像安装依赖并构建前端项目。构建完成后,会生成 dist 目录。
第二个阶段使用 Nginx 镜像,只复制构建产物和 Nginx 配置。这样最终镜像中不会包含 Node.js、源码、构建缓存等内容,体积更小,也更安全。
前端 Nginx 配置可以这样写:
server {
listen 80;
server_name localhost;
location / {
root /usr/share/nginx/html;
index index.html;
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
这里有一个关键点:proxy_pass http://backend:8080/ 中的 backend 并不是服务器域名,而是 Docker Compose 中定义的服务名称。Compose 会自动创建内部网络,不同容器之间可以通过服务名互相访问。
这也是 Docker Compose 非常方便的地方。我们不需要在配置中写死容器 IP,因为容器 IP 可能变化,而服务名是稳定的。
六、编写 docker-compose.yml
接下来是整个一键部署的核心文件:docker-compose.yml。
services:
frontend:
build:
context: ./frontend
container_name: demo-frontend
ports:
- "80:80"
depends_on:
- backend
restart: always
backend:
build:
context: ./backend
container_name: demo-backend
ports:
- "8080:8080"
environment:
DB_HOST: mysql
DB_PORT: 3306
DB_NAME: demo
DB_USER: root
DB_PASSWORD: ${MYSQL_ROOT_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
- mysql
- redis
restart: always
mysql:
image: mysql:8.0
container_name: demo-mysql
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: demo
volumes:
- mysql-data:/var/lib/mysql
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql
restart: always
redis:
image: redis:7-alpine
container_name: demo-redis
ports:
- "6379:6379"
restart: always
volumes:
mysql-data:
这份配置定义了四个服务:frontend、backend、mysql 和 redis。
frontend 使用本地 frontend 目录构建镜像,并将容器的 80 端口映射到宿主机的 80 端口。用户访问服务器 IP 时,就能进入前端页面。
backend 使用本地 backend 目录构建镜像,同时通过环境变量获取数据库和 Redis 配置。注意,数据库地址写的是 mysql,Redis 地址写的是 redis,这都是 Compose 服务名。
mysql 使用官方 MySQL 镜像,并通过数据卷 mysql-data 持久化数据库文件。即使容器删除,只要数据卷还在,数据就不会丢失。
redis 使用官方 Redis 镜像,配置相对简单。
最后的 volumes 定义了一个命名数据卷,用于保存 MySQL 数据。这一点非常重要,生产环境绝不能把数据库数据只存放在容器内部,否则容器删除后数据也会丢失。
七、使用 .env 管理配置
为了避免在 docker-compose.yml 中硬编码敏感信息,我们可以使用 .env 文件。
MYSQL_ROOT_PASSWORD=your_secure_password
在 Compose 文件中,可以通过 ${MYSQL_ROOT_PASSWORD} 引用这个变量。
这样做有几个好处:
- 配置和编排文件解耦;
- 不同环境可以使用不同
.env文件; - 敏感信息不必直接写入 Compose 文件;
- CI/CD 部署时可以通过环境变量动态注入。
需要注意的是,生产项目中不要把包含真实密码的 .env 文件提交到代码仓库。通常应该提交 .env.example 作为示例,而真实 .env 文件由部署环境单独维护。
八、一键部署流程
当上述文件准备完成后,部署过程就非常简单。
首先,在服务器上安装 Docker 和 Docker Compose。现在较新的 Docker 版本已经集成了 Compose 插件,可以直接使用:
docker compose version
确认可用后,将项目代码上传到服务器,进入项目根目录,执行:
docker compose up -d --build
这个命令会自动构建前端和后端镜像,拉取 MySQL 与 Redis 镜像,创建网络和数据卷,并在后台启动所有服务。
启动后可以查看服务状态:
docker compose ps
查看日志:
docker compose logs -f
如果只想查看后端日志,可以执行:
docker compose logs -f backend
如果需要停止服务:
docker compose down
如果需要停止服务但保留数据库数据,只执行 down 即可;如果连数据卷也要删除,则需要执行:
docker compose down -v
这个命令会删除数据卷,生产环境一定要谨慎使用。
九、常见问题与解决思路
1. 后端连接不上数据库
这是最常见的问题之一。首先要检查后端配置中的数据库地址是否写成了 localhost。在容器内部,localhost 指的是当前容器自身,而不是 MySQL 容器。因此应该使用 Compose 服务名,例如 mysql。
其次要注意 MySQL 启动需要一定时间。depends_on 只能保证容器启动顺序,并不能保证 MySQL 已经完全就绪。如果后端启动太快,可能会出现首次连接失败。更稳妥的做法是在后端增加连接重试机制,或者使用健康检查。
2. 前端接口请求失败
如果前端通过浏览器直接请求后端接口,需要注意请求地址。如果写死为 http://localhost:8080,那么用户浏览器会访问用户自己电脑的 8080 端口,而不是服务器的后端服务。
更推荐的方式是通过 Nginx 反向代理,把 /api/ 请求转发给后端容器。这样前端只需要请求相对路径 /api/...,不需要关心后端真实地址。
3. 数据库数据丢失
如果 MySQL 没有配置数据卷,数据会存储在容器内部。一旦容器被删除,数据也会消失。因此,数据库服务必须配置 volumes,将数据目录持久化。
同时,生产环境还应该定期备份数据库,不要只依赖 Docker 数据卷。
4. 镜像体积过大
镜像体积过大会影响构建、传输和部署速度。优化方式包括:
- 使用 Alpine 版本基础镜像;
- 使用多阶段构建;
- 避免将源码、日志、缓存文件复制进镜像;
- 配置
.dockerignore; - 减少不必要的系统依赖安装。
例如前端项目应该添加 .dockerignore:
node_modules
dist
.git
npm-debug.log
这样可以避免将无关文件发送到 Docker 构建上下文中。
十、生产环境部署建议
虽然 Docker Compose 很适合中小型项目部署,但在生产环境中还需要考虑更多问题。
首先,服务配置要区分环境。开发环境、测试环境、生产环境的数据库地址、密码、日志级别、接口域名通常都不同。不要把所有配置写死在镜像中,而应该通过环境变量或配置中心管理。
其次,日志应该统一收集。容器日志可以通过 docker logs 查看,但生产环境更推荐接入 ELK、Loki、Promtail、Filebeat 等日志系统,方便检索和告警。
第三,数据库、Redis 等有状态服务要谨慎容器化。对于小型项目,把 MySQL 放进 Docker Compose 中很方便;但对于高可用生产系统,更推荐使用云数据库、独立数据库集群或专业运维方案。
第四,要设置合理的重启策略。restart: always 可以在容器异常退出后自动重启,提升服务可用性。但如果应用本身配置错误,容器可能会不断重启,因此仍然需要日志监控和告警。
第五,要关注安全问题。不要在镜像中写入明文密钥,不要使用默认密码,不要暴露不必要的端口。比如 MySQL 和 Redis 如果只供后端内部访问,生产环境可以不映射到宿主机端口,只在 Docker 内部网络中通信。
第六,要建立版本化发布流程。镜像最好使用明确版本号,例如 demo-backend:1.0.0,而不是长期依赖 latest。这样当新版本出现问题时,可以快速回滚到旧版本。
十一、从一键部署到自动化发布
一键部署解决的是“如何快速启动项目”的问题,而自动化发布解决的是“如何持续、稳定、可追踪地交付项目”的问题。
一个常见的 CI/CD 流程如下:
- 开发者提交代码到 Git 仓库;
- CI 工具自动拉取代码;
- 执行单元测试和构建;
- 构建 Docker 镜像;
- 推送镜像到镜像仓库;
- 服务器拉取新镜像;
- 执行
docker compose up -d更新服务; - 检查服务健康状态;
- 失败时自动回滚或通知负责人。
当项目规模不大时,可以先从 Docker Compose 开始,逐步引入自动化脚本和 CI/CD。等服务数量增加、团队规模扩大、可用性要求提高后,再考虑 Kubernetes、Docker Swarm 或云原生平台。
技术选型不应该一开始就追求复杂,而应该围绕实际问题逐步演进。对于很多中小团队来说,Docker Compose 已经可以显著提升部署效率,并且维护成本较低。
十二、总结
Docker 一键部署的核心价值,不只是“少敲几条命令”,而是通过标准化、镜像化和自动化,让应用交付过程更加稳定、可复制、可维护。
通过本文的案例,我们可以看到,一个完整的一键部署方案通常包括以下内容:
- 使用
Dockerfile封装前端和后端应用; - 使用多阶段构建减小镜像体积;
- 使用
docker-compose.yml编排多个服务; - 使用服务名实现容器之间通信;
- 使用数据卷持久化数据库数据;
- 使用
.env管理环境变量; - 使用
docker compose up -d --build实现一键启动; - 结合日志、健康检查、版本管理和 CI/CD 提升生产可用性。
Docker 并不是万能的,它不能替代良好的架构设计,也不能自动解决所有运维问题。但它提供了一套非常实用的标准化交付方式,让开发、测试和部署之间的边界更加清晰。
对于刚开始做项目部署的团队来说,Docker Compose 是非常合适的起点。它学习成本低,配置直观,足以支撑大多数中小型应用的一键部署需求。随着业务增长,再逐步引入镜像仓库、自动化流水线、监控告警和容器编排平台,就可以形成更加成熟的 DevOps 流程。
真正高质量的部署体系,不是一次性搭建出来的,而是在不断实践、复盘和优化中演进出来的。Docker 的价值,正是在这个过程中帮助团队减少重复劳动、降低环境风险,并让每一次交付都更加可靠。