☰
双栈工坊容器化部署:Docker Compose 与多阶段构建实战
2026/10/11 23:45:54 网站建设 项目流程

在实际的前后端分离项目里,开发环境跑通只是第一步,真正让人头疼的往往是把整个系统交付到服务器上运行:前端要构建、后端要装 JDK、数据库要初始化、端口要互不冲突、服务要开机自启、日志出问题时要能快速定位。这一连串需求,非常适合用 Docker 来统一管理和部署。

“双栈工坊”可以理解为一个典型的前后端双技术栈应用:前端是一套静态页面工程,运行时由 Nginx 托管;后端是一套 Java Web 服务,对外提供 API。再加上 MySQL 作为数据层,整个系统就构成了一个多容器组合。Docker 负责把这三部分分别封装成镜像,用 Docker Compose 定义它们之间的网络、依赖、数据卷和环境变量,最终通过一组命令完成构建、启动、扩容、日志查看和故障恢复。

这篇文章以“双栈工坊”为示例项目,完整走一遍从环境检查、Dockerfile 编写、Compose 编排、镜像构建、容器启动到问题排查的过程。读完以后,你可以把同样的思路迁移到自己的前后端项目中,无论是本机开发、测试环境部署,还是生产容器化改造,都能有一套可复用的操作路径。

1. 先想清楚双栈项目为什么需要容器化部署

很多人最初接触 Docker 时只把它当成“轻量虚拟机”,觉得把项目打进去能跑就行。但双栈项目里真正的问题不是“能不能跑”,而是“每个环境能不能保持一致地跑”。理解容器化解决的核心问题,比记住命令更重要。

1.1 “双栈”在这里指什么

“双栈”并没有一个唯一的技术定义。在本文的示例项目里,它指前后端两套技术栈同时存在:

  • 前端技术栈:使用 Node.js 环境进行依赖安装和构建,产物是纯静态文件,运行时交给 Nginx。
  • 后端技术栈:使用 Java 生态,代码经过 Maven 编译打包成 JAR 包,运行在 JDK 环境中。
  • 数据层:MySQL 提供数据持久化,但它在后端服务内部并非独立技术栈,而是独立容器。

这种结构的典型特征是一个系统里同时存在“构建环境”和“运行环境”,而且两种环境对底层软件版本的要求完全不同。比如前端需要 Node.js 18 才能完成构建,但运行时只需要 Nginx;后端需要 JDK 17 编译,但运行时只需要 JRE。如果直接在宿主机上部署,你需要手动安装 Node、Nginx、JDK、MySQL,并且处理版本冲突和环境污染。

Dockerfile 多阶段构建可以把“构建环境”和“运行环境”分开,最终镜像里只保留运行所需的内容。这正是容器化特别适合双栈项目的原因。

1.2 不容器化的典型痛点

在没有 Docker 时,双栈项目的部署过程通常是这样:

  1. 在服务器上安装 Node.js,执行npm install和npm run build。
  2. 安装 Nginx,把前端构建产物拷贝到指定目录,再修改配置文件。
  3. 安装 JDK,设置JAVA_HOME,把后端 JAR 包放到某个目录。
  4. 安装 MySQL,创建数据库和账号,执行初始化 SQL。
  5. 配置后端连接数据库的地址、账号、密码。
  6. 手工启动后端进程,再启动 Nginx。
  7. 下一次换一台服务器,以上过程全部重来。

这个流程存在几个非常现实的问题。第一是环境差异,本地能跑、服务器跑不起来,经常是因为 Node 或 JDK 版本不一致。第二是进程管理,后端一旦异常退出,如果没有 systemd 或 supervisor,服务就悄悄没了。第三是环境污染,为了一个项目在服务器上装一堆运行时,其他项目可能会踩到依赖冲突。第四是回滚困难,代码更新后如果新版本有问题,需要手工把旧包找回来重新替换,操作路径很长。

容器化之后,镜像里已经固定了 Node、JDK、Nginx 的版本和配置。同一套镜像在笔记本、测试服务器、生产服务器上跑出来的结果是一致的,进程退出后可以由restart策略拉起,回滚时只需要切换镜像标签。

1.3 容器化部署的基本构成

在开始操作前,先统一三个概念:

概念通俗含义在示例项目中的作用
镜像只读模板,包含运行环境和应用文件前端镜像、后端镜像、MySQL 镜像
容器镜像的运行实例,可启动、停止、删除每个服务跑在一个容器里
编排定义多个容器如何协作Docker Compose 管理网络、依赖、数据卷

