ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

双栈工坊容器化部署:Docker Compose 与多阶段构建实战

双栈工坊容器化部署:Docker Compose 与多阶段构建实战 在实际的前后端分离项目里开发环境跑通只是第一步真正让人头疼的往往是把整个系统交付到服务器上运行前端要构建、后端要装 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 时双栈项目的部署过程通常是这样在服务器上安装 Node.js执行npm install和npm run build。安装 Nginx把前端构建产物拷贝到指定目录再修改配置文件。安装 JDK设置JAVA_HOME把后端 JAR 包放到某个目录。安装 MySQL创建数据库和账号执行初始化 SQL。配置后端连接数据库的地址、账号、密码。手工启动后端进程再启动 Nginx。下一次换一台服务器以上过程全部重来。这个流程存在几个非常现实的问题。第一是环境差异本地能跑、服务器跑不起来经常是因为 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 ar /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用户组避免每次执行命令都加sudosudo usermod -aG docker $USER newgrp dockerusermod只是修改用户组需要重新登录或者执行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 wasnt detected” 这类报错说明虚拟化功能没有启用或者在 BIOS 中没有开启 CPU 虚拟化。检查路径如下在 Windows 的“任务管理器 - 性能 - CPU”中查看“虚拟化”状态是否为“已启用”。如果显示已禁用需要进入 BIOS找到 “Intel Virtualization Technology” 或 “SVM Mode” 选项设置为 Enabled。如果已经启用但 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 ] }然后重启 Dockersudo 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 --frombuild /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 --frombuild /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.timezoneAsia/Shanghai, -Xms512m, -Xmx1024m, -jar, app.jar]-Xms和-Xmx是 JVM 堆内存的初始值和最大值。值要结合宿主机内存决定不能无脑设大。在容器环境里堆上限最好低于容器内存限制否则容器会因内存超额被系统杀掉。3.4 .dockerignore 与镜像瘦身要点COPY . .会复制整个上下文目录。如果目录里有node_modules、target、.git、log等目录它们会被一并发送到 Docker 构建上下文导致构建慢甚至让镜像变大。必须在每个构建目录里创建.dockerignore。前端工程的.dockerignorenode_modules dist .git *.log .DS_Store后端工程的.dockerignoretarget .git *.log .idea *.iml .DS_Store镜像瘦身的常见手段不只是选小基础镜像。经常被忽略的是清理构建阶段留下的临时文件比如 npm 缓存、Maven 本地仓库。这些内容只存在于第一阶段第二阶段没有复制它们所以不会进最终镜像。这也是多阶段构建的优势最终镜像只包含最小运行文件。检查镜像大小的方法docker images观察SIZE列如果发现镜像体积异常大可以进入容器查看/usr/share/nginx/html或/app目录里是否多了无关文件。4. 用 Docker Compose 把容器编排在一起有了镜像之后下一步是定义多个容器的协作关系。前端容器需要访问后端 API后端容器需要连接 MySQLMySQL 需要持久化数据。这些关联如果靠手工docker run一个个启动命令会越来越长也容易漏配置。Docker Compose 用一份 YAML 文件把所有服务关系记录下来。4.1 服务规划与端口分配表在写 Compose 文件之前先规划三个服务的运行方式服务名镜像来源容器内端口宿主机端口作用frontend本地构建8080提供前端页面反向代理 APIbackend本地构建80808080提供业务 APImysqlmysql:8.033063306存储业务数据这里有一个原则容器内端口由应用监听宿主机端口决定外部如何访问。如果宿主机 3306 端口已被占用可以把宿主机端口改成 33060写成33060:3306。前端的80端口在服务器上默认 HTTP 端口生产环境如果还有 Web 防火墙或负载均衡一般不会直接暴露 80而是由上层网关转发。4.2 docker-compose.yml 完整示例在项目根目录创建docker-compose.ymlservices: 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.urljdbc:mysql://mysql:3306/shuangzhan_db?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/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/mysqlmysql-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列应该是UpMySQL 服务在健康检查通过后可能显示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检查容器内是否能连通 MySQLdocker 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 解析异常。排查链路先看具体报错是超时还是 404。404 通常是镜像名写错超时通常是网络问题。执行docker search mysql测试 Docker Hub 是否可访问。检查/etc/docker/daemon.json中是否配置了有效的镜像源。确认镜像源地址是否可用可以用curl -I请求镜像源地址检查连通性。处理建议优先使用云厂商容器镜像服务提供的镜像加速地址或者内网私有 Registry。不要依赖来路不明的公共加速地址服务质量和合规性都无法保证。同时在服务器第一次拉取大镜像时尽量选择网络空闲时段。6.2 容器启动后立即退出现象docker compose ps显示某个服务处于Exited状态或一直Restarting。可能原因应用启动时抛出异常启动命令错误容器没有前台进程内存不足被系统杀掉。排查链路查看容器日志这是最直接的线索。如果是后端容器日志里通常有完整堆栈。如果是前端 Nginx 容器检查nginx -t校验配置文件语法。用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:8080Nginx 容器会把请求发给自己而后端并不在同一个容器里结果必然是 502。排查链路docker compose ps确认后端容器是否在运行。在后端日志中看是否有请求进来。在 frontend 容器里执行wget http://backend:8080/actuator/health或类似命令验证 Nginx 能否访问后端。检查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 中加上allowPublicKeyRetrievaltrueuseSSLfalse但要注意useSSLfalse只适合开发环境生产环境数据库连接必须走加密连接不能无脑关闭。时区问题也很常见。后端连接 MySQL 后发现时间差 8 小时可以在 JDBC URL 中指定serverTimezoneAsia/Shanghai同时把容器时区设置为TZAsia/Shanghai并在 JVM 启动参数中加上-Duser.timezoneAsia/Shanghai。三层配合才能保证时间一致。6.5 容器时间不对与日志乱码现象容器内执行date显示 UTC 时间后端日志里的中文变成乱码。时间不对的原因是基础镜像默认时区是 UTC。在 Compose 的environment里加environment: TZ: Asia/Shanghai后端服务如果依赖 JVM 时区还要加上 JVM 参数ENTRYPOINT [java, -Duser.timezoneAsia/Shanghai, -jar, app.jar]日志乱码多数是因为容器默认 locale 不支持中文或者日志文件本身没有按 UTF-8 写入。首先确认应用日志配置编码为 UTF-8比如 Logback 里charsetUTF-8/charset。其次确认挂载到宿主机的日志文件编码尽可能统一为 UTF-8。7. 学习环境与生产环境的差异化配置同一个 Compose 文件在笔记本上能跑通直接搬到生产环境可能会踩不少问题。学习环境的目的是快速验证功能生产环境的核心要求是稳定性、可观测性和可回滚。7.1 学习环境怎么快速跑通学习环境不需要引入太多复杂配置。可以用以下最小原则镜像源能拉取就行不必追求极致速度。环境变量直接写在 Compose 文件里不引入外部保管。不设置健康检查也能跑通但建议保留因为它是排查启动顺序问题的好工具。把宿主机端口直接映射到 80、8080、3306方便访问。如果在本地反复试验建议每改一次 Compose 配置后执行docker compose up -dup会自动对比配置变化并重建需要变更的容器。不要先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 堆上限和容器内存限制的关系。容器限制 1GBJVM 最大堆最好设置在 768MB 以下因为 JVM 除了堆之外还有元空间、线程栈、JIT 编译等内存开销。如果堆上限设置接近容器限制容器很容易触发 OOM 被杀。7.3 发布与回滚思路在生产环境不要直接使用docker compose build后立即up的方式发布。更稳妥的做法是把构建和部署分离在 CI 流水线中构建镜像打上不可变的版本标签。将镜像推送到私有 Registry。在部署机上修改 Compose 文件中的镜像版本。执行docker compose pull拉取新镜像。执行docker compose up -d更换容器。这样发布的镜像内容是可追溯的。如果新版本有问题只需要改回旧镜像标签执行docker compose up -d容器会基于旧镜像重新创建回滚操作很直接。关键前提是 Compose 文件里写的是image而不是buildbackend: 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 --frombuild /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 编排、依赖健康检查、日志排查”这组能力迁移到真实项目中。对你手里的双栈工坊这才是最有价值的落地点。
返回列表