【Kubernetes从入门到精通】第02篇:容器到底是个啥——Docker快速入门
2026/7/29 16:44:55 网站建设 项目流程

上一篇【第01篇】K8s凭什么称霸容器编排——从Borg到Kubernetes的封神之路
下一篇【第03篇】手把手搭建K8s学习环境——minikube/kind/k3s三选一


摘要

“我的机器上能跑啊!”——这句话大概是程序员圈流传最广的甩锅金句了。测试说"这有个bug",你回一句"我本地没问题啊",两边面面相觑,最后发现是环境差异——你的 Java 15,生产是 Java 11;你的 Python 3.9,他装的是 3.7;你的 npm 装了某个全局依赖,他那没有……这些破事儿放到 Docker 面前就是一句话:“把环境连同应用一起打包,拿着到处跑。

本文带你搞懂容器到底是个什么玩意儿:它跟虚拟机有什么区别?Docker 三要素(镜像、容器、仓库)各自是干嘛的?怎么自己写 Dockerfile 把应用装进去?镜像的分层结构为什么像积木一样堆起来?读完这篇,你就能动手把自己的项目容器化——从此告别"环境不一致"的鬼打墙。


一、虚拟机 vs 容器——一栋楼 vs 一套房

要理解容器,必须先搞清楚它跟虚拟机的区别。很多人第一次接触 Docker 时会想:“这不就是个轻量级虚拟机吗?”——大错特错。这俩的差别,用住房来比喻最合适:

【虚拟机 vs 容器:住房视角】 虚拟机(VM): ┌─────────────────────────────────────┐ │ Hypervisor │ │ ┌──────────┐ ┌──────────┐ │ │ │ App A │ │ App B │ │ │ │ ┌─────┐ │ │ ┌─────┐ │ │ │ │ │库/依赖│ │ │ │库/依赖│ │ │ │ │ └─────┘ │ │ └─────┘ │ │ │ │ ┌───────┐│ │ ┌───────┐│ │ │ │ │Guest OS││ │ │Guest OS││ │ │ │ │(Ubuntu)││ │ │(CentOS)││ │ │ │ └───────┘│ │ └───────┘│ │ │ └──────────┘ └──────────┘ │ └─────────────────────────────────────┘ 每户 = 一栋独立别墅(独占OS内核、内存、磁盘) 搬家 = 搬整栋房子,费时费力 占地 = 每户几GB起步 容器(Docker): ┌─────────────────────────────────────┐ │ Docker Engine │ │ ┌──────────┐ ┌──────────┐ │ │ │ App A │ │ App B │ │ │ │ ┌─────┐ │ │ ┌─────┐ │ │ │ │ │库/依赖│ │ │ │库/依赖│ │ │ │ │ └─────┘ │ │ └─────┘ │ │ │ └──────────┘ └──────────┘ │ │ 共享 Host OS 内核 │ └─────────────────────────────────────┘ 每户 = 同栋楼里的一套公寓(共享大楼水电) 搬家 = 拎包入住,几秒钟搞定 占地 = 每户几十MB到几百MB

要点:虚拟机给每个应用分配一整套独立操作系统(Guest OS),而容器直接共享宿主机的操作系统内核,只在应用层面做隔离。这就解释了为什么容器启动是秒级,VM 是分钟级;容器镜像才几十 MB,VM 镜像动不动几个 GB。

用更直白的话说:虚拟机是在你的物理机上模拟出一台完整的电脑,容器只是在你的操作系统里隔出几个小格子。虚拟机隔出来的每个格子都有独立的地基和水电;容器隔出来的格子只隔墙,地基和水电是共用的。

两者的对比一览:

对比维度虚拟机(VM)容器(Container)
隔离层级硬件级虚拟化进程级隔离(namespace + cgroup)
操作系统每个VM有独立Guest OS共享Host OS内核
启动速度分钟级秒级(甚至毫秒级)
镜像体积几GB(含完整OS)几十MB到几百MB(只含应用+依赖)
资源利用率低(每个VM预留资源)高(按需分配,动态调整)
移植性依赖HypervisorDocker镜像到处跑
安全性强(硬件隔离)较VM弱(内核共享)
适合场景需要强隔离、不同OS、运行非Linux应用微服务、快速迭代、一致环境

要点:容器不是"更好的虚拟机",它是解决不同问题的工具。如果你需要跑 Windows + Linux 两个 OS 上的应用,那当然还是得用虚拟机;但如果你需要的是"让同一个 Linux 应用在开发/测试/生产三个环境表现一致",容器就是最优解,没有之一。