本文的核心主线是:先用 Dockerfile 把前端和后端做成镜像,再用 Docker Compose 把前端、后端、MySQL 三个服务编排起来。整个过程会覆盖镜像构建、容器管理、网络通信、数据持久化和日志排查,这正好对应“双栈工坊 Docker 管理部署容器”这个主题。

2. 环境准备:宿主机装好 Docker,后面所有操作才可复现

容器化操作的起点不是写代码,而是确认宿主机上的 Docker 环境可用。这里区分两类常见场景:一类是 Linux 服务器,另一类是本地 Windows 或 Mac 开发机。

2.1 Linux 服务器环境

在 Ubuntu 或 Debian 系列系统上,安装 Docker 的标准路径是配置官方软件源,然后安装docker-ce。CentOS 7 及类似系统的安装命令稍有区别,但思路相同。

# Ubuntu / Debian 示例 sudo apt-get update sudo apt-get install -y ca-certificates curl gnupg sudo install -m 0755 -d /etc/apt/keyrings curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg sudo chmod a+r /etc/apt/keyrings/docker.gpg echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null sudo apt-get update sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

这里有三点需要说明。第一,安装的是 Docker 引擎本身,而不是 Docker Desktop;服务器场景不需要图形界面。第二,docker-compose-plugin是 Docker Compose 的现代安装方式,安装后系统里存在docker compose子命令,而不是独立二进制docker-compose。第三,不同发行版的官方源地址不同,命令里的系统代号也随版本变化,生产环境应以 Docker 官方文档为准。

安装完成后,把当前用户加入docker用户组,避免每次执行命令都加sudo:

sudo usermod -aG docker $USER newgrp docker

usermod只是修改用户组,需要重新登录或者执行newgrp让用户组生效。这里要提醒一下:加入docker用户组相当于把宿主机 root 权限的一部分交给了该用户,在多人服务器上要谨慎管理成员。

2.2 Windows 和 Mac 本地环境

本地开发机通常使用 Docker Desktop。这个工具在 Windows 上依赖 WSL2 或 Hyper-V,在 Mac 上依赖系统虚拟化框架。

Windows 安装 Docker Desktop 前,先打开“启用或关闭 Windows 功能”,确认以下项已启用:

  • “适用于 Linux 的 Windows 子系统”
  • “虚拟机平台”

然后执行:

wsl --install

安装完 WSL2 后重启系统,再安装 Docker Desktop。启动后如果出现 “Docker Desktop failed to start because virtualisation support wasn't detected” 这类报错,说明虚拟化功能没有启用,或者在 BIOS 中没有开启 CPU 虚拟化。检查路径如下:

  1. 在 Windows 的“任务管理器 -> 性能 -> CPU”中查看“虚拟化”状态是否为“已启用”。
  2. 如果显示已禁用,需要进入 BIOS,找到 “Intel Virtualization Technology” 或 “SVM Mode” 选项,设置为 Enabled。
  3. 如果已经启用但 Docker Desktop 仍报错,检查是否安装了旧版本 WSL,执行wsl --update更新。

在本地使用 Docker Desktop 的优点是自带图形面板、日志查看和 Docker Compose 支持,适合学习和调试。但它默认会占用一定内存,WSL2 模式下还需要留意.wslconfig中内存上限的设置。

2.3 镜像源与版本核对

Docker 拉取镜像默认从 Docker Hub 获取。网络环境不同,拉取速度差异很大。针对“镜像下载慢”的问题,最可靠的做法是在服务器上配置镜像加速或使用内部私有 Registry。

镜像加速地址因服务商而异,通常可以在云厂商的容器镜像服务控制台找到。修改方式是在/etc/docker/daemon.json中写入:

{ "registry-mirrors": [ "https://your-registry-mirror.example.com" ] }

然后重启 Docker:

sudo systemctl daemon-reload sudo systemctl restart docker

这里不要照抄任何固定的加速地址,因为第三方地址可能随时失效,也可能存在内容安全和合规风险。最稳妥的方案是使用云平台提供的镜像服务,或者在内网搭建自建 Registry。

配置完成后,用以下命令检查镜像源是否生效:

docker info

在输出中搜索Registry Mirrors,能看到配置的地址就说明生效了。

2.4 安装完成后的环境检查清单

环境是否准备好,不要只凭“docker 命令能找到”来判断。建议按这份清单逐项检查:

检查项命令预期结果
Docker 客户端版本docker version正常输出版本号
Docker 服务状态docker info服务正常运行,无错误提示
当前用户权限docker ps能列出容器列表,无权限报错
Compose 插件docker compose version输出 Compose 版本
镜像源生效docker info能看到 Registry Mirrors 配置

如果docker info提示无法连接到 Docker daemon,先确认服务是否启动:

sudo systemctl status docker sudo systemctl enable --now docker

在本地 Windows 上如果出现连接失败,优先检查 Docker Desktop 是否处于运行状态,而不是先怀疑配置。

3. 为双栈工坊编写基础 Dockerfile

环境准备好之后,开始构造应用镜像。这一章先把项目结构定下来,再分别写前端和后端的 Dockerfile,最后说明.dockerignore的重要性。

3.1 项目结构说明

示例项目采用标准的前后端分离结构,目录如下:

shuangzhan-gongfang/ ├── frontend/ # 前端工程 │ ├── Dockerfile │ ├── .dockerignore │ ├── package.json │ ├── vite.config.js │ └── src/ ├── backend/ # 后端工程 │ ├── Dockerfile │ ├── .dockerignore │ ├── pom.xml │ └── src/ ├── mysql/ │ ├── init.sql # 数据库初始化脚本 │ └── my.cnf # 自定义 MySQL 配置 ├── nginx/ │ └── default.conf # 前端容器内 Nginx 配置 ├── docker-compose.yml └── .env

在真实的项目中,目录名和构建方式会有差异,但核心思想一致:每个部署单元对应一个目录,每个目录里有自己的 Dockerfile。Docker Compose 在根目录统一管理这些镜像的构建和启动。

3.2 前端镜像:多阶段构建与 Nginx 托管

前端工程不能直接扔进 Nginx 镜像,因为运行静态文件之前必须先完成依赖安装和构建。如果直接把开发态文件复制进镜像,镜像里必须带着 Node.js,运行时会白白多出几百兆体积。

更好的方式是使用多阶段构建。第一阶段使用 Node.js 镜像完成构建,第二阶段使用 Nginx 镜像只保留构建结果,也就是dist目录。

# frontend/Dockerfile # 第一阶段:构建前端静态资源 FROM node:18-alpine AS build WORKDIR /app COPY package.json package-lock.json ./ RUN npm install COPY . . RUN npm run build # 第二阶段:使用 Nginx 提供静态服务 FROM nginx:1.25-alpine COPY --from=build /app/dist /usr/share/nginx/html COPY nginx/default.conf /etc/nginx/conf.d/default.conf EXPOSE 80 CMD ["nginx", "-g", "daemon off;"]

这个 Dockerfile 有四个关键点。

第一,node:18-alpine作为构建阶段的基础镜像,相比完整版 Ubuntu 镜像更小,且 Node 版本固定。如果原始项目没有明确 Node 版本,落地前要确认package.json里的engines字段或 CI 脚本实际使用的版本。

第二,先复制package.json和package-lock.json,再执行npm install,最后才复制源码。这样做可以利用 Docker 的层缓存:只要依赖文件不变,每次修改源码时npm install层就不会重新执行,构建速度会快很多。

第三,CMD ["nginx", "-g", "daemon off;"]是 Nginx 官方镜像的默认启动方式,以前台进程运行 Nginx。不要在容器里使用systemctl start nginx,容器没有 systemd,进程结束后容器就退出了。

第四,nginx/default.conf会被覆盖进容器的/etc/nginx/conf.d/,用于配置前端页面和 API 反向代理。后面第 5 章会具体看这个配置。

3.3 后端镜像:JDK 基础镜像与 JAR 包

后端 Java 项目的目标是产出一个可运行 JAR 包。构建过程依赖 Maven 和 JDK,运行过程只需要 JRE。同样使用多阶段构建:

# backend/Dockerfile # 第一阶段:编译打包 FROM maven:3.9-eclipse-temurin-17 AS build WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src RUN mvn package -DskipTests # 第二阶段:运行 FROM eclipse-temurin:17-jre-alpine WORKDIR /app COPY --from=build /app/target/*.jar app.jar EXPOSE 8080 ENTRYPOINT ["java", "-jar", "app.jar"]

这段配置中的mvn dependency:go-offline会在源码复制之前下载依赖,目的是缓存依赖层。如果先复制pom.xml再执行这个命令,之后源码变化时不会重新下载依赖。

运行时使用eclipse-temurin:17-jre-alpine,它只包含运行 Java 程序所需的 JRE,不包含编译器,体积比 JDK 镜像小很多。

