上一篇 下一篇 分享链接 返回 返回顶部

一套可直接落地的 Docker 部署实战:前后端、MySQL、Redis 与 Nginx 配置全流程

发布人:慈云数据-客服中心 发布时间:5小时前 阅读量:2

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 的逻辑比较简单:

  1. 使用 node:20-alpine 作为基础镜像,体积较小。
  2. 设置工作目录为 /app
  3. 先复制 package.json,再执行依赖安装。
  4. 最后复制业务代码。
  5. 暴露 3000 端口。
  6. 通过 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 是否映射端口要谨慎。如果不需要外部访问,建议不要暴露 33066379,只允许后端容器通过内部网络访问。


七、编写环境变量文件

.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 实战的核心不是记住多少命令,而是学会用容器化思维,把应用、配置、依赖和运行环境组织成一套稳定、清晰、可交付的系统。

目录结构
全文