我见过一个公司,生产环境是 CentOS 7,开发用 macOS,测试用 Ubuntu 18.04。每天都有新的"环境不一致"事故——ImageMagick 版本不对、glibc 版本不一致、locale 设置不一样……他们搞了一年以后全面容器化,从此这类 bug 归零。老板开心得请全组吃了海底捞。


二、Docker三要素——镜像、容器、仓库

Docker 体系有三个核心概念,搞懂了它们,Docker 的基本框架就立起来了。用编程的类比最容易记:

【Docker 三要素 = 编程三件套】 编程世界 Docker 世界 ┌───────────┐ ┌───────────┐ │ 类 │ ←→ │ 镜像 │ │ (Class) │ │ (Image) │ └─────┬─────┘ └─────┬─────┘ │ new │ docker run ▼ ▼ ┌───────────┐ ┌───────────┐ │ 对象 │ ←→ │ 容器 │ │ (Object) │ │(Container)│ └───────────┘ └───────────┘ 存在内存中 存在磁盘上 一个类可以 new 出多个对象 → 一个镜像可以 run 出多个容器,互不影响 ┌───────────┐ ┌───────────┐ │ GitHub │ ←→ │ Registry │ │ (代码仓库) │ │ (镜像仓库) │ └───────────┘ └───────────┘ push/pull 代码 push/pull 镜像

镜像(Image)——应用的"快照模板"

镜像是一个只读的静态模板,包含了运行应用所需的一切:代码、运行时、系统库、环境变量、配置文件。你可以把镜像理解成盖房子的设计图纸——它规定了这栋房子的全部细节,但本身不是住人的地方。

# 拉取一个 nginx 镜像dockerpull nginx:1.25# 查看本地已有的镜像dockerimages# REPOSITORY TAG IMAGE ID CREATED SIZE# nginx 1.25 abc123def456 2 weeks ago 187MB# 查看镜像的层结构dockerhistorynginx:1.25

Docker 官方仓库 Docker Hub 上有数百万个公开镜像,几乎所有主流软件都有官方镜像——nginx、redis、mysql、postgres、python、node……你几乎不需要从零开始。

容器(Container)——镜像的运行实例

容器是基于镜像创建的运行中的进程实例。就像根据同一份设计图纸可以建出完全一样的多栋房子,基于同一个 nginx 镜像可以跑出多个 nginx 容器——它们共享同一个镜像文件,但各自的文件系统、网络、进程空间完全独立。

# 用 nginx 镜像启动一个容器dockerrun-d--namemy-nginx-p8080:80 nginx:1.25# 查看运行中的容器dockerps# CONTAINER ID IMAGE COMMAND STATUS PORTS# f8a3b2c1d4e5 nginx:1.25 "/docker-entrypoint.…" Up 2 minutes 0.0.0.0:8080->80/tcp# 进入容器内部(交互式 shell)dockerexec-itmy-nginxbash# 停止和删除容器dockerstop my-nginxdockerrmmy-nginx

启动一个容器是秒级的事情,容器停止后它的文件系统变更就丢失了(除非你使用了数据卷)——这就是"容器的无状态性",也是容器设计哲学的核心。

要点:容器本身的文件系统是临时的——容器删了,里面新写的文件也没了。需要持久化数据,必须用 Volume(数据卷)把宿主机的目录挂进去。很多初学者在这栽跟头:跑了个 MySQL 容器,数据存容器里,容器一删数据库没了,当场石化。

仓库(Registry)——镜像的 GitHub

仓库就是集中存储和分发镜像的地方。最大的公共仓库是 Docker Hub,国内常用的还有阿里云容器镜像服务、腾讯云 TCR、华为云 SWR。公司内部一般会搭建私有仓库(Harbor 是最常用的开源方案)。

# 给镜像打标签(格式:仓库地址/命名空间/镜像名:版本)dockertag nginx:1.25 myregistry.com/myapp/nginx:1.25# 推送到私有仓库dockerpush myregistry.com/myapp/nginx:1.25# 从私有仓库拉取dockerpull myregistry.com/myapp/nginx:1.25

docker pull默认去 Docker Hub 拉,docker push也默认推 Docker Hub。加个前缀就可以指向私有仓库,就这么简单。


三、Dockerfile实战——把你的Java应用装进容器

光看概念是学不会的,必须动手。下面我们就来写一个实战的 Dockerfile,把一个 Spring Boot Java 应用打包进容器。

场景描述

假设你有一个 Spring Boot 项目,打包成了app.jar,需要 Java 17 来运行。传统的部署方式是一台服务器一台服务器地去配 Java 环境、上传 jar 包、写启动脚本……一旦换服务器或者集群扩容,噩梦就开始了。容器化以后,这些全部写在一个 Dockerfile 里搞定。