后端服务启动时要注意时区和内存参数。JVM 默认时区可能不是Asia/Shanghai,生产环境建议在启动参数里指定。修改后的启动命令:

ENTRYPOINT ["java", "-Duser.timezone=Asia/Shanghai", "-Xms512m", "-Xmx1024m", "-jar", "app.jar"]

-Xms和-Xmx是 JVM 堆内存的初始值和最大值。值要结合宿主机内存决定,不能无脑设大。在容器环境里,堆上限最好低于容器内存限制,否则容器会因内存超额被系统杀掉。

3.4 .dockerignore 与镜像瘦身要点

COPY . .会复制整个上下文目录。如果目录里有node_modules、target、.git、log等目录,它们会被一并发送到 Docker 构建上下文,导致构建慢,甚至让镜像变大。必须在每个构建目录里创建.dockerignore。

前端工程的.dockerignore:

node_modules dist .git *.log .DS_Store

后端工程的.dockerignore:

target .git *.log .idea *.iml .DS_Store

镜像瘦身的常见手段不只是选小基础镜像。经常被忽略的是清理构建阶段留下的临时文件,比如 npm 缓存、Maven 本地仓库。这些内容只存在于第一阶段,第二阶段没有复制它们,所以不会进最终镜像。这也是多阶段构建的优势:最终镜像只包含最小运行文件。

检查镜像大小的方法:

docker images

观察SIZE列,如果发现镜像体积异常大,可以进入容器查看/usr/share/nginx/html或/app目录里是否多了无关文件。

4. 用 Docker Compose 把容器编排在一起

有了镜像之后,下一步是定义多个容器的协作关系。前端容器需要访问后端 API,后端容器需要连接 MySQL,MySQL 需要持久化数据。这些关联如果靠手工docker run一个个启动,命令会越来越长,也容易漏配置。Docker Compose 用一份 YAML 文件把所有服务关系记录下来。

4.1 服务规划与端口分配表

在写 Compose 文件之前,先规划三个服务的运行方式:

服务名镜像来源容器内端口宿主机端口作用
frontend本地构建8080提供前端页面,反向代理 API
backend本地构建80808080提供业务 API
mysqlmysql:8.033063306存储业务数据

这里有一个原则:容器内端口由应用监听,宿主机端口决定外部如何访问。如果宿主机 3306 端口已被占用,可以把宿主机端口改成 33060,写成33060:3306。前端的80端口在服务器上默认 HTTP 端口,生产环境如果还有 Web 防火墙或负载均衡,一般不会直接暴露 80,而是由上层网关转发。

4.2 docker-compose.yml 完整示例

在项目根目录创建docker-compose.yml:

services: mysql: image: mysql:8.0 container_name: shuangzhan-mysql restart: always environment: MYSQL_ROOT_PASSWORD: root_pass_2024 MYSQL_DATABASE: shuangzhan_db MYSQL_USER: shuangzhan MYSQL_PASSWORD: app_pass_2024 TZ: Asia/Shanghai ports: - "3306:3306" volumes: - mysql-data:/var/lib/mysql - ./mysql/init.sql:/docker-entrypoint-initdb.d/init.sql:ro - ./mysql/my.cnf:/etc/mysql/conf.d/my.cnf:ro healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "127.0.0.1", "-uroot", "-proot_pass_2024"] interval: 10s timeout: 5s retries: 5 backend: build: context: ./backend container_name: shuangzhan-backend restart: always environment: MYSQL_HOST: mysql MYSQL_PORT: 3306 MYSQL_DATABASE: shuangzhan_db MYSQL_USER: shuangzhan MYSQL_PASSWORD: app_pass_2024 TZ: Asia/Shanghai ports: - "8080:8080" depends_on: mysql: condition: service_healthy frontend: build: context: ./frontend container_name: shuangzhan-frontend restart: always ports: - "80:80" depends_on: - backend volumes: mysql-data:

这个文件比常见的入门示例多加了几个关键配置,逐一说明。

restart: always表示容器异常退出后由 Docker 自动重启。这对进程守护非常有用,后端 Java 进程如果因为内存溢出崩溃,容器会被自动拉起。

healthcheck用于 MySQL 健康检查。MySQL 容器启动后并不会立刻可用,它需要初始化数据目录和用户。depends_on如果只写服务名,Docker Compose 只保证启动顺序,不保证服务真正可用。加上condition: service_healthy后,backend 会等待 MySQL 通过健康检查再启动,避免后端启动时数据库还没就绪。

