devops系列(三) Docker 容器化入门:告别“在我电脑上能跑“

Docker 容器化入门:告别"在我电脑上能跑"

一、那个让全公司程序员血压飙升的瞬间

你有没有经历过这种场景?

周五下午五点,你正美滋滋地收拾东西准备下班,测试小哥突然在群里@你:“哥,你昨天提测的功能又挂了。”

你一脸懵:“不可能啊,我本地跑得好好的!”

十分钟后,你蹲在测试机前面排查,发现是 JDK 版本不对——你本地用的是 JDK 17,测试环境还是 JDK 8。改完 JDK,又发现某个系统环境变量没配。好不容易环境变量搞定了,依赖库的版本又对不上……

等全部修完,窗外天都黑了,晚饭变夜宵。

说白了,这就是经典的"在我电脑上能跑"综合症。环境不一致,就像你搬家时只带了家具没带螺丝,到了新家发现怎么装都装不上。

那有没有一种办法,能把"我家"原封不动地搬到"测试家"、"生产家"呢?

有,这就是 Docker。


二、Docker 到底是啥?用"搬家"给你讲明白

我第一次接触 Docker 的时候,也被一堆概念绕晕了:镜像、容器、仓库……听着就头大。

后来我想了个办法,用"搬家"来类比,一下子就通透了。

镜像(Image)= 搬家前的打包清单

你要搬家,得先把家里的东西一样一样打包好,列个清单。这个清单加上打包好的箱子,就是镜像。它包含了你的应用代码、运行环境、依赖库、系统配置,一应俱全。

容器(Container)= 搬进去住的房子

镜像本身不会跑,就像打包好的箱子不会自己打开。你得找个房子,把箱子拆开,家具摆好,才能真正住进去。这个"住进去"的状态,就是容器。你可以用同一个镜像,在不同的服务器上启动无数个容器。

仓库(Registry)= 搬家公司/仓库

你打包好的东西,可以存到仓库里。下次要搬到别的地方,直接从仓库拉过来就行。Docker Hub 就是最大的公共仓库,你们公司也可以搭自己的私有仓库。

还有一个更形象的比喻是俄罗斯套娃:镜像是最外面那个完整的套娃,容器是你打开后里面那一层正在运行的状态。每个容器都是独立的,互不干扰。


三、手写 Dockerfile:从"能跑"到"跑得优雅"

光理解概念不够,咱们来动手写一个 Dockerfile。

假设你有一个 Spring Boot 项目,用 Maven 构建,打包后是个 JAR 文件。最朴素的写法可能是这样的:

# 阶段一:直接基于一个带 JDK 的完整系统镜像
FROM ubuntu:22.04

# 安装 JDK 和 Maven
RUN apt-get update && apt-get install -y openjdk-17-jdk maven

# 把代码拷进去
COPY . /app
WORKDIR /app

# 编译打包
RUN mvn clean package -DskipTests

# 运行
CMD ["java", "-jar", "target/myapp.jar"]

这段 Dockerfile 能跑吗?能。但问题大了去了:

  1. 镜像体积爆炸:基于 Ubuntu,还装了 JDK 和 Maven,动辄 1GB 起步。
  2. 构建效率低:每次改一行代码,Maven 依赖都要重新下载。
  3. 安全风险高:带着完整的编译工具跑到生产环境,没必要。

优化思路:分层构建 + 多阶段构建

Docker 的构建是分层缓存的。咱们要做的就是:让变化少的层在前面,变化多的层在后面,这样改动代码时就能复用缓存。

另外,多阶段构建的意思是:编译用一个镜像,运行用另一个更小的镜像,最后只把 JAR 文件带过去。

优化后的 Dockerfile 长这样:

# ========== 阶段一:编译 ==========
FROM maven:3.9-eclipse-temurin-17-alpine AS builder

WORKDIR /app

# 关键:先把 pom.xml 拷进去,单独下载依赖
# 这样只要 pom.xml 不变,这层缓存就一直复用
COPY pom.xml .
RUN mvn dependency:go-offline

# 再把源码拷进去编译
COPY src ./src
RUN mvn clean package -DskipTests

# ========== 阶段二:运行 ==========
FROM eclipse-temurin:17-jre-alpine

WORKDIR /app