Dockerfile 编写

# 第一阶段:构建阶段(多阶段构建) FROM maven:3.9-eclipse-temurin-17 AS builder # 设置工作目录 WORKDIR /workspace # 先把 pom.xml 单独复制进来(利用 Docker 缓存层) COPY pom.xml . # 下载依赖(这层会被缓存,除非 pom.xml 变化) RUN mvn dependency:go-offline -B # 复制源码并构建 COPY src ./src RUN mvn package -DskipTests -B # 第二阶段:运行阶段(最终镜像只保留 JRE 和 jar) FROM eclipse-temurin:17-jre-alpine # 元数据 LABEL maintainer="your-name@example.com" LABEL description="My Spring Boot Application" # 创建非 root 用户(安全最佳实践) RUN addgroup -S appgroup && adduser -S appuser -G appgroup WORKDIR /app # 从构建阶段复制 jar 包 COPY --from=builder /workspace/target/*.jar app.jar # 切换为非 root 用户 USER appuser # 暴露端口(告知容器需要监听哪个端口,但不会自动映射) EXPOSE 8080 # 健康检查 HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ CMD wget --quiet --spider http://localhost:8080/actuator/health || exit 1 # 容器启动命令 ENTRYPOINT ["java", "-jar", "app.jar"]

命令详解

指令作用注意事项
FROM指定基础镜像必须第一条指令。用-alpine版本更小
WORKDIR设置工作目录后续 RUN/CMD/COPY 都在此目录下执行
COPY从宿主机复制文件到镜像推荐用 COPY 而非 ADD(ADD 有隐式行为)
RUN在构建时执行命令每一条 RUN 会生成一个新的镜像层
LABEL添加元数据用来标注维护者、版本等信息
USER切换到指定用户运行不用 root 跑容器是基本的安全素养
EXPOSE声明容器监听端口仅是文档声明,实际映射用 -p 参数
HEALTHCHECK健康检查K8s 也能用这个来判断容器是否健康
ENTRYPOINT容器启动命令与 CMD 的区别:ENTRYPOINT 是固定命令,CMD 是默认参数

要点:多阶段构建(Multi-stage Build)是 Dockerfile 的最佳实践之一。第一阶段用完整的 Maven/Gradle 环境编译代码,第二阶段只复制编译好的产物到精简的 JRE 镜像中。最终镜像不包含 JDK、源码,体积从可能 500MB+ 缩小到 100MB 以内。

构建和运行

# 构建镜像(-t 指定标签名)dockerbuild-tmyapp:1.0.# 运行容器dockerrun-d\--namemyapp\-p8080:8080\-eSPRING_PROFILE=prod\-v/host/logs:/app/logs\myapp:1.0# 查看日志dockerlogs-fmyapp# 确认容器状态dockerps|grepmyapp# 验证服务正常curlhttp://localhost:8080/actuator/health

看到{"status":"UP"}的那一刻,你的第一个容器化应用就跑起来了。成就感拉满。


四、分层镜像原理——像搬家一样打包

Docker 镜像最精妙的设计就是分层结构。这玩意儿第一次听可能有点绕,但用"搬家"来比喻,三分钟就能通透。

搬家比喻

假设你从北京搬到上海:

  • 传统方式:把所有家当塞进一辆卡车,一次性运过去。下次就算只是买了把新椅子,也得整辆卡车重新跑一趟——所有的家具重新搬一遍。
  • Docker 的方式:把家当按类别分成箱子——衣服一箱、书一箱、厨具一箱、电器一箱。搬家时只运那些有变化的箱子。
【镜像分层结构:搬家版本】 镜像标签: myapp:2.0 镜像标签: myapp:2.1 ┌─────────────────┐ ┌─────────────────┐ │ 第5层:新jar包 │ ◄─ 变了 │ 第5层:新jar包 │ ← 只需要传/存这一层 ├─────────────────┤ ├─────────────────┤ │ 第4层:配置文件 │ │ 第4层:配置文件 │ ← 没变,复用 ├─────────────────┤ ├─────────────────┤ │ 第3层:依赖lib │ │ 第3层:依赖lib │ ← 没变,复用 ├─────────────────┤ ├─────────────────┤ │第2层:JDK运行时 │ │第2层:JDK运行时 │ ← 没变,复用 ├─────────────────┤ ├─────────────────┤ │ 第1层:Alpine OS │ │ 第1层:Alpine OS │ ← 没变,复用 └─────────────────┘ └─────────────────┘ 总大小:第1~5层之和 实际占用:只有第5层差异!

技术原理