MYSQL_USER和MYSQL_PASSWORD会在 MySQL 容器首次初始化时自动创建对应的普通用户,并授权访问MYSQL_DATABASE指定的数据库。这比直接用 root 连接安全。

后端环境变量中的MYSQL_HOST: mysql是 Compose 网络内的服务名,不是 IP。这个值会由 Docker 内置 DNS 解析到 MySQL 容器的 IP。

4.3 网络:服务间为什么用服务名访问

在 Compose 文件中,所有服务默认加入同一个网络,服务名就是主机名。因此 backend 里的 JDBC 地址可以写成:

spring.datasource.url=jdbc:mysql://mysql:3306/shuangzhan_db?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai

如果后端代码里把数据库地址写成localhost,容器启动后就会出现Connection refused。原因很简单:在容器内部,localhost指的是当前容器自己,而不是宿主机,更不是 MySQL 容器。

这一点是双栈项目容器化部署里最常见的错误之一。以下表格总结了连接地址的三种情况:

连接方式MySQL 地址适用场景
相同 Compose 网络mysql:3306后端容器连 MySQL 容器
宿主机进程连容器127.0.0.1:3306本地调试时用 Navicat 连容器 MySQL
跨服务器连数据库服务器 IP:端口外部应用访问数据库

如果后端代码的数据库地址是写死的,可以通过环境变量覆盖 Spring Boot 配置。例如在application.yml中使用占位符:

spring: datasource: url: jdbc:mysql://${MYSQL_HOST}:${MYSQL_PORT}/${MYSQL_DATABASE} username: ${MYSQL_USER} password: ${MYSQL_PASSWORD}

这样同一份代码,在本地不设置环境变量时可以用默认值,在容器里通过 Compose 注入环境变量,成本和风险都更低。

4.4 数据卷:MySQL 数据与后端日志的持久化

容器的文件系统是临时的。容器删除后,写入容器内部的数据也会消失。MySQL 的数据必须放在宿主机持久化目录里。

volumes: - mysql-data:/var/lib/mysql

mysql-data是 Docker 命名卷。它和./mysql/init.sql这种直接挂载宿主目录的方式不同。命名卷由 Docker 管理,不关心具体路径,适合数据库这类需要权限和性能较高的数据。直接挂载目录适合配置文件和日志,因为运维人员需要直接查看和修改。

初始化 SQL 脚本通过只读方式挂载到/docker-entrypoint-initdb.d/。MySQL 官方镜像会在初次启动数据目录时按文件名顺序执行这个目录下的.sql脚本。要注意:只有数据目录为空时才会执行,如果 MySQL 卷已经有数据,修改init.sql不会重新初始化。

后端日志如果重要,建议把容器的日志目录挂载到宿主机:

volumes: - ./logs/backend:/app/logs

不过,更推荐的方式是在 Compose 层统一配置日志驱动,生产环境通常把容器标准输出收集到集中日志系统。这一点在第 7 章展开。

5. 构建、启动与验证一条龙

Compose 文件写完后,整个项目的启动流程变得很简短。但“能启动”不等于“部署成功”,还需要按顺序验证容器状态、页面访问、API 通断、日志是否正常。

5.1 构建与启动

在项目根目录执行:

docker compose build

这个命令会按照build指令逐个构建frontend和backend镜像。构建过程中如果看到某一步报错,优先查看报错输出而不是急于调整代码。常见问题包括 npm 下载依赖超时、Maven 依赖拉取失败、源代码在构建环境中缺失等。

构建完成后,直接启动全部服务:

docker compose up -d

-d表示后台模式,容器会在后台运行,不在终端里打印日志。首次启动时 Docker 会自动拉取mysql:8.0镜像,如果本地没有,这个步骤会花费一定时间。

如果要重新构建并启动,可以合并执行:

docker compose up -d --build

这在代码更新后非常常用。--build会先重新构建镜像,再启动容器,省去手工执行两个命令的过程。

5.2 验证容器状态与服务可用性

启动后先看容器状态:

docker compose ps

正常输出中,每一个服务的STATUS列应该是Up,MySQL 服务在健康检查通过后可能显示Up (healthy)。如果某一行的状态是Restarting或Exited,说明启动过程有问题。

再看端口监听情况:

ss -tlnp | grep -E '80|8080|3306'

在本地浏览器打开http://localhost应该看到前端页面。如果页面能打开但接口报错,打开浏览器开发者工具,查看网络请求的失败状态。

