第 19 章 · Docker/K8s 交付与 Java 框架选型
本章目标:为 shop-spring-demo 编写多阶段 Dockerfile、.dockerignore 与非 root 用户;使用 docker-compose.prod.yml 编排 app + Nginx + MySQL + Redis;理解 K8s 核心对象并阅读虚构 api.example.com YAML;全面对照 Spring Boot vs Django / Flask / FastAPI;回答面试框架选型;完成 19 章结业总结与 100 分进阶验收表(选修);说明与 spring-cloud-web、xiaozi-cloud 衔接。
学时建议:6~8 小时(含 3 小时容器演练)
前置:ch11 jar 部署;ch18 可观测性;ch10 Redis。微服务深度见 spring-cloud-web;K8s 运维见 xiaozi-cloud。
19.1 从「能跑」到「可交付」
源码 ──mvn package──► docker build ──► registry.example.com/shop-spring:1.0.0
│
▼
K8s Deployment / compose
api.example.com
| 交付物 | 说明 |
|---|---|
| 可执行 jar | spring-boot-maven-plugin 胖 jar |
| Docker 镜像 | 不可变制品,含 JRE 与应用 |
| compose / K8s YAML | 拓扑与配置 |
.env.example | 文档化变量,不含密钥 |
虚构 shop-spring-demo、api.example.com、registry.example.com;严禁真实镜像仓库凭证、Kubeconfig、数据库密码。
19.2 多阶段 Dockerfile
shop-spring-demo/Dockerfile:
# ── 构建阶段 ──
FROM eclipse-temurin:21-jdk-alpine AS builder
WORKDIR /build
COPY pom.xml mvnw ./
COPY .mvn .mvn
RUN ./mvnw -q -DskipTests dependency:go-offline
COPY src ./src
RUN ./mvnw -q -DskipTests package
# ── 运行阶段 ──
FROM eclipse-temurin:21-jre-alpine AS runtime
RUN addgroup -g 1000 app && adduser -u 1000 -G app -D app
WORKDIR /app
COPY --from=builder /build/target/shop-spring-demo-*.jar app.jar
USER app
EXPOSE 8080
ENV JAVA_OPTS="-XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s \
CMD wget -qO- http://127.0.0.1:8080/actuator/health || exit 1
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS -jar app.jar"]
.dockerignore:
target/
.git/
.idea/
*.iml
.env
logs/
| 实践 | 原因 |
|---|---|
| 多阶段 | 镜像不含 JDK 与源码 |
| 非 root | 降低容器逃逸风险 |
MaxRAMPercentage | K8s 内存 limit 下自适应堆 |
| HEALTHCHECK | 配合 orchestrator 探针 |
19.3 docker-compose.prod.yml
services:
app:
image: registry.example.com/shop-spring:1.0.0
environment:
SPRING_PROFILES_ACTIVE: prod
DB_HOST: mysql
DB_PASSWORD: ${DB_PASSWORD}
REDIS_HOST: redis
depends_on:
mysql: { condition: service_healthy }
redis: { condition: service_started }
ports: ["8080:8080"]
mysql:
image: mysql:8.0
environment:
MYSQL_DATABASE: shop_spring
MYSQL_USER: shop_spring
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
volumes: [mysql_data:/var/lib/mysql]
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports: ["6379:6379"]
nginx:
image: nginx:1.25-alpine
ports: ["443:443", "80:80"]
volumes:
- ./nginx/api.example.com.conf:/etc/nginx/conf.d/default.conf:ro
depends_on: [app]
volumes:
mysql_data:
Nginx 反代片段(虚构域名):
server {
listen 443 ssl;
server_name api.example.com;
location / {
proxy_pass http://app:8080;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
location /actuator/prometheus {
allow 10.0.0.0/8;
deny all;
proxy_pass http://app:9090;
}
}
19.4 Kubernetes 核心对象
k8s/deployment.yaml(教学简化):
apiVersion: apps/v1
kind: Deployment
metadata:
name: shop-spring
namespace: shop-demo
spec:
replicas: 2
selector:
matchLabels: { app: shop-spring }
template:
metadata:
labels: { app: shop-spring }
spec:
containers:
- name: app
image: registry.example.com/shop-spring:1.0.0