【Docker 镜像分层技术架构】 ┌─────────────────────────────────────────────┐ │ 镜像 (Image) │ │ │ │ ┌───────────────────────────────┐ │ │ │ Layer 5: 应用 jar 包 │ R/W │ │ ├───────────────────────────────┤ (可写层) │ │ │ Layer 4: 依赖 lib │ ┐ │ │ ├───────────────────────────────┤ │ │ │ │ Layer 3: JDK 运行时 │ │ 只读 │ │ ├───────────────────────────────┤ │ 层 │ │ │ Layer 2: 系统工具 (curl/vim) │ │ │ │ ├───────────────────────────────┤ │ │ │ │ Layer 1: Alpine Linux 基础 │ ┘ │ │ └───────────────────────────────┘ │ │ │ │ ▲ 每层都是一个独立的文件系统快照 │ │ ▲ 下层为只读,最上层(容器层)可写 │ │ ▲ 多个镜像可以共享相同的底层(存储复用) │ └─────────────────────────────────────────────┘ 容器层 (Container Layer) = 最顶层的可写层 ┌───────────────────────────────────────┐ │ 你在容器里创建/修改的文件都写在这层 │ │ 删除容器 → 这一层就没了 │ │ 基础镜像的层不受任何影响 │ └───────────────────────────────────────┘

每一层对应 Dockerfile 中的一条指令——每个RUNCOPYADD都会创建一个新层。层与层之间是叠加关系(Union File System),上层文件会覆盖下层同名文件。

要点:这就是为什么 Dockerfile 的指令顺序很重要——把不常变的放在前面(比如先 COPY pom.xml 再 RUN mvn install),常变的放在后面(比如 COPY 源代码)。充分利用缓存,构建速度能快好几倍。

镜像分层带来了三大好处:

  1. 存储复用:你本机有 10 个 Java 应用的镜像,它们都基于eclipse-temurin:17-jre,那么这层 JDK 只需要存一份。10 个镜像共享同一个 JDK 层,省下几个 GB 毫无压力。
  2. 加速分发:更新镜像时只需要推送变化的那几层,其它层直接从缓存读取。一个 500MB 的镜像,如果你只改了 jar 包(假设 50MB),实际推送的只有 50MB。
  3. 构建缓存:构建时如果某一层和上次完全一样,Docker 就直接用缓存,跳过执行。这在你频繁构建的开发阶段节省了大量时间。

要点:COPY 指令的缓存判断依据是文件内容的哈希值。如果你的源代码文件内容变了,COPY 那一层就会失效,后续所有层也会跟着重建。所以把依赖安装(npm install/pip install/mvn dependency)放在 COPY 源码之前,依赖没变就能命中缓存。


五、Docker 常用命令全家桶——一招制敌

Docker 命令看着多,归类整理后就六大类:镜像相关、容器相关、日志排查、网络、数据卷、清理。下面给出最常用的一套:

镜像管理

# 搜索镜像dockersearch nginx# 拉取镜像dockerpull nginx:1.25-alpine# alpine版更小(~40MB)# 列出本地镜像dockerimagesdockerimages|grepnginx# 查看镜像详情(层结构、环境变量、端口等)dockerinspect nginx:1.25-alpine# 查看镜像构建历史dockerhistorynginx:1.25-alpine# 删除镜像(必须先删除引用它的容器)dockerrmi nginx:1.25-alpine# 清理无用镜像(dangling images)dockerimage prune

容器管理

# 创建并启动容器dockerrun-d--nameweb-p8080:80 nginx:alpine# 查看运行中的容器dockerps# 只看运行中的dockerps-a# 包括已停止的# 启动/停止/重启容器dockerstart webdockerstop webdockerrestart web# 删除容器dockerrmweb# 必须已停止dockerrm-fweb# 强制删除(先stop再rm)# 容器内执行命令dockerexec-itwebbash# 进入交互式shelldockerexecwebls/etc/nginx# 执行单条命令# 查看容器资源使用dockerstats web# 实时CPU/内存/网络# 查看容器详情dockerinspect web# 大量JSON信息

日志和调试

# 查看日志dockerlogs web# 所有日志dockerlogs-fweb# 实时跟踪(Ctrl+C退出)dockerlogs--tail100web# 最近100行dockerlogs--since5m web# 最近5分钟的日志# 查看容器内的进程dockertopweb# 查看端口映射dockerport web# 复制文件(宿主机↔容器)dockercpweb:/etc/nginx/nginx.conf ./nginx.confdockercp./index.html web:/usr/share/nginx/html/

网络和数据卷

