在这里插入图片描述

👋 大家好,欢迎来到我的技术博客!
📚 在这里,我会分享学习笔记、实战经验与技术思考,力求用简单的方式讲清楚复杂的问题。
🎯 本文将围绕Docker这个话题展开,希望能为你带来一些启发或实用的参考。
🌱 无论你是刚入门的新手,还是正在进阶的开发者,希望你都能有所收获!


文章目录

Docker - 核心概念速通,镜像、容器、仓库一次弄懂 🐳

“Docker 不是虚拟机,但它让部署像搭积木一样简单。”
—— 这句话背后,藏着三个看似简单却常被混淆的核心概念:镜像(Image)容器(Container)仓库(Registry)
本文不堆砌术语,不罗列命令,而是用 Java 开发者最熟悉的视角——从 mvn clean packagejava -jar app.jar,再到 docker run myapp:1.2.0——层层剥开 Docker 的本质逻辑。全程配有可运行的 Java 示例、清晰的 Mermaid 流程图、真实可用的外链资源,以及关键原理的直觉化类比。读完,你将真正理解:
✅ 为什么 docker build 不等于“编译”,而更像“制作标准化安装包”?
✅ 为什么 docker run 启动的是“进程快照”,而非“操作系统副本”?
✅ 为什么 docker push 推送的不是代码,而是可复现的执行环境契约


一、先问一个灵魂问题:没有 Docker,Java 应用部署有多“脆弱”? 💥

假设你正在维护一个 Spring Boot 微服务:user-service。它依赖:

  • Java 17(OpenJDK)
  • Maven 3.8+
  • MySQL 8.0.33(含特定字符集配置)
  • Redis 7.0(启用 TLS)
  • 环境变量 SPRING_PROFILES_ACTIVE=prodDB_URL=jdbc:mysql://db:3306/userdb

你本地开发一切正常 ✅
测试环境上线后报错 ❌

Caused by: java.sql.SQLException: Unknown system variable 'default_authentication_plugin'

排查发现:测试环境 MySQL 是 5.7,而你的 application.yml 中用了 8.0+ 特性。

运维同事说:“换 MySQL 版本太重,我们加个兼容层吧。”
你回复:“那我本地也得降级?CI/CD 流水线怎么改?”
……最终,大家在 Slack 里发了 47 条消息,耗时 3 小时,临时打了补丁。

⚠️ 这就是典型的 环境不一致(Environment Drift)

开发 → 测试 → 预发 → 生产,每个环节的 OS、库版本、配置路径、甚至时区都可能不同。
Java 的 “Write Once, Run Anywhere” 在 JVM 层成立,但在 JVM 之下的操作系统层,它失效了。

Docker 要解决的,正是这个“JVM 之下”的不确定性。它不替代 Java,而是为 Java 提供一个可移植、可声明、可验证的运行基座


二、镜像(Image):一份“只读的、自包含的、可复现的执行环境说明书” 📜

2.1 镜像是什么?—— 类比 Java 的 .jar 文件 🔍

想象你写了一个极简的 Spring Boot Web 应用:

// src/main/java/com/example/hello/HelloController.java
package com.example.hello;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {
    @GetMapping("/api/hello")
    public String hello() {
        return "Hello from Dockerized Spring Boot! 🌟";
    }
}

mvn clean package 后生成 target/hello-spring-boot-0.0.1-SNAPSHOT.jar
这个 JAR 文件包含了:

  • 编译好的 .class 字节码
  • META-INF/MANIFEST.MF(声明主类、依赖等)
  • BOOT-INF/lib/ 下所有依赖 JAR
  • application.properties 配置(打包进去了)

✅ 它是一个自包含(self-contained) 的交付物:只要有 java -version >= 17,就能运行。