# 只把编译好的 JAR 从 builder 阶段拷过来
COPY --from=builder /app/target/*.jar app.jar

# 暴露端口
EXPOSE 8080

# 运行
ENTRYPOINT ["java", "-jar", "app.jar"]

关键点在哪?

  • maven:3.9-eclipse-temurin-17-alpineeclipse-temurin:17-jre-alpine 都是 Alpine 版本,体积比 Ubuntu 小得多。
  • 先拷 pom.xml 单独下载依赖,利用了 Docker 分层缓存,后续改源码不用重新下依赖。
  • --from=builder 实现了多阶段构建,最终镜像只包含 JRE 和 JAR,没有 Maven、没有源码,干净清爽。

效果对比(我实际测的数据):

方案镜像体积二次构建时间
单阶段 Ubuntu 方案~1.2GB3-5 分钟
多阶段 Alpine 方案~180MB20-30 秒

这差距,相当于从绿皮火车升级到了高铁。


四、Docker Compose:一键启动整个"小区"

单个容器搞定了,但真实项目很少只有一个服务。你的 Web 应用可能要连 Redis、MySQL、Nginx,手动一个个 docker run 太麻烦了。

Docker Compose 就是来解决这个问题的。你可以把它理解成小区物业管家:一份配置文件,一键启动整个小区的所有住户。

下面这份 docker-compose.yml,可以直接拿去用:

version: "3.8"

services:
  # ===== Web 应用 =====
  app:
    build: .
    container_name: my-app
    ports:
      - "8080:8080"
    environment:
      - SPRING_DATASOURCE_URL=jdbc:mysql://mysql:3306/mydb
      - SPRING_REDIS_HOST=redis
    depends_on:
      - mysql
      - redis
    networks:
      - my-network

  # ===== MySQL =====
  mysql:
    image: mysql:8.0
    container_name: my-mysql
    environment:
      MYSQL_ROOT_PASSWORD: root123
      MYSQL_DATABASE: mydb
    volumes:
      - mysql_data:/var/lib/mysql
    ports:
      - "3306:3306"
    networks:
      - my-network

  # ===== Redis =====
  redis:
    image: redis:7-alpine
    container_name: my-redis
    volumes:
      - redis_data:/data
    ports:
      - "6379:6379"
    networks:
      - my-network

# 数据卷:容器删了,数据还在
volumes:
  mysql_data:
  redis_data:

# 自定义网络:容器之间可以通过服务名互相访问
networks:
  my-network:
    driver: bridge

几个重点解释一下:

  • build: . 表示在当前目录找 Dockerfile 构建应用镜像。
  • depends_on 只是控制启动顺序,不保证服务已就绪。生产环境建议加健康检查。
  • volumes 挂载数据卷,这是血泪教训——不挂 volume,容器一删,MySQL 数据全没了。
  • networks 让所有容器在同一个自定义网络里,它们可以通过服务名(如 mysqlredis)直接互相访问,不用记 IP。

启动命令就一行:

docker-compose up -d

停止也是一行:

docker-compose down

要连数据一起清掉:

docker-compose down -v

五、几个不得不说的踩坑记录

Docker 用起来爽,但坑也不少。我挑两个印象最深的分享给你。

坑一:容器里时区不对,日志时间差了 8 小时

有一次排查线上问题,看日志发现时间全是 UTC,跟北京时间差了 8 小时。找半天才发现是 Alpine 镜像默认没有时区数据。

解决办法,在 Dockerfile 里加上:

# Alpine 镜像需要手动安装时区数据
RUN apk add --no-cache tzdata
ENV TZ=Asia/Shanghai

如果是非 Alpine 镜像,可以直接挂载宿主机的时区文件:

ENV TZ=Asia/Shanghai
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone

坑二:数据没挂 volume,容器删了数据没了

这个坑我踩过两次,刻骨铭心。

第一次是本地搭测试环境,MySQL 容器跑得好好的,我手贱执行了 docker system prune -a,把所有没挂 volume 的容器数据一起清了。测试数据全没,哭都没地方哭。

第二次是生产环境迁移,同事直接 docker run 起了个新容器,没挂 volume。结果服务器重启后容器自动重建,数据库直接归零。

核心原则:有状态服务必须挂 volume。 MySQL、Redis、Elasticsearch,但凡存数据的,volume 就是你的救命稻草。


六、容器调试:几个常用的小技巧

容器跑起来了,但出问题怎么排查?分享几个我常用的命令:

# 看容器日志
docker logs -f 容器名

# 进容器内部看看
docker exec -it 容器名 /bin/sh

# 如果是基于 Ubuntu/Debian 的镜像,可以用 bash
docker exec -it 容器名 /bin/bash

# 查看容器资源占用
docker stats

# 查看容器网络信息
docker inspect 容器名 | grep IPAddress

进容器后,你可以像操作普通 Linux 一样排查:看进程、看文件、ping 其他容器。不过要注意,很多精简镜像(比如 Alpine、distroless)连 curlping 都没有,需要提前安装或者换镜像调试。


七、总结:Docker 到底改变了什么?

写到这里,咱们来回顾一下:

  • 问题:环境不一致导致"在我电脑上能跑"成了程序员噩梦。
  • 方案:Docker 用镜像把应用和环境一起打包,做到"一次构建,到处运行"。
  • 实现:通过合理的 Dockerfile 分层和多阶段构建,把镜像体积从 GB 级压到 MB 级;用 Docker Compose 一键管理多容器环境。
  • 验证:镜像体积和构建效率大幅提升,环境一致性得到保障。

Docker 不是什么高深莫测的黑科技,它本质上就是一个更优雅的打包和搬家工具。理解了这一点,你会发现它其实很好上手。

当然,Docker 只是容器化的起点。再往后还有 Kubernetes、Service Mesh、CI/CD 流水线……路还长着呢。


八、聊聊你的经历

你在工作中遇到过"在我电脑上能跑"的崩溃时刻吗?是用 Docker 解决的,还是踩过别的坑?

关于 Dockerfile 优化、Docker Compose 使用,或者容器化迁移,你有什么独门技巧?欢迎在评论区交流,咱们一起进步!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值