# 查看网络dockernetworklsdockernetwork inspect bridge# 创建自定义网络(容器间可通过名字互相访问)dockernetwork create mynetdockerrun-d--nameweb--networkmynet nginx:alpine# 查看数据卷dockervolumels# 创建并挂载数据卷dockervolume create mydatadockerrun-d--namedb\-vmydata:/var/lib/mysql\mysql:8# 挂载宿主机目录dockerrun-d--nameweb\-v/host/path:/container/path\nginx:alpine

一键清理

# 清理所有停止的容器、无用网络、悬空镜像、悬空构建缓存dockersystem prune-a# 更彻底:包括所有未使用的数据卷dockersystem prune-a--volumes
# 也可以用 docker-compose 管理多容器应用# docker-compose.yml 示例version:'3.8'services:app:build:.ports:-"8080:8080"environment:-DB_HOST=dbdepends_on:-dbdb:image:mysql:8environment:MYSQL_ROOT_PASSWORD:secretvolumes:-db_data:/var/lib/mysqlvolumes:db_data:
# docker-compose 常用命令dockercompose up-d# 启动所有服务(后台)dockercompose down# 停止并删除所有容器dockercompose logs-f# 查看所有服务日志dockercomposeps# 查看服务状态

要点:别把所有容器都放在默认的 bridge 网络里!创建自定义网络后,容器之间可以直接用容器名互访(Docker 内置 DNS 解析)——不用再费心查 IP 了。同一个自定义网络里的web容器可以直接curl db:3306


六、容器化不只是装进去——几个实用最佳实践

你以为学会docker run就结束了吗?Naive。容器化里有些暗坑,我踩过,你就不必再踩一遍。

1. 镜像越小越好

# ❌ 错误示范 FROM ubuntu:22.04 RUN apt-get update && apt-get install -y python3 # 镜像体积:~300MB # ✅ 正确示范 FROM python:3.12-alpine # 镜像体积:~50MB

要点:能用 alpine 就别用完整的 ubuntu/debian 镜像。alpine 是一个只有 5MB 的安全 Linux 发行版,专门为容器优化。但注意 alpine 用 musl libc 而非 glibc,极少数依赖 glibc 的程序在上面可能有兼容问题(比如某些需要安装 .so 文件的 Python 包)。

2. 不要用 root 跑容器

# ❌ 默认就是 root 运行,容器一旦被攻破,攻击者拿到的是宿主机的 root FROM node:20-alpine COPY . /app CMD ["node", "/app/server.js"] # ✅ 创建非 root 用户 FROM node:20-alpine RUN addgroup -S app && adduser -S app -G app USER app COPY --chown=app:app . /app CMD ["node", "/app/server.js"]

3. .dockerignore 不能忘

.dockerignore放在 Dockerfile 同级目录,避免把node_modules.git、IDE 配置等无关文件复制进镜像:

# .dockerignore node_modules .git .idea .vscode *.log target/ Dockerfile docker-compose.yml .env

4. 一个容器一个进程

要点:一个容器只跑一个主进程。不要在容器里跑 supervisord 启动 nginx + php-fpm + cron + ……这不是容器化的正确姿势。正确的做法是每个进程一个容器,用 docker-compose 或 K8s Pod 来编排。

容器不是虚拟机——它本质上是进程的隔离环境,设计哲学就是"一个容器一个职责"。把多个进程塞进一个容器,就失去了容器化的大部分好处:独立扩缩容、独立日志、独立健康检查、独立重启策略。


本篇小结

恭喜你,Docker 的核心知识你已经掌握了:

  1. 容器 vs 虚拟机:容器共享宿主机内核,启动快、体积小、资源利用率高;虚拟机硬件隔离,安全性更强但更重。两者不是替代关系,是互补关系。
  2. Docker 三要素:镜像是静态模板(类比类的定义),容器是运行实例(类比 new 出来的对象),仓库是镜像的 GitHub。
  3. Dockerfile:多阶段构建是标配——构建阶段用完整环境,运行阶段只留必要的文件和运行时。
  4. 分层镜像:每一层是只读的叠加文件系统。层与层共享、缓存复用,这是 Docker 构建和分发速度的底层秘密。
  5. 常用命令:镜像管理、容器管理、日志、网络、数据卷五大板斧,够日常用了。

现在你已经能把任何应用容器化了。下一篇我们就搭起 K8s 学习环境——minikube、kind、k3s 三选一,选哪个?别急,下一篇给你讲得明明白白。


上一篇【第01篇】K8s凭什么称霸容器编排——从Borg到Kubernetes的封神之路
下一篇【第03篇】手把手搭建K8s学习环境——minikube/kind/k3s三选一


需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询