Docker 镜像,就是这个理念在操作系统层的延伸。
它不是一个“操作系统 ISO”,而是一份分层的、只读的、声明式的环境说明书,描述了:

  • 基础操作系统(如 openjdk:17-jre-slim
  • 必需的二进制依赖(如 curl, jq
  • 应用文件(你的 hello-spring-boot-*.jar
  • 启动命令(ENTRYPOINT ["java", "-jar", "/app.jar"]
  • 环境变量、端口暴露、工作目录等元数据

它和 JAR 一样:
🔹 不可变(Immutable):构建后内容固定,无法修改(只能 docker build 新镜像)
🔹 可传输(Transferable):可 push 到远程仓库,pull 到任意机器
🔹 可验证(Verifiable):通过 sha256 摘要唯一标识,确保“所见即所得”

✅ 关键认知:镜像 ≠ 虚拟机镜像。VM 镜像包含完整内核 + 用户空间;Docker 镜像只包含用户空间(rootfs),共享宿主机内核。轻量百倍,启动毫秒级。

2.2 构建一个 Java 镜像:Dockerfile 实战 🛠️

创建项目根目录下的 Dockerfile

# 第一行:指定基础镜像(官方 OpenJDK 17 运行时,Debian Slim 版)
FROM openjdk:17-jre-slim

# 维护者信息(非必需,但推荐)
LABEL maintainer="dev@mycompany.com"

# 创建非 root 用户提升安全性(重要!)
RUN groupadd -g 1001 -f appgroup && useradd -s /bin/bash -u 1001 -g appgroup appuser

# 设置工作目录
WORKDIR /app

# 复制本地 JAR 到镜像中(注意:使用相对路径,避免 COPY 整个 target 目录)
COPY target/hello-spring-boot-0.0.1-SNAPSHOT.jar app.jar

# 暴露应用端口(仅文档作用,实际防火墙需另行配置)
EXPOSE 8080

# 设置运行用户(安全最佳实践)
USER appuser

# 定义启动命令(ENTRYPOINT 更适合封装,CMD 可被覆盖)
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/app.jar"]

💡 解析关键指令:

指令 作用 类比 Java
FROM 指定父镜像,构成镜像的第一层 extends 父类(但这里是组合,非继承)
COPY 将本地文件复制进镜像文件系统 maven-assembly-plugin 打包资源
EXPOSE 声明容器监听端口(不自动映射) @SpringBootApplication 上的 @EnableAutoConfiguration(声明能力,非强制生效)
ENTRYPOINT 容器启动时执行的默认命令(不易被覆盖) public static void main(String[]) —— 程序入口点

⚠️ 注意:不要写 RUN java -jar app.jar!那是运行时行为,镜像构建阶段应只做“准备”,不“执行”。

构建镜像(确保已 mvn clean package):

docker build -t hello-spring-boot:1.0.0 .

输出类似:

[+] Building 12.5s (10/10) FINISHED
 => [internal] load build definition from Dockerfile                       0.0s
 => => transferring dockerfile: 366B                                       0.0s
 => [internal] load .dockerignore                                            0.0s
 => => transferring context: 2B                                              0.0s
 => [internal] load metadata for docker.io/library/openjdk:17-jre-slim     1.2s
 => [1/5] FROM docker.io/library/openjdk:17-jre-slim@sha256:...            10.1s
 => => resolve docker.io/library/openjdk:17-jre-slim@sha256:...              0.0s
 => [2/5] RUN groupadd -g 1001 -f appgroup && useradd ...                   0.5s
 => [3/5] WORKDIR /app                                                       0.0s
 => [4/5] COPY target/hello-spring-boot-0.0.1-SNAPSHOT.jar app.jar          0.1s
 => [5/5] EXPOSE 8080                                                        0.0s
 => exporting to image                                                       0.2s
 => => exporting layers                                                      0.2s
 => => writing image sha256:9a8b7c6d5e4f3a2b1c0d9e8f7a6b5c4d3e2f1a0b9c8d7e6f5a4b3c2d1 0.0s
 => => naming to docker.io/library/hello-spring-boot:1.0.0                  0.0s

✅ 镜像构建完成!现在它是一个静态的、可分发的实体。

2.3 镜像的分层存储机制:为什么 docker build 很快? 🧱

Docker 镜像由**只读层(Read-Only Layers)**叠加而成。每条 Dockerfile 指令(FROM, RUN, COPY, EXPOSE)都会生成一层。这些层被缓存并复用。

Mermaid 图表直观展示分层结构:

Image hello-spring-boot:1.0.0

Base Layer
openjdk:17-jre-slim
- Debian rootfs
- OpenJDK 17 JRE

Layer 2
RUN groupadd & useradd
- 创建 appgroup/appuser

Layer 3
WORKDIR /app
- 创建 /app 目录

Layer 4
COPY app.jar
- 添加 15MB 的 jar 包

Layer 5
EXPOSE 8080
- 元数据:声明端口

Layer 6
ENTRYPOINT ...
- 元数据:定义启动命令

✅ 分层优势:

  • 高效复用:若只改 app.jarCOPY 指令),只有 Layer 4 及之后层重建,前面 3 层直接复用缓存。
  • 节省空间:多个镜像共享相同基础层(如 openjdk:17-jre-slim),物理磁盘只存一份。
  • 安全审计:可逐层检查 RUN apt-get update 是否引入风险包。

🔗 想深入理解分层?访问 Docker 官方文档:Images and layers(权威、实时更新、无登录墙)


三、容器(Container):镜像的“运行时实例” —— 一个受控的进程沙盒 🧪

3.1 容器是什么?—— 类比 java -jar app.jar 的进程 🔗

当你执行:

java -jar hello-spring-boot-0.0.1-SNAPSHOT.jar

发生了什么?

  • JVM 启动一个 java 进程(PID=1234)
  • 加载 app.jar 和所有依赖到内存
  • 绑定 localhost:8080(或失败,因端口被占)
  • 进程生命周期:从 main() 开始,到 System.exit() 或异常终止

Docker 容器,就是这个过程的增强版:
它把 java -jar app.jar 这个命令,放到一个隔离的、资源受限的、网络独立的 Linux 命名空间(Namespace)和控制组(Cgroup) 中运行。

✅ 容器 = 镜像 + 可写层(Container Layer) + 隔离运行时环境(Namespaces + Cgroups)

  • 可写层(Top Layer):容器启动时,在镜像最上层叠加一个可读写层。所有运行时改动(如 touch /tmp/log.txt, echo "ok" > status)都发生在此层。容器停止后,此层默认丢弃(除非用 -v 挂载卷)。
  • 命名空间(Namespaces):提供视图隔离
    • pid:进程 PID 隔离 → 容器内 ps aux 只看到自己的进程
    • net:网络栈隔离 → 容器有自己 lo, eth0,IP 独立
    • mnt:文件系统挂载点隔离 → df -h 只显示容器视角的磁盘
  • 控制组(Cgroups):提供资源限制
    • --memory=512m:限制最大内存使用
    • --cpus=1.5:限制最多使用 1.5 个 CPU 核心
    • --pids-limit=100:限制最多 100 个进程

3.2 启动一个容器:docker run 的全貌 🚀

运行刚构建的镜像:

docker run \
  --name hello-app \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=prod \
  -e DB_URL=jdbc:mysql://mysql-host:3306/userdb \
  --rm \
  hello-spring-boot:1.0.0

参数详解:

参数 作用 类比 Java
--name hello-app 为容器指定易记名称 Thread.currentThread().setName("hello-worker")
-p 8080:8080 将宿主机 8080 端口映射到容器 8080 端口 ServerSocket server = new ServerSocket(8080)(但绑定的是容器网络)
-e KEY=VALUE 向容器注入环境变量 System.setProperty("spring.profiles.active", "prod")
--rm 容器退出后自动删除(清理可写层) try { ... } finally { cleanup(); }(自动资源回收)

✅ 启动后,你会看到 Spring Boot 的标准日志:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.2.0)

2024-04-10T10:22:33.123Z  INFO 1 --- [           main] c.e.h.HelloSpringBootApplication       : Started HelloSpringBootApplication in 2.1 seconds (process running for 2.5)

此时,打开浏览器访问 http://localhost:8080/api/hello → 显示 "Hello from Dockerized Spring Boot! 🌟"

3.3 容器 vs 虚拟机:一张图看懂本质差异 🆚

宿主机操作系统
Linux Kernel 6.5

Docker 容器方案

直接调用系统调用

直接调用

直接调用

Linux Kernel
- 进程调度
- 内存管理
- 网络协议栈
- 设备驱动

Docker Engine
(守护进程 dockerd)

Container Layer
(可写层,/tmp, /var/log)

Image Layers
(只读层:OpenJDK + app.jar)

虚拟机方案

Hypervisor
(如 VirtualBox/KVM)

Guest OS Kernel
(完整 Linux Kernel)

Java App
(运行在 Guest OS 上)

核心结论:

维度 虚拟机(VM) Docker 容器
启动速度 秒级(启动 Guest OS) 毫秒级(启动进程)
资源占用 GB 级(完整 OS 内存 + 磁盘) MB 级(仅应用 + 运行时)
隔离强度 强(硬件级,完全独立内核) 中(OS 级,共享宿主机内核)
适用场景 运行不同 OS(Windows/Linux)、强安全隔离需求 微服务、CI/CD、开发环境一致性

🔗 对隔离原理好奇?推荐阅读 Red Hat 官方文章:How containers work(图文并茂,原理清晰)

3.4 容器生命周期管理:不只是 runstop 🔄

容器是短暂的(Ephemeral)。掌握其状态流转至关重要:

created

docker rm -f

docker run/start

docker start

docker pause

docker unpause

exit code / SIGTERM

docker start

docker rm

exec ENTRYPOINT/CMD

running

healthcheck passes

healthcheck fails 3x

healthcheck recovers

healthy

unhealthy

paused

exited

removed

waiting_for_start

starting

Healthcheck is defined in Dockerfile:
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ CMD curl -f http://localhost:8080/actuator/health || exit 1

常用命令实战:

# 查看正在运行的容器
docker ps

# 查看所有容器(包括已退出)
docker ps -a

# 查看容器日志(实时跟踪)
docker logs -f hello-app

# 进入容器执行命令(调试用,生产慎用)
docker exec -it hello-app /bin/sh

# 发送信号优雅停止(触发 Spring Boot 的 Shutdown Hook)
docker stop hello-app

# 强制杀死(类似 kill -9)
docker kill hello-app

# 删除已停止容器
docker rm hello-app

# 删除镜像(需先删容器)
docker rmi hello-spring-boot:1.0.0

💡 生产最佳实践:

  • 永远使用 --rm 或明确 docker rm 清理,避免磁盘被僵尸容器占满。
  • HEALTHCHECK 定义健康探针,让编排工具(如 Kubernetes)知道何时重启。
  • 容器内永不运行多个主进程(如 nginx + java),应拆分为多个容器(nginx-container + app-container)。

四、仓库(Registry):镜像的“Maven Central” —— 团队协作的枢纽 🌐

4.1 仓库是什么?—— 类比 Maven 仓库的核心定位 📦

Maven 项目 pom.xml 中:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.0</version>
</dependency>

Maven 如何找到这个 JAR?
→ 查询 https://repo.maven.apache.org/maven2/(中央仓库)
→ 下载 org/springframework/boot/spring-boot-starter-web/3.2.0/...jar
→ 本地缓存 ~/.m2/repository/

Docker Registry 就是这个逻辑的镜像版:
它是一个 HTTP 服务,用于:

  • 存储 镜像的分层数据(tar 包 + JSON 元数据)
  • 索引 镜像标签(tag),如 hello-spring-boot:1.0.0, hello-spring-boot:latest
  • 认证 用户权限(谁可以 push?谁可以 pull?)
  • 分发 镜像到全球任何一台装了 Docker 的机器

✅ 仓库 ≠ Docker Hub。Docker Hub 是一个公共 Registry 实例(由 Docker Inc. 运营),就像 Maven Central 是 Apache 运营的公共 Maven 仓库。你完全可以搭建私有 Registry(如 Harbor、AWS ECR、阿里云 ACR)。

4.2 使用 Docker Hub:公共镜像的获取与推送 🚪

▶ 获取公共镜像(docker pull
# 拉取官方 OpenJDK 镜像(无需指定 registry,缺省为 Docker Hub)
docker pull openjdk:17-jre-slim

# 拉取 Nginx(Web 服务器,常作反向代理)
docker pull nginx:alpine

# 拉取 MySQL(数据库)
docker pull mysql:8.0

这些镜像都托管在 Docker Hub —— 全球最大的公共容器镜像仓库。你可以在这里搜索、查看 Dockerfile、阅读文档。

🔗 访问 Docker Hub 官网(可直接搜索 openjdk,查看官方镜像详情页)

▶ 推送私有镜像(docker push

要推送,需先登录:

docker login
# 输入 Docker ID 和密码(非邮箱!)

然后给镜像打上符合 username/repository:tag 格式的标签:

# 查看当前镜像 ID
docker images | grep hello-spring-boot

# 假设 IMAGE ID 是 9a8b7c6d5e4f
docker tag 9a8b7c6d5e4f your-docker-id/hello-spring-boot:1.0.0

# 推送到 Docker Hub
docker push your-docker-id/hello-spring-boot:1.0.0

✅ 推送成功后,任何人只要执行:

docker pull your-docker-id/hello-spring-boot:1.0.0

就能获得完全一致的镜像——无论他在东京、法兰克福还是圣保罗。这就是“一次构建,处处运行”的基石。

4.3 私有仓库实践:为什么企业必须用它? 🔐

Docker Hub 有明显局限:

  • ❌ 免费账户仅支持 1 个私有仓库
  • ❌ 无细粒度权限控制(如:研发组只能 pull,运维组可 push
  • ❌ 无漏洞扫描(CVE 检测)
  • ❌ 网络延迟高(国内访问慢)

企业级方案:Harbor(CNCF 毕业项目,开源免费)

  • ✅ Web UI 管理镜像、项目、用户
  • ✅ RBAC 权限模型(project-admin, developer, guest
  • ✅ 集成 Trivy 扫描镜像漏洞
  • ✅ 支持 Helm Chart 仓库(统一管理容器和 K8s 应用)

部署 Harbor 只需几行命令(基于 Docker Compose):

# 下载离线安装包(官网提供)
wget https://github.com/goharbor/harbor/releases/download/v2.10.0/harbor-offline-installer-v2.10.0.tgz

# 解压并编辑 harbor.yml(配置域名、证书、存储路径)
tar xvf harbor-offline-installer-v2.10.0.tgz
cd harbor
vi harbor.yml  # 设置 hostname: harbor.mycompany.com

# 安装(自动拉取所需镜像并启动)
sudo ./install.sh

🔗 获取 Harbor 最新安装指南:Harbor 官方文档

4.4 镜像标签(Tag)的语义化实践:别再用 latest

latest 是 Docker 默认标签,但它是个“移动目标”:

docker pull nginx      # 等价于 docker pull nginx:latest
# 下次 pull,可能已是全新大版本(如 1.25 → 1.26),导致兼容性问题!

✅ 推荐策略(参考 Semantic Versioning 2.0):

标签类型 示例 用途 安全性
精确版本 hello-spring-boot:1.0.0 生产环境部署 ✅ 最高(不可变)
次要版本 hello-spring-boot:1.0 CI/CD 流水线自动构建 ⚠️ 中(1.0.x 自动更新)
主要版本 hello-spring-boot:1 基础设施测试(如兼容性验证) ⚠️ 低(1.x.y 自动更新)
Git Commit hello-spring-boot:abc1234 追溯构建来源 ✅ 高(唯一)
时间戳 hello-spring-boot:20240410-1422 日常构建快照 ✅ 高(唯一)

Dockerfile 中,永远显式指定基础镜像版本:

# ✅ GOOD:锁定 OpenJDK 版本
FROM openjdk:17.0.2-jre-slim

# ❌ BAD:使用 latest,下次构建可能失败
# FROM openjdk:latest

五、Java 开发者必知:Docker 与 Spring Boot 的深度协同 🌱

5.1 Spring Boot 3.x 原生支持容器化:jlinklayers.idx 🎯

Spring Boot 2.3+ 引入 BuildpacksLayered JAR,大幅优化容器体验。

▶ 方案一:使用 Buildpacks(零 Dockerfile

Maven 插件自动构建 OCI 镜像:

<!-- pom.xml -->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
            <configuration>
                <image>
                    <builder>paketobuildpacks/builder-jammy-base</builder>
                    <name>your-registry.com/myapp:${project.version}</name>
                </image>
            </configuration>
        </plugin>
    </plugins>
</build>

执行:

mvn spring-boot:build-image

✅ 自动生成优化镜像(基于 distroless,无 Shell,更小更安全),无需手写 Dockerfile

▶ 方案二:分层 JAR(Layered JAR)加速构建

Spring Boot 2.3+ 将 app.jar 拆分为多层:

unzip -l target/hello-spring-boot-0.0.1-SNAPSHOT.jar | head -20

输出含:

META-INF/
META-INF/MANIFEST.MF
BOOT-INF/
BOOT-INF/classes/
BOOT-INF/lib/               ← 依赖层(变化少,可缓存)
BOOT-INF/layers.idx         ← 层定义文件

layers.idx 内容示例:

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/resources/"

配合 Dockerfile 利用分层:

# 利用 Spring Boot 分层,实现极速 rebuild
FROM eclipse/jetty:11-jre17-slim

# 复制分层 JAR
ARG JAR_FILE=target/hello-spring-boot-0.0.1-SNAPSHOT.jar
COPY ${JAR_FILE} app.jar

# 提取分层(关键!)
RUN java -Djarmode=layertools -jar app.jar extract

# 按层复制(依赖层复用率高)
FROM eclipse/jetty:11-jre17-slim
COPY --from=0 dependencies/ ./
COPY --from=0 spring-boot-loader/ ./
COPY --from=0 snapshot-dependencies/ ./
COPY --from=0 application/ ./

ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]

✅ 效果:当只改业务代码(application 层),docker build 仅需复制最后一层,秒级完成。

5.2 容器内 Java 应用调优:别让 JVM “瞎猜” 🧠

在容器中,JVM 默认行为可能出错:

  • Runtime.getRuntime().availableProcessors() 返回宿主机 CPU 数,而非容器限制值
  • -Xmx 未设置时,JVM 用宿主机内存的 1/4,远超容器 --memory=512m 限制 → OOM Kill!

✅ 正确做法(Spring Boot 2.2+ 自动适配):

# application.yml
management:
  endpoint:
    health:
      show-details: always
  endpoints:
    web:
      exposure:
        include: health,info,metrics,prometheus

# JVM 启动参数(推荐写在 ENTRYPOINT 或 docker run -e JAVA_TOOL_OPTIONS)
JAVA_TOOL_OPTIONS: "-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
  • -XX:+UseContainerSupport:启用容器感知(JDK 10+ 默认开启)
  • -XX:MaxRAMPercentage=75.0:JVM 堆最大为容器内存限制的 75%

验证是否生效:

docker run --rm -m 512m hello-spring-boot:1.0.0 java -XX:+PrintFlagsFinal -version | grep MaxHeapSize
# 输出应为约 384MB (512 * 0.75),而非数 GB

5.3 容器网络实战:Spring Boot 连接 MySQL 和 Redis 🌐

典型微服务架构:

[Spring Boot App] <--(HTTP)--> [Nginx]
       ↓
[MySQL]  [Redis]  [RabbitMQ]

在容器中,服务间通信靠 Docker 网络,而非 localhost

▶ 步骤 1:创建自定义网络(推荐)
docker network create myapp-network
▶ 步骤 2:启动 MySQL(带初始化脚本)
docker run -d \
  --name mysql-db \
  --network myapp-network \
  -e MYSQL_ROOT_PASSWORD=root123 \
  -e MYSQL_DATABASE=userdb \
  -v $(pwd)/init.sql:/docker-entrypoint-initdb.d/init.sql \
  -p 3306:3306 \
  mysql:8.0
▶ 步骤 3:启动 Redis
docker run -d \
  --name redis-cache \
  --network myapp-network \
  -p 6379:6379 \
  redis:7-alpine
▶ 步骤 4:启动 Spring Boot App(使用服务名作为 host)
docker run -d \
  --name hello-app \
  --network myapp-network \
  -p 8080:8080 \
  -e SPRING_PROFILES_ACTIVE=docker \
  -e SPRING_DATASOURCE_URL=jdbc:mysql://mysql-db:3306/userdb \
  -e SPRING_REDIS_HOST=redis-cache \
  hello-spring-boot:1.0.0

✅ 关键点:mysql-dbredis-cache 是容器名,Docker 内置 DNS 会将其解析为对应 IP。无需硬编码 IP!

🔗 学习 Docker 网络原理:Docker 官方网络教程


六、避坑指南:Java 工程师踩过的 5 个经典 Docker 坑 🚧

❌ 坑 1:在容器里用 localhost 连接其他容器

现象Connection refused
原因localhost 指向容器自身,而非宿主机或其他容器
解法:用 --network + 容器名(如 mysql-db),或用 host.docker.internal(Mac/Win,Linux 需 --add-host=host.docker.internal:host-gateway

❌ 坑 2:docker buildmvn clean package 太慢

现象:每次构建都重新下载 Maven 依赖
解法:利用 Docker BuildKit 的 --mount=type=cache

# 开启 BuildKit:export DOCKER_BUILDKIT=1
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /app
COPY pom.xml .
# 缓存 ~/.m2/repository
RUN --mount=type=cache,target=/root/.m2/repository \
    mvn dependency:go-offline
COPY . .
RUN mvn clean package -DskipTests

FROM openjdk:17-jre-slim
COPY --from=builder /app/target/*.jar app.jar
ENTRYPOINT ["java","-jar","/app.jar"]

❌ 坑 3:日志不输出到 docker logs

现象docker logs hello-app 为空
原因:Spring Boot 默认将日志写入 ./logs/ 文件,而非 stdout
解法:在 application.yml 中配置:

logging:
  level:
    root: INFO
  pattern:
    console: "%d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n"
  file:
    name: "logs/app.log"  # 仍可写文件,但 console 必须开启

❌ 坑 4:容器时间与宿主机不同步

现象:JWT Token 报 Invalid JWT timestamp
原因:容器内时钟未同步
解法:启动时挂载宿主机时间:

docker run -v /etc/localtime:/etc/localtime:ro ...

❌ 坑 5:java.io.tmpdir 空间不足

现象java.lang.InternalError: XXX 或 PDF 生成失败
原因:Docker 默认 /tmp 很小(64MB)
解法:启动时指定更大 tmpdir:

docker run -e JAVA_TOOL_OPTIONS="-Djava.io.tmpdir=/tmp-large" \
  -v /tmp-large:/tmp-large ...

七、总结:一张图收束全部核心概念 🧩

Ecosystem

编写

docker build

docker run

docker commit

docker push

docker pull

开发者

Dockerfile

镜像 Image
- 只读分层
- 声明式环境
- sha256 唯一标识

容器 Container
- 可写顶层
- Namespaces 隔离
- Cgroups 限流
- 进程沙盒

新镜像
(不推荐,破坏不可变性)

仓库 Registry
- 存储中心
- 权限控制
- 漏洞扫描

Docker Hub
public

Harbor
private

AWS ECR
cloud

✅ 终极一句话总结:

镜像是“Java 的 .jar”,容器是“java -jar 的进程”,仓库是“Maven Central”。
Docker 没有发明新概念,它只是把 Java 工程师早已熟稔的“封装-分发-运行”范式,完美迁移到了操作系统层。


八、下一步行动清单:学完就用 🚀

  1. 立刻实践:把你当前的 Spring Boot 项目,按本文流程写一个 Dockerfiledocker builddocker run 起来。
  2. 升级构建:尝试 mvn spring-boot:build-image,对比传统 Dockerfile 的体积与构建速度。
  3. 接入仓库:注册 Docker Hub,docker push 你的镜像,并让同事 docker pull 验证。
  4. 学习编排:用 docker-compose.yml 编排 MySQL + Redis + 你的 App,一键启动整套环境。
  5. 深入原理:阅读 Linux NamespacesCgroups v2 官方文档,理解底层魔法。

🌟 记住:Docker 的价值不在技术本身,而在于它消除了环境摩擦,让开发者专注业务逻辑。当你不再为“在我机器上是好的”而争论,真正的高效协作才开始。

祝你 Docker 之旅顺畅,代码世界澄澈如初 🐳✨
—— 写于一个容器正在后台安静运行的清晨


🙌 感谢你读到这里!
🔍 技术之路没有捷径,但每一次阅读、思考和实践,都在悄悄拉近你与目标的距离。
💡 如果本文对你有帮助,不妨 👍 点赞、📌 收藏、📤 分享 给更多需要的朋友!
💬 欢迎在评论区留下你的想法、疑问或建议,我会一一回复,我们一起交流、共同成长 🌿
🔔 关注我,不错过下一篇干货!我们下期再见!✨

Logo

智能硬件社区聚焦AI智能硬件技术生态,汇聚嵌入式AI、物联网硬件开发者,打造交流分享平台,同步全国赛事资讯、开展 OPC 核心人才招募,助力技术落地与开发者成长。

更多推荐