一、镜像1GB,启动3分钟
2020年,我们的Docker镜像有1GB,启动时间长达3分钟。
问题在哪?
- 基础镜像用了完整版openjdk:17(500MB+)
- 把构建工具也打进了镜像(Maven、Git)
- 日志文件、临时文件没清理
- 依赖包重复下载
每次发布都要等3分钟,回滚更慢。双十一时,紧急回滚花了10分钟,损失惨重。
优化后:
- 镜像从1GB降到150MB
- 启动时间从3分钟降到10秒
- 发布效率提升20倍
二、镜像优化实战
2.1 多阶段构建
dockerfile
# ===================== 构建阶段 =====================
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /build
# 先复制依赖文件,利用缓存
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 再复制源码
COPY src ./src
RUN mvn package -DskipTests -B
# 解压JAR(Spring Boot分层优化)
RUN java -Djarmode=layertools -jar target/*.jar extract --destination extracted
# ===================== 运行阶段 =====================
FROM eclipse-temurin:17-jre-alpine
WORKDIR /app
# 创建非root用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# Spring Boot分层复制(利用缓存)
COPY --from=builder /build/extracted/dependencies/ ./
COPY --from=builder /build/extracted/spring-boot-loader/ ./
COPY --from=builder /build/extracted/snapshot-dependencies/ ./
COPY --from=builder /build/extracted/application/ ./
# 设置权限
RUN chown -R appuser:appgroup /app
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=10s --timeout=3s --start-period=30s --retries=3 \
CMD wget -q --spider http://localhost:8080/actuator/health || exit 1
# JVM参数
ENV JAVA_OPTS="-XX:+UseG1GC \
-XX:MaxRAMPercentage=75.0 \
-XX:+UseStringDeduplication \
-Djava.security.egd=file:/dev/./urandom \
-Duser.timezone=Asia/Shanghai"
ENTRYPOINT ["sh", "-c", "java $JAVA_OPTS org.springframework.boot.loader.JarLauncher"]
优化要点:
- 使用JRE而非JDK(减少200MB+)
- 使用Alpine基础镜像(更小)
- 多阶段构建(构建工具不进入最终镜像)
- Spring Boot分层(依赖和应用分层,利用缓存)
- 非root用户运行(安全)
2.2 镜像大小对比
┌────────────────────────────────────────────────────────────────┐
│ 镜像大小对比 │
│ │
│ openjdk:17 → 670MB │
│ eclipse-temurin:17-jre → 450MB │
│ eclipse-temurin:17-jre-alpine → 180MB ✅ 推荐 │
│ │
│ 未优化的Spring Boot镜像 → 800MB │
│ 多阶段构建后 → 200MB │
│ 多阶段+分层构建 → 150MB ✅ 推荐 │
│ │
└────────────────────────────────────────────────────────────────┘
2.3 构建缓存优化
dockerfile
# ❌ 错误示例:每次都重新构建
FROM maven:3.8-openjdk-17
WORKDIR /app
COPY . .
RUN mvn package -DskipTests -B
# ✅ 正确示例:利用缓存
FROM maven:3.8-openjdk-17 AS builder
WORKDIR /build
# 1. 先复制pom.xml,下载依赖(缓存层)
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 2. 再复制源码,编译(只有源码变化才重新编译)
COPY src ./src
RUN mvn package -DskipTests -B
Docker缓存原则:
- 从上到下依次执行
- 某层变化,后面所有层重新构建
COPY . .会让所有变化都触发重建- 把不常变化的放前面,常变化的放后面
2.4 .dockerignore
dockertext
# .dockerignore - 忽略不需要的文件
.git
.gitignore
.idea
*.iml
target/
!target/*.jar
*.log
*.tmp
.env
docker-compose.yml
Dockerfile
README.md
三、安全加固
3.1 非root用户运行
dockerfile
# 创建专用用户
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
# 设置文件权限
COPY --chown=appuser:appgroup app.jar /app/app.jar
# 切换用户
USER appuser
# 后续命令都以appuser身份运行
3.2 镜像安全扫描
bash
# 使用Trivy扫描漏洞
trivy image my-registry/order-service:v1.0.0
# 输出示例
2023-10-15T10:00:00.000Z INFO Detected OS: alpine
2023-10-15T10:00:00.000Z INFO Number of language-specific files: 1
2023-10-15T10:00:00.000Z INFO Detecting jar vulnerabilities...
order-service (alpine 3.18.0)
================================
Total: 2 (UNKNOWN: 0, LOW: 0, MEDIUM: 1, HIGH: 1, CRITICAL: 0)
┌──────────────┬────────────────┬──────────┬─────────────────────┐
│ Library │ Vulnerability │ Severity │ Installed Version │
├──────────────┼────────────────┼──────────┼─────────────────────┤
│ openssl │ CVE-2023-XXXX │ HIGH │ 3.1.0-r0 │
└──────────────┴────────────────┴──────────┴─────────────────────┘
# CI/CD集成 - 高危漏洞阻止构建
trivy image --exit-code 1 --severity HIGH,CRITICAL my-registry/order-service:v1.0.0
3.3 最小权限原则
dockerfile
# 只安装必要的包
RUN apk add --no-cache \
curl \
tzdata
# 删除不必要的文件
RUN rm -rf /var/cache/apk/* /tmp/*
# 设置只读文件系统(K8s)
# securityContext:
# readOnlyRootFilesystem: true
四、Docker Compose本地开发
4.1 完整开发环境
yaml
# docker-compose.yml
version: '3.8'
services:
# 应用服务
order-service:
build:
context: .
dockerfile: Dockerfile
target: development # 使用开发阶段
ports:
- "8080:8080"
- "5005:5005" # 远程调试端口
environment:
SPRING_PROFILES_ACTIVE: dev
MYSQL_HOST: mysql
REDIS_HOST: redis
NACOS_ADDR: nacos:8848
JAVA_OPTS: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005
volumes:
- ./src:/app/src # 热重载
depends_on:
mysql:
condition: service_healthy
redis:
condition: service_started
nacos:
condition: service_started
networks:
- app-network
# MySQL
mysql:
image: mysql:8.0
environment:
MYSQL_ROOT_PASSWORD: root123
MYSQL_DATABASE: order_db
TZ: Asia/Shanghai
ports:
- "3306:3306"
volumes:
- mysql-data:/var/lib/mysql
- ./init.sql:/docker-entrypoint-initdb.d/init.sql
command: --default-authentication-plugin=mysql_native_password --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-h", "localhost"]
interval: 10s
timeout: 5s
retries: 5
networks:
- app-network
# Redis
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis-data:/data
command: redis-server --appendonly yes
networks:
- app-network
# Nacos
nacos:
image: nacos/nacos-server:v2.2.3
environment:
MODE: standalone
SPRING_DATASOURCE_PLATFORM: mysql
MYSQL_SERVICE_HOST: mysql
MYSQL_SERVICE_PORT: 3306
MYSQL_SERVICE_DB_NAME: nacos
MYSQL_SERVICE_USER: root
MYSQL_SERVICE_PASSWORD: root123
ports:
- "8848:8848"
depends_on:
mysql:
condition: service_healthy
networks:
- app-network
# Prometheus(可选)
prometheus:
image: prom/prometheus:v2.45.0
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
networks:
- app-network
# Grafana(可选)
grafana:
image: grafana/grafana:10.0.0
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_PASSWORD: admin
volumes:
- grafana-data:/var/lib/grafana
networks:
- app-network
networks:
app-network:
driver: bridge
volumes:
mysql-data:
redis-data:
grafana-data:
4.2 常用命令
bash
# 启动所有服务
docker-compose up -d
# 查看日志
docker-compose logs -f order-service
# 重建并启动
docker-compose up -d --build order-service
# 进入容器
docker-compose exec order-service sh
# 停止并清理
docker-compose down -v
# 查看服务状态
docker-compose ps
五、踩坑实录
坑1:时区问题
问题:容器内时区是UTC,日志时间差8小时。
踩坑场景 :
排查问题时,看日志时间是上午10点,但实际是下午6点,差点误判。
解决方案:
dockerfile
# 方案1:设置环境变量
ENV TZ=Asia/Shanghai
# 方案2:安装时区包并设置
RUN apk add --no-cache tzdata && \
cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \
echo "Asia/Shanghai" > /etc/timezone
坑2:OOM Killed
问题:容器内存限制2GB,但JVM默认堆大小超过了限制。
踩坑场景 :
容器内存限制2GB,JVM默认堆大小是物理内存的1/4。在16GB内存的机器上,堆大小变成4GB,超出容器限制,被OOMKilled。
解决方案:
dockerfile
# 使用容器感知的JVM参数
ENV JAVA_OPTS="-XX:MaxRAMPercentage=75.0 -XX:InitialRAMPercentage=50.0"
# 这样JVM会根据容器内存限制自动计算堆大小
# 容器内存2GB → 堆大小 = 2GB × 75% = 1.5GB
坑3:日志文件过大
问题:日志写在容器内,磁盘被撑满。
踩坑场景 :
应用写日志到/app/logs/,容器重启后日志还在积累,最终撑满磁盘。
解决方案:
yaml
# 方案1:日志输出到stdout(推荐)
# logging:
# driver: "json-file"
# options:
# max-size: "10m"
# max-file: "3"
# 方案2:挂载日志目录
volumes:
- ./logs:/app/logs
# 方案3:使用日志采集器(生产推荐)
# 应用输出到stdout,由Fluentd/Filebeat收集
坑4:镜像标签用latest
问题:每次部署都拉latest,版本不可控。
踩坑场景 :
我们用latest标签,某天误推了一个有bug的版本,所有环境都受影响,无法回滚到之前的版本。
解决方案:
bash
# 使用具体版本号
docker build -t my-registry/order-service:v1.2.3 .
# 或使用Git Commit SHA
docker build -t my-registry/order-service:abc1234 .
# 禁用latest
# 在CI/CD中强制检查
if [[ "$IMAGE_TAG" == "latest" ]]; then
echo "Error: Using 'latest' tag is not allowed!"
exit 1
fi
坑5:构建缓存失效
问题:每次构建都从零开始,耗时很长。
踩坑场景 :
每次docker build都要重新下载所有依赖,构建时间10分钟起步。
解决方案:
dockerfile
# 利用Docker层缓存
# 1. 先复制pom.xml,下载依赖
COPY pom.xml .
RUN mvn dependency:go-offline -B
# 2. 再复制源码
COPY src ./src
RUN mvn package -DskipTests -B
# 或使用BuildKit缓存
# DOCKER_BUILDKIT=1 docker build --cache-from my-registry/order-service:cache .
坑6:网络问题
问题:容器间无法通信。
踩坑场景 :
order-service调用user-service,报"Connection refused",但两个容器都在运行。
解决方案:
yaml
# 确保在同一网络
networks:
- app-network
# 使用服务名访问
# order-service → http://user-service:8080
# 检查网络
docker network ls
docker network inspect app-network
六、最佳实践清单
Docker最佳实践检查清单:
□ 镜像优化
□ 使用多阶段构建
□ 选择合适的基础镜像(Alpine)
□ 使用.dockerignore
□ Spring Boot分层构建
□ 镜像大小 < 200MB
□ 安全加固
□ 非root用户运行
□ 定期扫描漏洞
□ 使用可信基础镜像
□ 不在镜像中存储敏感信息
□ 配置管理
□ 使用具体版本标签
□ 环境变量配置
□ 配置文件外挂
□ 运行优化
□ 健康检查配置
□ 资源限制设置
□ 日志输出到stdout
□ 优雅关闭处理
□ 开发环境
□ Docker Compose一键启动
□ 热重载支持
□ 远程调试支持
七、血的教训
容器化不是打包就行,要考虑安全、性能、可维护性。
我们踩过的坑:
- 镜像过大:发布慢,存储成本高
- 权限过高:安全隐患
- 版本混乱:无法回滚
- 日志丢失:排查困难
记住: Docker只是工具,用好工具需要理解原理。
八、思考题
- 你的Docker镜像优化到多大了?
- 你有什么Docker最佳实践可以分享?
- 你遇到过哪些Docker的坑?
个人观点,仅供参考