一套可直接落地的 Docker 部署实战:前后端、MySQL、Redis 与 Nginx 配置全流程
Docker 实战案例分享|附配置文件
在现代软件开发与交付流程中,Docker 已经从“可选工具”逐渐变成了“基础设施标配”。无论是个人开发者、创业团队,还是大型企业技术部门,几乎都会在某个环节使用 Docker:本地开发环境统一、测试环境快速搭建、生产服务标准化部署、CI/CD 自动化交付、微服务拆分与编排等。
很多人学习 Docker 时,会先接触镜像、容器、Dockerfile、Compose 等概念,但真正落地时,常常会遇到一些实际问题:开发环境和生产环境如何区分?配置文件怎么管理?数据库是否应该容器化?日志如何查看?服务之间如何通信?容器重启策略怎么设置?本文将通过一个完整的实战案例,分享如何使用 Docker 部署一个典型 Web 应用,并附上可直接参考的配置文件。
一、案例背景
假设我们要部署一个常见的 Web 项目,项目包含以下几个部分:
- 前端服务:基于 Nginx 部署静态页面
- 后端服务:基于 Node.js / Java / Python 等框架提供 API
- 数据库:MySQL
- 缓存服务:Redis
- 反向代理:Nginx 统一入口
- 数据持久化:数据库和日志需要保存到宿主机
- 环境变量:通过
.env文件管理敏感配置
这个架构非常典型,适用于很多中小型业务系统,例如后台管理系统、企业官网、SaaS 平台、小程序后端、内部工具平台等。
整体访问链路如下:
用户浏览器
↓
Nginx 反向代理
↓
前端静态资源 / 后端 API 服务
↓
MySQL / Redis
在传统部署方式中,我们需要手动安装 Node.js、MySQL、Redis、Nginx,配置系统服务,处理端口冲突,管理不同版本的软件依赖。随着项目变多,环境维护成本会越来越高。
使用 Docker 后,我们可以将每个服务封装成独立容器,并通过 docker-compose.yml 统一编排。这样不仅部署更简单,也能让团队成员在本地快速启动一套完整环境。
二、目录结构设计
一个清晰的目录结构,是 Docker 项目可维护性的基础。推荐结构如下:
docker-demo/
├── docker-compose.yml
├── .env
├── backend/
│ ├── Dockerfile
│ ├── package.json
│ └── src/
├── frontend/
│ ├── Dockerfile
│ ├── dist/
│ └── nginx.conf
├── nginx/
│ └── default.conf
├── mysql/
│ └── init.sql
└── data/
├── mysql/
└── redis/
各目录作用说明:
| 路径 | 说明 |
|---|---|
docker-compose.yml |
Docker Compose 主配置文件 |
.env |
环境变量配置 |
backend/ |
后端服务代码与镜像构建文件 |
frontend/ |
前端构建产物与前端 Nginx 配置 |
nginx/ |
网关 Nginx 配置 |
mysql/ |
数据库初始化脚本 |
data/ |
数据持久化目录 |
这里有一个重要原则:配置文件和业务代码分离,持久化数据和容器生命周期分离。
容器可以删除、重建、升级,但数据库数据、日志和上传文件不能随容器消失。因此,生产环境中一定要合理使用数据卷或目录挂载。
三、编写后端 Dockerfile
以后端 Node.js 服务为例,假设服务监听 3000 端口。
backend/Dockerfile:
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --production
COPY . .
EXPOSE 3000
CMD ["npm", "start"]
这个 Dockerfile 的逻辑比较简单:
- 使用
node:20-alpine作为基础镜像,体积较小。 - 设置工作目录为
/app。 - 先复制
package.json,再执行依赖安装。 - 最后复制业务代码。
- 暴露
3000端口。 - 通过
npm start启动服务。
这里有一个细节:为什么不一开始就 COPY . .?
因为 Docker 构建镜像时会使用缓存。如果先复制依赖声明文件,再安装依赖,只要 package.json 没有变化,依赖安装层就可以复用缓存,从而提升构建速度。
四、编写前端 Dockerfile
前端通常有两种部署方式:
第一种是在本地或 CI/CD 中提前构建好 dist,然后只把静态文件交给 Nginx;第二种是在 Docker 镜像构建阶段完成前端打包。这里采用更常见的多阶段构建方式。
frontend/Dockerfile:
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 --from=builder /app/dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
多阶段构建的好处是:最终镜像中只包含 Nginx 和静态资源,不包含 Node.js、源码和构建依赖。这样镜像更小,也更安全。
frontend/nginx.conf:
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://backend:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
如果是 Vue、React、Angular 等单页应用,刷新页面时可能出现 404。try_files $uri $uri/ /index.html; 的作用就是把前端路由交给浏览器端处理。
五、编写统一入口 Nginx 配置
在真实项目中,我们通常不会直接暴露后端容器端口,而是通过统一的网关 Nginx 转发请求。
nginx/default.conf:
server {
listen 80;
server_name example.com;
client_max_body_size 50m;
location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /api/ {
proxy_pass http://backend:3000/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
这个配置中,访问 / 时转发到前端容器,访问 /api/ 时转发到后端容器。
需要注意的是,Docker Compose 内部会自动创建网络,服务之间可以通过服务名互相访问。例如:
http://backend:3000
http://mysql:3306
http://redis:6379
这也是 Docker Compose 非常方便的地方:容器之间不需要写死 IP 地址。
六、编写 docker-compose.yml
docker-compose.yml 是整个项目的核心。它负责定义服务、镜像构建、端口映射、环境变量、依赖关系、数据卷、网络等内容。
version: "3.9"
services:
nginx:
image: nginx:1.25-alpine
container_name: demo-nginx
restart: always
ports:
- "80:80"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
- ./logs/nginx:/var/log/nginx
depends_on:
- frontend
- backend
networks:
- demo-network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
container_name: demo-frontend
restart: always
expose:
- "80"
networks:
- demo-network
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: demo-backend
restart: always
expose:
- "3000"
environment:
NODE_ENV: production
DB_HOST: mysql
DB_PORT: 3306
DB_NAME: ${MYSQL_DATABASE}
DB_USER: ${MYSQL_USER}
DB_PASSWORD: ${MYSQL_PASSWORD}
REDIS_HOST: redis
REDIS_PORT: 6379
depends_on:
- mysql
- redis
networks:
- demo-network
mysql:
image: mysql:8.0
container_name: demo-mysql
restart: always
ports:
- "3306:3306"
environment:
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
TZ: Asia/Shanghai
volumes:
- ./data/mysql:/var/lib/mysql
- ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro
command:
--default-authentication-plugin=mysql_native_password
--character-set-server=utf8mb4
--collation-server=utf8mb4_unicode_ci
networks:
- demo-network
redis:
image: redis:7.2-alpine
container_name: demo-redis
restart: always
ports:
- "6379:6379"
command: redis-server --appendonly yes
volumes:
- ./data/redis:/data
networks:
- demo-network
networks:
demo-network:
driver: bridge
这个 Compose 文件已经具备了一个较完整的部署能力。
关键点说明:
restart: always:容器异常退出后自动重启。depends_on:控制启动顺序,但不代表服务已经完全可用。expose:只暴露给 Compose 内部网络,不映射到宿主机。ports:映射到宿主机,可被外部访问。volumes:挂载配置文件、日志和数据目录。networks:将多个服务放入同一个自定义网络。
生产环境中,MySQL 和 Redis 是否映射端口要谨慎。如果不需要外部访问,建议不要暴露 3306 和 6379,只允许后端容器通过内部网络访问。
七、编写环境变量文件
.env:
MYSQL_ROOT_PASSWORD=RootPassword_ChangeMe
MYSQL_DATABASE=demo_app
MYSQL_USER=demo_user
MYSQL_PASSWORD=DemoPassword_ChangeMe
.env 文件非常适合管理环境差异,例如开发、测试、生产环境的数据库账号不同,服务域名不同,第三方 API Key 不同。
但要注意:
- 不要把生产环境
.env提交到公开仓库。 - 可以提交
.env.example作为模板。 - 生产环境密码应使用高强度随机字符串。
- 如果团队使用云平台,可以考虑密钥管理服务。
推荐提供一个 .env.example:
MYSQL_ROOT_PASSWORD=
MYSQL_DATABASE=
MYSQL_USER=
MYSQL_PASSWORD=
这样其他开发者只需要复制一份:
cp .env.example .env
然后填写自己的本地配置即可。
八、数据库初始化脚本
mysql/init.sql:
CREATE TABLE IF NOT EXISTS users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(64) NOT NULL UNIQUE,
password VARCHAR(255) NOT NULL,
email VARCHAR(128),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (username, password, email)
VALUES ('admin', 'change_me', 'admin@example.com');
MySQL 官方镜像有一个很好用的机制:容器首次初始化数据库时,会自动执行 /docker-entrypoint-initdb.d/ 目录下的 .sql 文件。
但这里要注意一个常见误区:初始化脚本只会在数据库目录为空时执行一次。如果 ./data/mysql 已经存在数据,即使你修改了 init.sql,容器重启后也不会重新执行。
如果是开发环境想重新初始化,可以删除本地数据目录:
docker compose down
rm -rf ./data/mysql
docker compose up -d
生产环境千万不要随意删除数据目录。
九、启动与验证
在项目根目录执行:
docker compose up -d --build
查看容器状态:
docker compose ps
查看日志:
docker compose logs -f
只查看后端日志:
docker compose logs -f backend
进入 MySQL 容器:
docker exec -it demo-mysql mysql -u root -p
进入 Redis 容器:
docker exec -it demo-redis redis-cli
停止服务:
docker compose down
如果需要连同匿名数据卷一起删除:
docker compose down -v
如果只想重新构建某个服务,例如后端:
docker compose up -d --build backend
十、常见问题与排查思路
1. 容器启动失败
先看容器状态:
docker compose ps
再看日志:
docker compose logs backend
大多数问题都可以通过日志定位,例如依赖安装失败、环境变量缺失、数据库连接失败、端口被占用等。
2. 后端连接不上 MySQL
常见原因包括:
- 后端配置中数据库地址写成了
localhost - MySQL 容器还没有完全启动
- 用户名或密码错误
- 数据库名称不存在
- 后端服务没有和 MySQL 加入同一个网络
在 Docker Compose 网络中,后端连接数据库时应使用:
mysql:3306
而不是:
localhost:3306
因为 localhost 指的是后端容器自己,而不是 MySQL 容器。
3. depends_on 不等于服务可用
depends_on 只能保证容器启动顺序,不能保证 MySQL 已经完成初始化。
如果后端启动时立即连接数据库,可能会出现短暂失败。解决方式包括:
- 后端代码中加入数据库重试机制。
- 使用健康检查。
- 使用等待脚本,例如
wait-for-it.sh。 - 在框架层面配置连接池重试。
更推荐在应用层实现重试,因为生产环境中数据库也可能临时不可用。
4. 前端页面刷新 404
如果使用 Vue Router 或 React Router 的 history 模式,刷新页面可能会直接请求后端路径,导致 Nginx 找不到文件。
解决方式是在前端 Nginx 中配置:
try_files $uri $uri/ /index.html;
这样所有未命中的路径都会回退到 index.html,由前端路由处理。
5. 修改配置后不生效
如果修改的是挂载配置,例如 nginx/default.conf,通常需要重启对应容器:
docker compose restart nginx
如果修改的是 Dockerfile 或镜像内部文件,需要重新构建:
docker compose up -d --build
如果修改的是数据库初始化脚本,但数据库目录已经存在,则不会重新执行初始化脚本。
十一、生产环境优化建议
上面的配置适合入门和中小型项目,但生产环境还需要进一步增强。
1. 不要直接暴露数据库端口
如果 MySQL 和 Redis 只给后端使用,建议删除:
ports:
- "3306:3306"
以及:
ports:
- "6379:6379"
改为只在内部网络通信,降低安全风险。
2. 使用更严格的 Redis 配置
生产环境 Redis 建议开启密码、限制访问来源,并根据业务情况配置内存策略。例如:
command: redis-server --appendonly yes --requirepass ${REDIS_PASSWORD}
同时后端通过环境变量读取 Redis 密码。
3. 增加健康检查
可以为 MySQL、Redis、后端服务增加 healthcheck:
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
健康检查可以帮助我们更准确地判断服务状态,也方便在部署系统中进行自动恢复。
4. 日志集中管理
本地开发时使用 docker compose logs 已经足够,但生产环境建议将日志接入统一平台,例如:
- ELK / OpenSearch
- Loki + Grafana
- 云厂商日志服务
- Datadog / New Relic
日志集中管理后,可以更方便地做错误追踪、告警和性能分析。
5. 镜像版本不要使用 latest
很多初学者喜欢写:
image: mysql:latest
这在生产环境中不推荐。因为 latest 会随着官方镜像更新而变化,可能导致不可预期的兼容性问题。
更推荐写明确版本:
image: mysql:8.0
image: redis:7.2-alpine
image: nginx:1.25-alpine
这样部署结果更可控,也方便回滚。
6. 区分开发环境和生产环境
开发环境可以开放更多端口、挂载源码、启用热更新;生产环境应更注重安全、稳定和可观测性。
可以准备两个 Compose 文件:
docker-compose.yml
docker-compose.prod.yml
启动生产环境时执行:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
这样既能复用基础配置,又能针对生产环境覆盖差异配置。
十二、一次完整部署流程示例
下面是一套比较完整的部署流程:
git clone https://example.com/docker-demo.git
cd docker-demo
cp .env.example .env
vim .env
docker compose up -d --build
docker compose ps
docker compose logs -f nginx
如果后续更新代码,可以执行:
git pull
docker compose up -d --build
如果只更新前端:
docker compose up -d --build frontend nginx
如果只更新后端:
docker compose up -d --build backend
十三、实战经验总结
Docker 的价值不只是“把服务放进容器”,更重要的是把部署过程标准化、可复制化、自动化。
通过本文的案例,我们完成了一个包含前端、后端、MySQL、Redis、Nginx 的完整 Docker 部署方案,并解决了实际项目中经常遇到的几个问题:服务通信、端口暴露、配置管理、数据持久化、日志查看、初始化脚本、生产环境安全优化等。
对于团队来说,Docker 带来的最大收益是降低环境差异。以前新成员加入项目,可能需要花半天甚至一天安装各种依赖;现在只需要安装 Docker,然后执行一条命令,就能启动完整开发环境。
对于运维和交付来说,Docker 让服务部署变得更加可控。镜像一旦构建完成,就可以在测试环境、预发布环境、生产环境中保持一致,减少“本地正常,线上异常”的问题。
当然,Docker 并不是万能的。它不能替代良好的应用架构,也不能自动解决数据库高可用、服务治理、监控告警、权限隔离等问题。但作为现代软件工程的基础工具,Docker 已经足够成熟,也足够值得掌握。
如果你刚开始学习 Docker,建议不要只停留在命令层面,而是尽快用一个真实项目练习:写 Dockerfile,写 Compose 文件,配置 Nginx,连接数据库,处理日志,模拟上线。只有经历完整流程,才能真正理解容器化部署的优势和边界。
最后,用一句话总结本文案例:
Docker 实战的核心不是记住多少命令,而是学会用容器化思维,把应用、配置、依赖和运行环境组织成一套稳定、清晰、可交付的系统。