Docker 与部署

“在我机器上能跑啊!“——这是开发对运维最经典的甩锅台词。你的机器装了 JDK 21、MySQL 8、Redis 7、一堆环境变量,到了服务器全没有,程序当然跑不起来。

Docker 把这锅彻底砸了——它把你的程序和它依赖的一切(OS 库、运行时、配置)打包成一个容器,这个容器在任何装了 Docker 的机器上跑都一样。“在我机器上能跑”从此等于”在哪都能跑”。

这一章我们看 Docker 是什么、怎么用 Docker 打包 Java 应用、Docker Compose 怎么编排多服务、以及 Kubernetes 怎么管理一大群容器。

一、容器 vs 虚拟机

容器化不是新概念——Linux 的 LXC、Solaris 的 Zones 早就有了。Docker 把它做简单了。

1.1 虚拟机

虚拟机(VM)——用 Hypervisor 在物理机上模拟出多个完整操作系统。每个 VM 有自己的内核、系统库、应用。

物理机
 └ Hypervisor
   ├ VM1: 完整 OS (内核 + 库 + 应用)
   ├ VM2: 完整 OS (内核 + 库 + 应用)
   └ VM3: 完整 OS (内核 + 库 + 应用)

优点——隔离彻底。缺点——每个 VM 跑一个完整 OS,几 GB 内存就没了,启动慢。

1.2 容器

容器——所有容器共享主机内核,只是隔离进程、文件系统、网络。没有自己的内核。

物理机
 └ 主机内核 (Linux)
   ├ 容器1: 库 + 应用
   ├ 容器2: 库 + 应用
   └ 容器3: 库 + 应用
  • 轻量——容器只有几十 MB,不像 VM 几 GB。
  • 启动快——秒级启动(VM 要分钟级)。
  • 密度高——一台机器跑几百个容器没问题。

容器靠 Linux 内核的 Namespace(隔离视图)和 Cgroup(限制资源)实现——Namespace 让容器以为自己独占系统,Cgroup 限制它用多少 CPU/内存。

二、Docker 核心概念

2.1 镜像与容器

  • 镜像(Image)——只读模板,包含应用和依赖。类似 OOP 里的”类”。
  • 容器(Container)——镜像的运行实例,可读写。类似”对象”。
docker pull openjdk:21           # 拉取镜像
docker images                    # 查看镜像
docker run -it openjdk:21 java -version   # 运行容器
docker ps                        # 查看运行中的容器
docker stop <container-id>       # 停止
docker rm <container-id>         # 删除

2.2 镜像分层

镜像是一层一层堆出来的——基础镜像层 + 加依赖层 + 加应用层。每层都是只读的,多镜像共享底层,节省空间。

镜像层 (从下到上):
  ├ debian:bookworm-slim   (基础 OS, 80MB)
  ├ openjdk:21-slim        (+ JDK, 200MB)
  ├ app.jar                (+ 应用, 30MB)
  └ 配置文件               (+ 配置, 1KB)

容器启动时在最上层加一个可写层——所有修改写在这里,镜像本身不变。

2.3 Dockerfile:镜像的”配方”

Dockerfile 是个文本文件,描述怎么构建镜像:

# 基础镜像
FROM openjdk:21-slim

# 工作目录
WORKDIR /app

# 复制 jar 包
COPY target/myapp.jar /app/app.jar

# 暴露端口
EXPOSE 8080

# 启动命令
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

构建并运行:

docker build -t myapp:1.0 .       # 构建镜像
docker run -d -p 8080:8080 myapp:1.0   # 后台运行, 端口映射

三、Java 应用的 Docker 最佳实践

3.1 多阶段构建

把”构建”和”运行”分开——构建阶段用 Maven 镜像打包,运行阶段只拷 jar 到精简的 JRE 镜像。最终镜像小很多。