如果服务器有防火墙,需要确认 80 和 8080 端口在安全组里已经放行。容器内部端口开得再正确,宿主机防火墙没放行,外部网络一样访问不到。

5.3 查看日志与进入容器

日志是排查容器问题的第一入口。查看所有服务的日志:

docker compose logs

只看某个服务的最新日志:

docker compose logs -f backend

-f类似于tail -f,会持续输出新日志,适合启动过程和接口调试。如果后端启动失败,日志里会直接出现异常堆栈,比如数据库连接失败、端口被占用、配置文件读取不到等。

需要进入容器内部排查时:

docker exec -it shuangzhan-backend sh

前端 Nginx 容器里可能没有 shell 之外的编辑器,可以用docker exec检查配置文件是否生效:

docker exec -it shuangzhan-frontend cat /etc/nginx/conf.d/default.conf

检查容器内是否能连通 MySQL:

docker exec -it shuangzhan-backend ping mysql

注意:有些精简镜像里没有ping命令。可以改用getent hosts mysql或直接让 Java 程序去连接,看日志结果。

5.4 常用管理命令速查表

容器化管理的过程会反复用到下面这些命令,整理成表格方便查阅:

操作命令说明
查看所有服务状态docker compose ps看容器是否在运行
查看日志docker compose logs -f backend实时跟踪后端日志
停止并删除容器docker compose down不删除镜像和数据卷
停止并删除卷docker compose down -v谨慎使用,会删除 MySQL 数据
重启单个服务docker compose restart backend快速重启服务
重新构建某个服务docker compose build backend只构建后端镜像
进入容器docker exec -it 容器名 sh交互式进入 shell
查看容器资源占用docker stats查看 CPU、内存、网络状况
查看镜像列表docker images确认镜像大小和构建时间

其中docker compose down -v必须特别小心。-v会删除 Compose 文件里定义的普通卷,MySQL 数据目录如果没有额外备份,将无法恢复。恢复数据的难度比修复代码大得多,操作前先确认数据有没有备份。

6. 实战中最高频的 5 个坑及排查链路

容器化部署的很多报错现象都很类似,但根因差异很大。下面整理 5 个双栈项目中最常见的问题,每条都给出现象、原因、检查方式和处理办法。

6.1 镜像拉取慢或拉取失败

现象:执行docker compose up时卡在Pulling mysql,或者直接提示failed to pull image。

可能原因:默认 Docker Hub 访问受限或速度慢;本地没有目标镜像;网络 DNS 解析异常。

排查链路:

  1. 先看具体报错,是超时还是 404。404 通常是镜像名写错,超时通常是网络问题。
  2. 执行docker search mysql测试 Docker Hub 是否可访问。
  3. 检查/etc/docker/daemon.json中是否配置了有效的镜像源。
  4. 确认镜像源地址是否可用,可以用curl -I请求镜像源地址检查连通性。

处理建议:优先使用云厂商容器镜像服务提供的镜像加速地址,或者内网私有 Registry。不要依赖来路不明的公共加速地址,服务质量和合规性都无法保证。同时,在服务器第一次拉取大镜像时,尽量选择网络空闲时段。

6.2 容器启动后立即退出

现象:docker compose ps显示某个服务处于Exited状态,或一直Restarting。

可能原因:应用启动时抛出异常;启动命令错误;容器没有前台进程;内存不足被系统杀掉。

排查链路:

  1. 查看容器日志,这是最直接的线索。
  2. 如果是后端容器,日志里通常有完整堆栈。
  3. 如果是前端 Nginx 容器,检查nginx -t校验配置文件语法。
  4. 用docker inspect 容器名查看State.ExitCode和Error字段。
docker compose logs backend --tail 100 docker inspect shuangzhan-backend | grep -A 10 '"State"'

处理建议:前端容器启动后立刻退出,优先查 Nginx 配置是否有语法错误;后端容器退出,优先查 JVM 启动参数、数据库连接和配置文件。不要直接删容器重启,日志会丢。

6.3 前端页面能打开,但接口报 502 或 504

现象:浏览器能加载前端页面,但请求 API 时返回 502 Bad Gateway 或 504。

可能原因:前端 Nginx 的proxy_pass地址写错;后端服务没有启动;后端监听端口与 Nginx 转发端口不一致。

假设前端容器里的 Nginx 配置如下:

server { listen 80; server_name _; location / { root /usr/share/nginx/html; index index.html; } location /api/ { proxy_pass http://backend:8080/api/; 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_pass http://backend:8080。backend是 Compose 网络中的服务名,如果写成http://localhost:8080,Nginx 容器会把请求发给自己,而后端并不在同一个容器里,结果必然是 502。

排查链路:

  1. docker compose ps确认后端容器是否在运行。
  2. 在后端日志中看是否有请求进来。
  3. 在 frontend 容器里执行wget http://backend:8080/actuator/health或类似命令,验证 Nginx 能否访问后端。
  4. 检查proxy_pass后面是否带了路径,路径拼接不一致会导致 404,而不是 502。

6.4 MySQL 连接失败:Host、时区、认证

现象:后端启动日志提示java.net.UnknownHostException: mysql或Access denied for user,又或者Communications link failure。

可能原因有三类,表现不同。

第一类:UnknownHostException,表示后端容器无法解析mysql这个主机名。原因通常是后端没加入 Compose 网络,或者数据库地址不是服务名。

第二类:Access denied for user 'shuangzhan'@'...',表示账号密码错误,或者用户没有目标数据库的权限。检查 Compose 文件里MYSQL_USER、MYSQL_PASSWORD是否和后端环境变量一致。

第三类:Public Key Retrieval is not allowed,通常出现在 MySQL 8.0 连接时。解决方案是在 JDBC URL 中加上:

allowPublicKeyRetrieval=true&useSSL=false

但要注意,useSSL=false只适合开发环境,生产环境数据库连接必须走加密连接,不能无脑关闭。

时区问题也很常见。后端连接 MySQL 后,发现时间差 8 小时,可以在 JDBC URL 中指定serverTimezone=Asia/Shanghai,同时把容器时区设置为TZ=Asia/Shanghai,并在 JVM 启动参数中加上-Duser.timezone=Asia/Shanghai。三层配合才能保证时间一致。

6.5 容器时间不对与日志乱码

现象:容器内执行date显示 UTC 时间;后端日志里的中文变成乱码。

时间不对的原因是基础镜像默认时区是 UTC。在 Compose 的environment里加:

environment: TZ: Asia/Shanghai

后端服务如果依赖 JVM 时区,还要加上 JVM 参数:

ENTRYPOINT ["java", "-Duser.timezone=Asia/Shanghai", "-jar", "app.jar"]

日志乱码多数是因为容器默认 locale 不支持中文,或者日志文件本身没有按 UTF-8 写入。首先确认应用日志配置编码为 UTF-8,比如 Logback 里<charset>UTF-8</charset>。其次确认挂载到宿主机的日志文件编码,尽可能统一为 UTF-8。

7. 学习环境与生产环境的差异化配置

同一个 Compose 文件在笔记本上能跑通,直接搬到生产环境可能会踩不少问题。学习环境的目的是快速验证功能,生产环境的核心要求是稳定性、可观测性和可回滚。

7.1 学习环境怎么快速跑通

学习环境不需要引入太多复杂配置。可以用以下最小原则:

  • 镜像源能拉取就行,不必追求极致速度。
  • 环境变量直接写在 Compose 文件里,不引入外部保管。
  • 不设置健康检查也能跑通,但建议保留,因为它是排查启动顺序问题的好工具。
  • 把宿主机端口直接映射到 80、8080、3306,方便访问。

如果在本地反复试验,建议每改一次 Compose 配置后执行:

docker compose up -d

up会自动对比配置变化,并重建需要变更的容器。不要先down再up,那样会增加不必要的容器重建时间。

7.2 生产环境还要补哪些能力

生产环境至少需要补齐以下能力:

能力说明建议方案
配置外置数据库密码不能明文写死在 Compose 文件里使用环境变量文件.env,或接入配置中心
日志采集容器销毁后日志不能丢使用 Docker 日志驱动或 filebeat 采集
健康检查应用是否就绪要能被自动化判断后端暴露健康检查端点,Compose 配置 healthcheck
资源限制防止单个容器耗尽宿主机内存在 Compose 中配置mem_limit、cpus
安全加固镜像漏洞、容器权限、网络暴露面定期扫描镜像,最小化基础镜像,只暴露必要端口
数据备份MySQL 数据定期备份定期mysqldump,并验证恢复流程

一个带内存限制的后端服务示例:

backend: build: context: ./backend restart: always mem_limit: 1g cpus: 1.0 environment: JAVA_OPTS: "-Xms512m -Xmx768m"

这里要注意 JVM 堆上限和容器内存限制的关系。容器限制 1GB,JVM 最大堆最好设置在 768MB 以下,因为 JVM 除了堆之外还有元空间、线程栈、JIT 编译等内存开销。如果堆上限设置接近容器限制,容器很容易触发 OOM 被杀。

7.3 发布与回滚思路

在生产环境,不要直接使用docker compose build后立即up的方式发布。更稳妥的做法是把构建和部署分离:

  1. 在 CI 流水线中构建镜像,打上不可变的版本标签。
  2. 将镜像推送到私有 Registry。
  3. 在部署机上修改 Compose 文件中的镜像版本。
  4. 执行docker compose pull拉取新镜像。
  5. 执行docker compose up -d更换容器。

这样发布的镜像内容是可追溯的。如果新版本有问题,只需要改回旧镜像标签,执行:

docker compose up -d

容器会基于旧镜像重新创建,回滚操作很直接。关键前提是 Compose 文件里写的是image,而不是build:

backend: image: registry.example.com/shuangzhan-backend:2024.06.01-r1

采用镜像仓库方式后,部署机上不再需要源代码,也不需要 Node 和 Maven 环境,这进一步降低了部署机的复杂度。

8. 最佳实践与下一步方向

容器化部署不是“写个 Dockerfile 就能上线”这么简单。真正可维护的部署方案,依赖一套稳定的规范。

8.1 镜像构建规范

以下几点是长期实践中值得坚持的规范:

  • 固定基础镜像版本。不要使用node:latest、openjdk:latest,同一份 Dockerfile 在不同时间构建可能得到不同结果。应使用node:18-alpine、eclipse-temurin:17-jre-alpine这类精确版本。
  • 把依赖安装放在源码复制之前,充分利用 Docker 构建缓存。
  • 构建阶段不保留无用的包管理工具缓存,多阶段构建能有效减小最终镜像体积。
  • 每个镜像只做一件事。不要在一个容器里同时运行 Nginx 和 Java 进程,进程管理会变得复杂。
  • 非 root 运行。默认情况下容器内使用 root 用户运行进程,攻击者一旦进入容器,能控制的文件范围很大。后端镜像可以创建普通用户:
FROM eclipse-temurin:17-jre-alpine RUN addgroup -S app && adduser -S app -G app WORKDIR /app COPY --from=build /app/target/*.jar app.jar USER app ENTRYPOINT ["java", "-jar", "app.jar"]

8.2 容器化项目的可复用清单

下面这份清单适合在每次发布前过一遍:

检查项操作
镜像构建是否成功docker compose build是否能顺利执行
镜像版本是否固定生产环境不使用 latest
数据库数据是否持久化MySQL 是否有命名卷挂载
敏感信息是否外置密码是否写死在镜像或 Compose 文件中
服务依赖顺序是否正确后端是否等待 MySQL 健康检查
端口是否冲突ss -tlnp确认宿主端口没有被占用
JVM 内存是否受限mem_limit是否大于 JVM 堆上限
日志是否能持久化容器日志是否有统一采集方案
回滚路径是否存在上一版本镜像是否还能拉取

这份清单不是一次性核对完就结束。每当项目依赖、版本、运行环境发生变化时,都要重新过一遍。

8.3 值得继续深入的方向

双栈工坊项目的容器化完成了第一步,后面还有多个方向可以继续深入。

第一个方向是容器编排升级。Docker Compose 适合单机场景,当服务规模增长到多台服务器时,可以学习 Kubernetes 或轻量级 K3s,理解 Pod、Service、Deployment 与 Compose 概念的对应关系。

第二个方向是镜像安全。可以从docker scan或trivy开始,定期扫描基础镜像和应用依赖的漏洞,理解镜像安全与容器安全是两个不同层面。

第三个方向是监控与日志链路。为后端接入 Actuator、Prometheus、Grafana,学习如何用指标判断容器健康状态;为日志接入 Elasticsearch 或 Loki,解决容器随时销毁后日志不可见的问题。

第四个方向是 CI/CD 整合。把镜像构建、推送到私有仓库、远程部署这三个步骤接入 GitLab CI、Jenkins 或 GitHub Actions,形成提交代码后自动部署的流水线。

从实践角度来看,这篇文章最重要的判断是:Docker 解决的不是“代码能不能跑”,而是“代码在每个环境里能不能一致地跑、出问题时能不能快速查”。双栈项目因为涉及前端、后端、数据库三种运行时,容器化的收益尤其明显。建议先在本机把这个最小示例完整跑通,再把“多阶段构建、Compose 编排、依赖健康检查、日志排查”这组能力迁移到真实项目中。对你手里的双栈工坊,这才是最有价值的落地点。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询