Docker部署实战:从零到一构建Spring Boot应用容器化部署全流程
1. 项目概述:为什么选择Docker部署?
如果你是一名开发者或者运维,大概率已经不止一次听过Docker的大名。但真正要把一个项目,比如一个Spring Boot应用、一个Node.js服务,或者一个Python脚本,从你的开发机搬到一台Linux服务器上跑起来,这个过程可能依然会让你头疼。传统的部署方式,我们得在服务器上配环境:装Java、配Python解释器、搞数据库、调各种依赖库版本,一个环节出错,就可能要折腾半天。更别提“在我机器上好好的,怎么到服务器上就不行了”这种经典难题。
Docker的出现,就是为了解决这个“环境一致性”的痛点。你可以把它理解为一个超级轻量级的“集装箱”。你的应用代码、运行环境、系统工具、系统库,所有依赖都被打包进这个标准化的集装箱里。这个集装箱在任何安装了Docker引擎的机器上,都能以完全相同的方式运行,彻底屏蔽了底层操作系统和硬件的差异。所以,部署就变成了一个极其简单的动作:把集装箱(镜像)拉到服务器上,然后启动它(运行容器)。
这次,我们就来手把手、超详细地走一遍使用Docker将一个项目部署到Linux服务器的完整流程。我会假设你有一台干净的CentOS 7或Ubuntu 20.04服务器,我们从零开始,涵盖Docker安装、镜像构建、容器运行、网络配置、数据持久化等核心环节,并分享我踩过的坑和积累的实操技巧。无论你是刚接触Docker的新手,还是想系统梳理流程的老手,这篇指南都能给你一个清晰的路线图。
2. 核心思路与方案选型:理解Docker部署的底层逻辑
在动手之前,我们需要先理清几个核心概念和为什么选择这套方案。Docker部署的核心是“镜像”和“容器”。镜像是静态的、分层的模板,定义了运行环境;容器则是镜像运行时的动态实例。我们的部署流程,本质上就是创建镜像、分发镜像、运行容器的过程。
2.1 为什么是Docker Compose?
对于简单的单应用部署,直接使用docker run命令就够了。但现实中,一个项目往往由多个服务组成,比如一个Web应用需要搭配数据库、缓存(Redis)、消息队列等。这时,手动管理多个docker run命令及其复杂的参数(网络、卷、环境变量)就非常繁琐且容易出错。
因此,我强烈推荐使用Docker Compose。它是一个用于定义和运行多容器Docker应用的工具。你只需要在一个docker-compose.yml文件中,用YAML语法描述所有服务(容器)的配置,然后一个docker-compose up命令就能启动整个应用栈。它解决了服务编排的基础问题,让部署声明化、可版本化,是单机多服务部署的绝佳选择。对于更复杂的集群部署,才需要考虑Kubernetes。
2.2 镜像构建策略:Dockerfile的学问
镜像是部署的基石,而Dockerfile是构建镜像的“菜谱”。如何编写一个高效、安全、可维护的Dockerfile,是部署成功的关键。这里有几个核心原则:
- 使用官方基础镜像:尽量使用
openjdk:11-jre-slim、node:16-alpine这类官方维护的、体积小的基础镜像。Alpine Linux镜像体积非常小,但某些情况下兼容性可能有问题,生产环境需充分测试。 - 利用构建缓存:Docker构建是分层的,每一行指令都会产生一个层。为了加速构建,应该把变化频率低的指令(如安装系统依赖)放在前面,变化频率高的指令(如复制应用代码)放在后面。
- 多阶段构建:对于需要编译的应用(如Java、Go),强烈推荐使用多阶段构建。在第一阶段(构建阶段)使用包含完整编译工具的大镜像进行编译;在第二阶段(运行阶段)仅复制第一阶段的编译产物到一个干净的小镜像中。这能极大减小最终镜像的体积,提升安全性和拉取速度。
- 非root用户运行:默认情况下,容器内进程以root用户运行,存在安全风险。应该在Dockerfile中创建并使用一个非root用户来运行应用。
我们的方案将围绕一个典型的Web应用(以Spring Boot为例)展开,使用多阶段构建的Dockerfile,并通过Docker Compose编排应用与MySQL数据库。
3. 环境准备:Linux服务器与Docker安装
假设我们拿到了一台全新的CentOS 7服务器。下面是从系统配置到Docker安装的详细步骤。
3.1 系统基础配置
首先,通过SSH登录服务器。进行一些基础优化:
# 更新系统包(首次更新可能较慢) sudo yum update -y # 安装常用的工具 sudo yum install -y vim wget curl net-tools lsof # 关闭防火墙和SELinux(仅用于实验环境,生产环境需配置安全组和防火墙规则) sudo systemctl stop firewalld sudo systemctl disable firewalld sudo setenforce 0 sudo sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config注意:生产环境中绝对不建议直接关闭防火墙和SELinux。正确的做法是配置防火墙(如firewalld或iptables)开放Docker及你的应用所需端口,并针对SELinux进行策略调整或设置为宽容模式。
3.2 Docker引擎安装与配置
我们将使用Docker官方仓库进行安装,确保获得最新稳定版本。
# 1. 卸载旧版本(如有) sudo yum remove -y docker docker-client docker-client-latest docker-common docker-latest docker-latest-logrotate docker-logrotate docker-engine # 2. 安装yum工具包并添加Docker官方仓库 sudo yum install -y yum-utils device-mapper-persistent-data lvm2 sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo # 3. 安装Docker引擎(社区版)、命令行工具和容器运行时接口 sudo yum install -y docker-ce docker-ce-cli containerd.io # 4. 启动Docker并设置开机自启 sudo systemctl start docker sudo systemctl enable docker # 5. 验证安装 sudo docker --version sudo docker run hello-world如果能看到Docker版本信息以及Hello from Docker!的提示,说明安装成功。
国内用户必备操作:配置镜像加速器从Docker Hub拉取镜像速度可能很慢,需要配置国内镜像加速器。
# 创建或修改Docker守护进程配置文件 sudo tee /etc/docker/daemon.json <<-'EOF' { "registry-mirrors": [ "https://docker.mirrors.ustc.edu.cn", "https://hub-mirror.c.163.com", "https://mirror.baidubce.com" ], "log-driver": "json-file", "log-opts": { "max-size": "100m", "max-file": "3" } } EOF # 重新加载配置并重启Docker sudo systemctl daemon-reload sudo systemctl restart docker # 检查加速器是否生效 sudo docker info | grep -A 1 "Registry Mirrors"3.3 Docker Compose安装
Docker Compose是一个独立的二进制文件,安装非常简单。
# 从Github下载稳定版本的Docker Compose # 请访问 https://github.com/docker/compose/releases 查看最新版本号,替换下面的`v2.20.0` sudo curl -L "https://github.com/docker/compose/releases/download/v2.20.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # 赋予执行权限 sudo chmod +x /usr/local/bin/docker-compose # 创建软链接(可选,方便直接使用`docker-compose`命令) sudo ln -s /usr/local/bin/docker-compose /usr/bin/docker-compose # 验证安装 docker-compose --version4. 项目准备与Dockerfile深度解析
现在,我们在本地开发环境准备一个简单的Spring Boot项目。项目结构如下:
my-springboot-app/ ├── src/ ├── pom.xml ├── Dockerfile # 镜像构建文件 └── docker-compose.yml # 服务编排文件4.1 编写高效的Dockerfile
一个优秀的Dockerfile是部署成功的半壁江山。下面是一个针对Spring Boot Jar包的多阶段构建Dockerfile示例:
# 第一阶段:构建阶段 (Builder Stage) # 使用包含Maven和JDK的较大镜像来编译项目 FROM maven:3.8.4-openjdk-11-slim AS builder # 设置工作目录 WORKDIR /app # 首先复制pom.xml文件,利用Docker缓存层 # 如果pom.xml没有变化,则不会重复下载依赖,极大加速构建 COPY pom.xml . RUN mvn dependency:go-offline -B # 复制源代码并打包 COPY src ./src RUN mvn clean package -DskipTests # 第二阶段:运行阶段 (Runtime Stage) # 使用仅包含JRE的轻量级镜像作为运行环境 FROM openjdk:11-jre-slim # 在运行镜像中创建一个非root用户和用户组,增强安全性 RUN groupadd -r spring && useradd -r -g spring spring USER spring:spring # 设置工作目录 WORKDIR /app # 从构建阶段复制打包好的jar文件 # 注意这里的 `--from=builder` 指定了来源阶段 COPY --from=builder /app/target/*.jar app.jar # 暴露应用端口(Spring Boot默认8080) EXPOSE 8080 # 设置JVM运行参数,例如堆内存、垃圾回收器等,根据实际应用调整 ENV JAVA_OPTS="-Xms512m -Xmx512m -XX:+UseG1GC" # 使用 exec 格式的 ENTRYPOINT 启动应用,使应用能接收Unix信号(如SIGTERM) ENTRYPOINT exec java $JAVA_OPTS -jar app.jar关键点解析与避坑指南:
- 多阶段构建:
FROM ... AS builder定义了构建阶段。最终镜像只包含运行阶段(第二个FROM)的内容,构建工具(Maven)和中间产物都被丢弃,这使得最终镜像体积可能从600MB+减少到200MB左右。 - 依赖缓存:先单独复制
pom.xml并执行mvn dependency:go-offline,这能提前下载所有依赖到本地仓库。只要pom.xml不变,这一层就会被缓存,后续构建无需重复下载网络依赖,速度极快。 - 非root用户:
USER spring:spring指令让容器以非特权用户运行,遵循了最小权限原则。特别注意:如果应用需要写入容器内某个目录(如日志),必须确保该目录对该用户有写权限,或者在运行阶段通过chown改变目录属主。 - ENTRYPOINT exec格式:使用
exec格式(ENTRYPOINT ["executable", "param1", "param2"])或shell格式中的exec关键字,可以让Java进程成为容器内的PID 1进程。这样,发送给容器的SIGTERM等终止信号才能被Java进程正确接收,从而实现优雅关机,避免强制杀死导致事务中断。
4.2 编写docker-compose.yml
接下来,我们编写编排文件,定义应用服务及其依赖的数据库。
version: '3.8' services: # 主应用服务 app: build: . # 使用当前目录下的Dockerfile构建镜像 container_name: my-springboot-app restart: unless-stopped # 容器退出时总是重启,除非手动停止 ports: - "8080:8080" # 主机端口:容器端口 environment: - SPRING_PROFILES_ACTIVE=prod - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/myappdb?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai - SPRING_DATASOURCE_USERNAME=root - SPRING_DATASOURCE_PASSWORD=YourStrongPassword123! depends_on: - mysql # 声明依赖,确保mysql先启动 networks: - app-network # 将主机目录挂载到容器内,实现日志、配置文件持久化 volumes: - ./logs:/app/logs - ./config/application-prod.yml:/app/config/application-prod.yml:ro # 只读挂载配置文件 # MySQL数据库服务 mysql: image: mysql:8.0 container_name: mysql-for-app restart: unless-stopped environment: MYSQL_ROOT_PASSWORD: YourStrongPassword123! MYSQL_DATABASE: myappdb MYSQL_USER: myappuser MYSQL_PASSWORD: MyAppUserPass123! networks: - app-network volumes: # 数据持久化:将主机上的`mysql-data`目录挂载到容器的数据目录 - mysql-data:/var/lib/mysql # 初始化脚本:可以将建表SQL放在`./init-scripts`目录下,容器启动时会自动执行 - ./init-scripts:/docker-entrypoint-initdb.d:ro command: - --default-authentication-plugin=mysql_native_password - --character-set-server=utf8mb4 - --collation-server=utf8mb4_unicode_ci # 定义命名卷和数据卷 volumes: mysql-data: # 声明一个命名卷,Docker会自动管理其在主机上的存储位置 # 定义自定义网络,使服务间可以通过服务名通信 networks: app-network: driver: bridge编排文件核心配置解读:
- 服务间通信:通过自定义网络
app-network,服务间可以直接使用服务名(如mysql)作为主机名进行通信。这是Docker Compose提供的DNS功能,比使用IP地址更稳定可靠。 - 数据持久化:
- 命名卷(Named Volume):
mysql-data是一个命名卷,Docker会将其存储在主机的一个特定路径下(通常/var/lib/docker/volumes/)。即使容器被删除,卷中的数据依然保留,是最推荐的数据持久化方式。 - 绑定挂载(Bind Mount):
./logs:/app/logs是将主机当前目录下的logs文件夹挂载到容器内。适合存放日志、配置文件等需要直接从主机访问的文件。
- 命名卷(Named Volume):
- 环境变量:敏感信息(如数据库密码)通过
environment传入。切勿将密码硬编码在Dockerfile或代码中。更安全的方式是使用Docker Secrets(Swarm模式)或外部配置中心。 depends_on:这仅控制容器的启动顺序(先启动mysql,再启动app),并不保证mysql数据库服务已就绪。对于数据库,应用需要有重连机制。更健壮的做法是使用wait-for-it.sh或dockerize等工具在应用启动前检测依赖服务端口。
5. 完整部署流程实操
环境与文件准备就绪,现在我们登录Linux服务器,开始部署。
5.1 传输项目文件到服务器
在本地项目根目录(包含Dockerfile和docker-compose.yml的目录)下操作:
# 使用scp命令将整个项目目录压缩后上传到服务器的/opt目录 tar -czf my-app.tar.gz . scp my-app.tar.gz user@your_server_ip:/opt/ # 登录服务器 ssh user@your_server_ip # 进入/opt目录并解压 cd /opt tar -xzf my-app.tar.gz -C my-springboot-app cd my-springboot-app5.2 构建与启动
在服务器上的项目目录中,执行以下命令:
# 使用Docker Compose构建镜像并启动所有服务 # `-d` 参数表示在后台运行(守护进程模式) docker-compose up -d --build这个命令会执行以下操作:
- 根据
docker-compose.yml中app服务的build: .配置,读取当前目录的Dockerfile构建镜像。 - 从Docker Hub拉取
mysql:8.0镜像(如果本地没有)。 - 创建
app-network网络。 - 创建
mysql-data命名卷。 - 按依赖顺序启动
mysql和app容器。
5.3 检查部署状态
启动后,我们需要确认服务是否正常运行。
# 查看所有由当前docker-compose管理的容器状态 docker-compose ps # 查看所有容器的实时日志(组合输出) docker-compose logs -f # 单独查看某个服务的日志,例如查看应用启动日志 docker-compose logs -f app # 查看容器内进程 docker-compose top # 进入应用容器内部进行检查(类似于ssh进容器) docker-compose exec app sh # 进入容器后,可以检查环境变量、查看进程等 # echo $SPRING_DATASOURCE_URL # ps aux # exit 退出容器如果看到应用日志显示“Started Application in X seconds”且无报错,同时MySQL容器健康运行,说明部署基本成功。
5.4 验证服务可用性
在服务器上或从本地机器,通过curl或浏览器访问应用。
# 在服务器上测试本地访问 curl http://localhost:8080/your-api-endpoint # 如果服务器防火墙/安全组开放了8080端口,可以从外部访问 # 浏览器打开:http://your_server_ip:80806. 生产环境进阶配置与优化
基础的跑起来只是第一步,要用于生产环境,还需要考虑更多。
6.1 资源限制与监控
默认情况下,容器可以使用宿主机的所有资源。这可能导致某个容器耗尽资源影响其他服务。我们需要设置资源限制。
修改docker-compose.yml中的app服务部分:
app: build: . ... deploy: # 注意:`deploy`部分仅在`docker-compose up`时生效,`docker run`需用其他参数 resources: limits: cpus: '1.0' # 限制最多使用1个CPU核心 memory: 1024M # 限制最多使用1GB内存 reservations: cpus: '0.5' memory: 512M对于单机Docker Compose,也可以使用docker run风格的资源限制(在compose文件的service下直接配置):
app: build: . ... mem_limit: 1024m mem_reservation: 512m cpus: 1.0监控:可以使用docker stats命令实时查看容器资源使用情况,或集成Prometheus、cAdvisor等监控工具。
6.2 日志管理策略
默认的json-file日志驱动会将日志存储在主机/var/lib/docker/containers/下,容易撑满磁盘。我们已在daemon.json中配置了日志轮转(max-size和max-file)。更佳实践是使用集中式日志系统。
- 使用日志驱动发送到外部:可以配置Docker使用
syslog、journald或gelf等驱动,将日志直接发送到日志服务器(如ELK Stack)。 - 应用日志挂载到主机:如我们之前做的
./logs:/app/logs,然后在主机上使用logrotate等工具管理日志文件。
6.3 健康检查配置
为服务添加健康检查,能让Docker Compose更好地了解服务状态,并可用于服务发现等高级场景。
在docker-compose.yml中为app服务添加:
app: ... healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8080/actuator/health"] # Spring Boot Actuator健康端点 interval: 30s timeout: 10s retries: 3 start_period: 40s这样,执行docker-compose ps时可以看到服务状态是healthy还是unhealthy。
6.4 使用.env文件管理环境变量
将敏感信息写在docker-compose.yml中并不安全。我们可以使用环境变量文件。
- 创建
.env文件(务必加入.gitignore):MYSQL_ROOT_PASSWORD=YourStrongPassword123! MYSQL_PASSWORD=MyAppUserPass123! - 修改
docker-compose.yml,引用环境变量:mysql: ... environment: MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} MYSQL_PASSWORD: ${MYSQL_PASSWORD} - 启动时,Docker Compose会自动读取同目录下的
.env文件。
7. 日常运维与问题排查实录
部署完成后,日常的启停、更新、备份和问题排查是运维常态。
7.1 常用运维命令
# 停止所有服务,但保留容器和网络 docker-compose stop # 停止并移除所有容器、网络(默认不移除卷和镜像) docker-compose down # 停止并移除所有容器、网络、卷(谨慎使用!会删除数据库数据!) docker-compose down -v # 重启某个服务,例如只重启应用 docker-compose restart app # 查看服务日志 docker-compose logs app # 在运行的服务中执行命令,例如查看应用环境变量 docker-compose exec app env # 拉取服务的最新镜像(如果使用远程镜像而非本地构建) docker-compose pull # 重新构建并启动服务(适用于代码更新后) docker-compose up -d --build7.2 常见问题与排查技巧
问题1:容器启动后立即退出(Exited (1))
- 排查:首先查看日志
docker-compose logs app。最常见的原因是:- 应用启动失败:检查日志中的Java报错,可能是数据库连接失败、配置文件错误、端口占用等。
- Dockerfile中ENTRYPOINT/CMD错误:命令执行失败。确保启动命令正确,例如
java -jar的参数路径无误。 - 容器内用户权限不足:如果使用了非root用户,检查其是否对要写入的目录(如日志目录)有权限。可以在Dockerfile的
USER指令前,用root权限创建目录并修改属主。
问题2:应用无法连接到数据库(Connection refused)
- 排查:
- 确认MySQL容器是否正常运行:
docker-compose ps。 - 进入应用容器,测试网络连通性:
docker-compose exec app ping mysql。 - 进入应用容器,测试端口连通性:
docker-compose exec app nc -zv mysql 3306。 - 检查应用容器的环境变量
SPRING_DATASOURCE_URL是否正确:docker-compose exec app env | grep SPRING。确保主机名是mysql(服务名),端口是3306。 - 检查MySQL容器日志,看是否有初始化错误:
docker-compose logs mysql。
- 确认MySQL容器是否正常运行:
问题3:构建镜像时下载依赖超时或失败
- 解决:
- 为Docker构建配置代理:在构建命令前设置环境变量,或在Dockerfile中使用
ARG定义构建参数来设置代理。 - 使用国内Maven仓库:在项目
pom.xml中或本地Mavensettings.xml中配置阿里云等镜像仓库。对于Docker构建,可以将配置好的settings.xml复制到镜像中:COPY settings.xml /root/.m2/。
- 为Docker构建配置代理:在构建命令前设置环境变量,或在Dockerfile中使用
问题4:磁盘空间不足
- 清理:Docker会占用大量磁盘空间,定期清理无用资源。
# 删除所有已停止的容器 docker container prune -f # 删除所有未被使用的镜像(悬空镜像) docker image prune -f # 删除所有未被使用的卷(谨慎!确认卷内无重要数据) docker volume prune -f # 删除所有未被使用的网络 docker network prune -f # 一键清理所有未被使用的资源(容器、镜像、网络、卷,构建缓存) docker system prune -af --volumes
问题5:如何更新已部署的应用?
- 流程:
- 将最新代码提交并推送到Git仓库。
- 在服务器上拉取最新代码:
git pull。 - 重新构建镜像并启动:
docker-compose up -d --build。Docker Compose会检测到镜像变化,先构建新镜像,然后停止旧容器,用新镜像启动新容器,实现无缝更新(会有极短服务中断)。 - 如果更新失败,可以快速回滚到上一个版本的镜像。
7.3 数据备份与恢复
对于数据库等有状态服务,备份至关重要。
备份MySQL数据卷:
# 创建一个临时容器,挂载MySQL的数据卷和主机备份目录,执行备份命令 docker run --rm -v mysql-data:/volume -v /opt/backups:/backup alpine \ tar -czf /backup/mysql-backup-$(date +%Y%m%d-%H%M%S).tar.gz -C /volume ./从备份恢复:
# 首先,停止并删除现有的MySQL容器和数据卷(注意:这会丢失现有数据!) docker-compose down -v # 重新启动服务(会创建新的空数据卷) docker-compose up -d mysql # 等待MySQL容器完全启动后,执行恢复 docker run --rm -v mysql-data:/volume -v /opt/backups:/backup alpine \ sh -c "rm -rf /volume/* && tar -xzf /backup/mysql-backup-20231027-120000.tar.gz -C /volume" # 重启应用容器 docker-compose up -d app整个流程走下来,从一台裸机Linux服务器到成功运行一个由Docker容器化的完整应用栈,涉及了环境准备、镜像构建、服务编排、生产优化和运维排查的全链路。Docker部署的魅力在于其声明式和一致性,一旦Dockerfile和docker-compose.yml定型,在任何环境下的部署都变得可预测且高效。记住,多实践、多踩坑,遇到问题善用docker-compose logs和docker exec进入容器排查,你的容器化部署技能会越来越熟练。