# 阶段 1: 构建
FROM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /build
COPY pom.xml .
RUN mvn dependency:go-offline           # 先下依赖 (利用缓存)
COPY src ./src
RUN mvn package -DskipTests

# 阶段 2: 运行
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /build/target/*.jar app.jar
EXPOSE 8080
ENTRYPOINT ["java", "-jar", "app.jar"]

阶段 1 用 600MB 的 Maven 镜像打包,阶段 2 只拷贝生成的 jar 到 150MB 的 JRE 镜像。最终镜像约 180MB——比单阶段构建小一半。

3.2 Spring Boot 分层

Spring Boot 2.3+ 支持分层 jar——把依赖、应用代码、资源分层打包,Docker 构建时每层一个 COPY,依赖不变就缓存命中,构建快。

FROM eclipse-temurin:21-jre as layers
WORKDIR /app
COPY target/*.jar app.jar
RUN java -Djarmode=layertools -jar app.jar extract

FROM eclipse-temurin:21-jre
WORKDIR /app
COPY --from=layers /app/dependencies/ ./
COPY --from=layers /app/spring-boot-loader/ ./
COPY --from=layers /app/snapshot-dependencies/ ./
COPY --from=layers /app/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.launch.JarLauncher"]

改了代码只重建 application 层,依赖层缓存命中——构建从 2 分钟降到 20 秒。

3.3 JVM 容器化注意事项

  • 内存限制感知——Java 10+ 自动识别 cgroup 内存限制。设 -XX:MaxRAMPercentage=75.0 让 JVM 用容器内存的 75%。
  • 不要 swap——容器 swap 慢,JVM GC 会卡。设 --memory-swap=内存相等
  • 使用 ZGC/Shenandoah——低延迟 GC 更适合容器。
ENTRYPOINT ["java", "-XX:MaxRAMPercentage=75.0", "-XX:+UseZGC", "-jar", "app.jar"]

四、Docker Compose:多容器编排

微服务有几十个容器,手动 docker run 不现实。Docker Compose 用一个 YAML 描述所有服务,一条命令全起来。

# docker-compose.yml
version: '3.8'
services:
  app:
    build: .
    ports:
      - "8080:8080"
    depends_on:
      - mysql
      - redis
    environment:
      SPRING_DATASOURCE_URL: jdbc:mysql://mysql:3306/mydb
      SPRING_REDIS_HOST: redis

  mysql:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: mydb
    volumes:
      - mysql_data:/var/lib/mysql

  redis:
    image: redis:7-alpine
    ports:
      - "6379:6379"

volumes:
  mysql_data:
docker-compose up -d        # 启动所有服务 (后台)
docker-compose logs -f app  # 看 app 日志
docker-compose down         # 停止并删除

Compose 自动创建一个网络,服务间用服务名(mysqlredis)互相访问——比 IP 方便。

五、Kubernetes:容器编排的王者

Docker Compose 适合单机。几十台机器、上千容器时,需要 Kubernetes(K8s)——谷歌开源的容器编排平台。

5.1 核心概念

  • Pod——K8s 最小调度单位,一个或多个紧密耦合的容器。
  • Deployment——管理 Pod 副本数,保证始终有 N 个实例运行。
  • Service——给一组 Pod 一个稳定的访问入口(Pod IP 会变,Service IP 不变)。
  • Ingress——HTTP 层入口,按域名/路径路由到 Service。
  • ConfigMap / Secret——配置和敏感信息。
用户 → Ingress → Service → Pod (容器)

              Deployment 管理 Pod 副本

5.2 部署一个 Java 应用

# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 3                    # 3 个副本
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
        - name: myapp
          image: myapp:1.0
          ports:
            - containerPort: 8080
          resources:
            limits:
              memory: "512Mi"
              cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
  name: myapp
spec:
  selector:
    app: myapp
  ports:
    - port: 80
      targetPort: 8080
  type: LoadBalancer
kubectl apply -f deployment.yaml   # 部署
kubectl get pods                   # 查看 Pod
kubectl scale deployment myapp --replicas=5   # 扩容到 5 个
kubectl rollout restart deployment myapp      # 滚动更新

K8s 的杀手锏——自愈弹性扩缩。Pod 挂了自动重启,流量大时自动扩容,闲时自动缩容。

六、CI/CD:自动化流水线

CI/CD(Continuous Integration / Continuous Deployment)——代码 push 后自动构建、测试、部署。

push 代码 → CI 服务器 → 编译 → 单测 → 打镜像 → 推镜像仓库 → CD 部署到 K8s

主流工具——GitHub ActionsGitLab CIJenkins

GitHub Actions 示例(.github/workflows/build.yml):

name: Build and Deploy
on:
  push:
    branches: [main]
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-java@v4
        with:
          java-version: '21'
          distribution: 'temurin'
      - name: Build with Maven
        run: mvn package -DskipTests
      - name: Build Docker image
        run: docker build -t myapp:${{ github.sha }} .
      - name: Push to registry
        run: |
          docker login -u ${{ secrets.REGISTRY_USER }} -p ${{ secrets.REGISTRY_PASS }}
          docker push myapp:${{ github.sha }}
      - name: Deploy to K8s
        run: |
          kubectl set image deployment/myapp myapp=myapp:${{ github.sha }}

每次 push 到 main 分支,自动构建、打镜像、推仓库、滚动部署——开发者只管 push,运维全自动。

七、实战:用 Java SE 模拟 Docker

Piston 在线环境跑不了 Docker。我们用 Java SE 模拟 Docker 的核心概念——镜像分层、容器生命周期、端口映射、Compose 编排。

Java · 在线运行

观察重点:镜像分层构建,每层一个步骤;容器有 created/running/stopped 生命周期;端口映射让外部能访问容器;Compose 按依赖顺序启动多服务;滚动更新逐个替换实例,保证零停机。

八、部署架构演进

物理机 → 虚拟机 → 容器 → K8s → Serverless
阶段单位特点
物理机整机隔离差,资源浪费
虚拟机VM隔离好,重
容器容器轻量,秒级启动
K8sPod自动调度、自愈、扩缩
Serverless函数按调用计费,无需管服务器

九、本章小结

知识点要点
容器 vs VM容器共享内核,轻量秒启;VM 隔离彻底但重
镜像只读分层模板,类似”类”
容器镜像的运行实例,类似”对象”
Dockerfile镜像构建配方:FROM/COPY/RUN/ENTRYPOINT
多阶段构建构建阶段 + 运行阶段,最终镜像小
Docker Compose单机多容器编排,YAML 描述
Kubernetes集群级容器编排,Pod/Deployment/Service
CI/CD自动化构建-测试-部署流水线

记忆口诀

  • 容器 vs VM——容器共享内核轻,VM 各带内核重。
  • 镜像容器关系——镜像是类,容器是对象。
  • Dockerfile 四件套——FROM 打底,COPY 拷贝,RUN 执行,ENTRYPOINT 启动。
  • 多阶段构建——构建用大镜像,运行用小镜像。
  • K8s 三大件——Pod 跑容器,Deployment 管副本, Service 给入口。

结语

Docker 把”环境一致”这件事做到了极致——一次打包,处处运行。K8s 又把”管理容器”这件事自动化了——自愈、扩缩、滚动更新。加上 CI/CD 流水线,从代码到上线全程自动化。

这是第十四阶段最后一篇。回顾这一阶段——设计模式教你写好代码,数据结构算法教你想清楚,Redis/消息队列/微服务/Docker 教你构建分布式系统。从单机到分布式,从代码到部署,你已经具备了一个 Java 工程师的核心知识体系。

下一阶段我们进入综合实战——亲手做一个完整的待办事项 CLI 项目,把前面学到的 IO、集合、并发、反射全用上。然后最后一阶段——面试题精讲,把零散知识串成应试武器。