个人主页 我不会起名字322 欢迎各位大佬莅临其他栏目 技术栈学习笔记 其他栏目 力扣Hot100题目解析 其他栏目 Go项目学习笔记 其他栏目 redis 文章目录Docker 入门镜像、容器与 Dockerfile 一次讲透一、三个概念镜像、容器、仓库二、动手从一条命令开始三、容器的五种状态与流转两个「不是故障」的现象四、Dockerfile真正决定生产力的东西一个完整的例子指令逐个说清EXPOSE 的真相它什么也不做五、缓存与多阶段构建层缓存为什么指令顺序很重要多阶段构建把镜像从 1GB 压到 20MB六、五个真实的坑七、清理Docker 很能吃磁盘八、命令速查九、总结Docker 入门镜像、容器与 Dockerfile 一次讲透我刚开始接触 Docker 的时候一直被一个说法绕晕「容器就是一个轻量级的虚拟机」。这个比喻是错的而且会误导你很久。虚拟机会虚拟出一整套硬件然后在上面装一个完整的操作系统内核而容器根本没有自己的内核它只是宿主机上一个被特殊隔离过的进程。理解这一点后面所有设计就都顺了为什么容器启动是毫秒级而虚拟机是分钟级因为它压根没启动操作系统。为什么容器里ps aux看不到宿主机的进程因为内核给了它一套独立的命名空间。这篇文章按我实际的踩坑顺序来先把三个核心概念理清再动手然后重点讲 Dockerfile这才是真正决定你能不能把 Docker 用起来的东西。一、三个概念镜像、容器、仓库这三个词必须一次分清否则后面全是糊涂账。概念是什么类比镜像Image静态的、只读的文件层集合类class容器Container镜像的运行实例带一个可写层对象object仓库Registry存放和分发镜像的服务代码托管平台关键在镜像和容器的关系镜像是只读的一个镜像可以启动出无数个互不干扰的容器。启动容器时Docker 会在只读层之上加一个可写层——你对容器的所有修改都落在这个可写层里镜像本身一个字节都不动。这解释了一个新手常见困惑你在容器里rm -rf删了文件重新起一个容器文件又回来了。因为你的删除只写在了那一个容器的可写层里。顺带纠正一个概念污染你可能听过「镜像文件」这个词ISO 镜像、系统镜像那是磁盘副本的意思跟 Docker 镜像没有关系。Docker 镜像是分层的文件系统快照。二、动手从一条命令开始假设你已经装好了 Docker。判断它是否正常dockerversiondockerinfo第一个容器dockerrun nginx这条命令做了五件事理解这个顺序很重要在本地找nginx镜像 → 找不到就去 Docker Hub 拉 → 用镜像创建容器 → 分配可写层和网络 → 启动容器主进程。但这条命令有个问题它占着前台你的终端被卡住了。真实使用一定是# -d 后台运行-p 端口映射--name 起名dockerrun-d-p8080:80--namemy-nginx nginx-p 8080:80的冒号前后千万别记反前面是宿主机的端口后面是容器内的端口。所以访问宿主机IP:8080会转发到容器的 80 端口。看看现在有什么# 只看运行中的dockerps# 加上 -a 连已停止的也看dockerps-adocker ps的输出值得逐列看一遍它是你日后排障的主要信息来源列含义CONTAINER ID容器 ID 前 12 位够用了IMAGE来自哪个镜像STATUSUp 3 minutes/Exited (0) 2 minutes agoPORTS端口映射情况排查访问不了先看这里NAMES名字--name指定的或随机生成三、容器的五种状态与流转容器不是「开」和「关」两个状态它有五个created ──start── running ──stop── stopped │ ↑ │ pause unpause start ↓ │ │ paused ───────────────┘ rm → deleted对应命令dockercreate# 创建但不启动状态 createddockerstart# 启动已停止的容器dockerrun# 相当于 create start一步到位dockerstop# 优雅停止先发 SIGTERM超时再 SIGKILLdockerkill# 直接 SIGKILL不给程序清理机会dockerrestart# 重启dockerpause# 暂停只冻结 CPU 调度dockerunpause# 恢复dockerrm# 删除已停止的容器dockerrm-f# 强制删除运行中的容器stop和kill的区别值得单独记一下因为踩过坑docker stop发的是SIGTERM应用有机会做优雅退出——关闭连接、刷盘、清理临时文件。默认等 10 秒后若还没退出才升级成 SIGKILLdocker kill发的是SIGKILL进程直接被内核干掉没有任何清理机会生产环境一律用stop除非容器已经卡死不响应。用kill删数据库容器是有可能丢数据的。两个「不是故障」的现象1. 容器 Exit 后又自己活了只要容器启动时带了--restart策略退出后就会被拉起来dockerrun-d--restartalways nginx# 退出就重启包括宿主机重启后dockerrun-d--restarton-failure:3 nginx# 非 0 退出才重启最多 3 次dockerrun-d--restartunless-stopped nginx# 总是重启但手动 stop 过的不算on-failure后面那个数字很关键不写次数会无限重启遇到一个启动就崩的应用会变成 CPU 打满的死循环。所以我一般至少写成on-failure:3。2. 容器被 OOM 杀掉这个坑最隐蔽杀容器的不是 Docker是宿主机的内核。因为容器本质上就是宿主机上的一组进程-m限制的内存是通过 cgroups 设的。当容器内进程申请内存超过上限触发的是宿主机内核的 OOM Killer然后内核挑一个最占内存的进程干掉。dockerrun-d-m512m nginx# 限制内存 512Mdockerinspect容器|grep-ioomkilled# 确认是不是被 OOM 了docker ps -a里如果显示Exited (137)基本就是被 OOM 杀了137 128 9即 SIGKILL。四、Dockerfile真正决定生产力的东西官方镜像能覆盖大部分场景但只要你有自己的代码要部署就必须自己写 Dockerfile。先建立最核心的认知Dockerfile 里每一条指令都会生成一层镜像。层是只读的会被缓存和复用。这句话是所有 Dockerfile 优化技巧的源头。一个完整的例子假设我有个 Go 写的 HTTP 服务项目结构如下myapp/ ├── main.go ├── go.mod └── Dockerfile# 1. 指定基础镜像必须是第一个有效指令 FROM golang:1.22-alpine AS builder # 2. 声明维护者信息用 LABELMAINTAINER 已废弃见下文 LABEL maintainermeexample.com # 3. 设定工作目录后续指令都在这个目录下执行 WORKDIR /src # 4. 先只拷依赖描述文件 —— 这是缓存优化的关键见下文 COPY go.mod ./ RUN go mod download # 5. 再拷源码并编译 COPY . . RUN CGO_ENABLED0 go build -o /out/app . # ---- 第二个阶段只带走编译产物 ---- FROM alpine:3.20 WORKDIR /app COPY --frombuilder /out/app /app/app # 6. 声明端口注意只是声明 EXPOSE 8080 # 7. 用非 root 用户运行安全 USER nobody # 8. 容器启动时执行的命令 CMD [/app/app]构建并运行dockerbuild-tmyapp:v1.dockerrun-d-p8080:8080--nameapp myapp:v1构建命令末尾那个.不是装饰——它指的是构建上下文build context即「把哪个目录发给 Docker 守护进程」。这个点是最容易出事的地方后面单独讲。指令逐个说清FROM—— 基础镜像。三种写法FROM nginx # 不写 tag等于 latest生产环境别这么干 FROM nginx:1.27-alpine # 指定版本 ✅ FROM nginxsha256:abc123... # 指定 digest最严格生产环境永远不要用latest。它会让你的构建结果不可复现——昨天能跑今天重新构建可能就挂了因为你根本不知道基础镜像变了什么。RUN—— 构建时执行命令。两种形式RUN apt-get update apt-get install -y curl # shell 形式 RUN [/bin/bash, -c, echo hello] # exec 形式这里有个必须掌握的技巧多条RUN要合并成一条。# ❌ 这样写会产生 3 层而且前两层的垃圾文件还在 RUN apt-get update RUN apt-get install -y curl RUN rm -rf /var/lib/apt/lists/* # ✅ 合并成一条清理在同一层内完成 RUN apt-get update \ apt-get install -y --no-install-recommends curl \ rm -rf /var/lib/apt/lists/*为什么第二种才对镜像层是只读且累加的。你在第 3 层删掉文件只是加了一个「删除标记」文件本身仍然躺在第 2 层里占着空间。只有在同一条RUN里完成安装和清理文件才不会进入最终镜像。COPYvsADD—— 优先用COPYCOPY app.conf /etc/app/ # 推荐 ADD app.tar.gz /opt/ # ADD 会自动解压 tar还能拉 URLADD有两个隐式行为自动解压、支持 URL恰恰是这种聪明容易造成意外。Docker 官方文档也建议除非你明确需要解压否则用COPY。CMDvsENTRYPOINT—— 这是最容易混淆的一对CMDENTRYPOINT作用提供默认命令定义固定入口docker run传参会怎样被覆盖参数会追加在后面一个 Dockerfile 中多个只有最后一个生效多个只有最后一个生效ENTRYPOINT [nginx] CMD [-g, daemon off;]这样docker run myimg执行nginx -g daemon off;而docker run myimg -v执行nginx -v——ENTRYPOINT定死了跑什么程序CMD只负责默认参数。必须用 exec 形式JSON 数组写CMDCMD [/app/app] # ✅ 进程 PID 1 是 /app/app能收到 SIGTERM CMD /app/app # ❌ 实际 PID 1 是 /bin/sh信号收不到第二种写法会让 shell 变成 PID 1docker stop发的 SIGTERM 被 shell 吃掉应用收不到结果就是每次停止都要等满 10 秒然后被 SIGKILL。这个坑非常典型。其他常用指令ENV APP_ENVprod # 环境变量构建时和运行时都在 ARG VERSION1.0 # 仅构建时的变量运行时不存在 WORKDIR /app # 切目录且目录不存在会自动创建 VOLUME [/data] # 声明数据卷挂载点 EXPOSE 8080 # 声明端口仅文档作用 USER nobody # 后续指令以此用户执行ARG和ENV的区别要记牢ENV的变量在容器运行时依然存在ARG的只在构建阶段有效。所以不要把密码写进ARG——docker history能看到构建参数。EXPOSE的真相它什么也不做这条最容易被误解EXPOSE 8080它不会打开任何端口也不会发布任何端口。它纯粹是给人和-P用的文档对人告诉使用者这个镜像的服务端口是 8080对-P大写随机映射时会以EXPOSE的端口为准真正让外部能访问的是docker run -p。写了EXPOSE但没加-p外面照样访问不了——这是「容器起来了但访问不了」的头号原因。五、缓存与多阶段构建层缓存为什么指令顺序很重要Docker 构建时会逐层找缓存只要某层的指令和它的输入没变就直接复用缓存不再执行。一旦某一层缓存失效它后面的所有层都会失效。所以把「很少变的东西」放前面「经常变的东西」放后面# ✅ 依赖清单先拷源码后拷 COPY go.mod go.sum ./ RUN go mod download COPY . . RUN go build -o /out/app .这样你每次改业务代码go mod download那层都能命中缓存如果反过来先COPY . .再下载依赖改一行代码就要重新下载全部依赖。调试缓存问题时dockerbuild --no-cache-tmyapp:v1.# 完全不用缓存dockerbuild--pull-tmyapp:v1.# 强制拉取基础镜像最新版多阶段构建把镜像从 1GB 压到 20MB这是 Dockerfile 里性价比最高的技巧。上面那个 Go 例子里第一阶段用golang:1.22-alpine带完整编译工具链几百 MB第二阶段用alpine:3.20才 5MB。FROM golang:1.22-alpine AS builder WORKDIR /src COPY . . RUN CGO_ENABLED0 go build -o /out/app . FROM alpine:3.20 # 重新开始前面的东西全不要 COPY --frombuilder /out/app /app/app CMD [/app/app]关键在于COPY --frombuilder它只从第一阶段拿走你指定的文件编译器、源码、中间产物全部留在第一阶段被丢弃。对比一下单阶段构建的 Go 镜像动辄 800MB多阶段能压到 20MB 以内。镜像小不只是省磁盘——拉取快、启动快、攻击面小镜像里没有编译器和源码被攻破也少很多可利用的东西。六、五个真实的坑1. 构建上下文太大docker build -t myapp:v1 .里的.会把整个当前目录打包发给守护进程。如果你的目录里有node_modules、.git、几个 G 的数据文件构建会在「Sending build context」这一步卡很久。解决写.dockerignore。.git node_modules *.log data/ Dockerfile这个文件的效果经常被低估——它不只是加速还能防止把密钥、日志误打进镜像。2.RUN cd不生效RUN cd /app # ❌ 下一层又回到原来的目录了 RUN ./start.sh WORKDIR /app # ✅ 用 WORKDIR RUN ./start.sh每条RUN都在新的层里执行cd的作用域只在那一条命令内部。切目录必须用WORKDIR。3. 容器一停数据就没了容器的可写层是临时的——容器一删里面的数据全没。dockerrun-d-v/host/data:/container/data nginx# 绑定挂载dockerrun-d-vmydata:/data nginx# 命名卷只要涉及数据持久化数据库、上传文件必须挂卷。把 MySQL 数据放在容器可写层里是新手最惨烈的事故之一。4.-d和--rm一起用dockerrun-d--rmnginx--rm的语义是「容器退出后自动删除」。配上-d容器一旦因任何原因退出你连排查现场的机会都没有——日志、退出码、文件系统状态全没了。--rm适合交互式调试docker run --rm -it ubuntu bash不适合后台服务。5.save/load和export/import混用这两组命令看起来像其实完全不同save/loadexport/import操作对象镜像容器保留历史与元数据✅ 完整保留❌ 全部丢弃文件大小大小能否重新运行✅⚠️ 需手动指定启动命令docker export导出的容器快照丢失了所有元数据所以用docker import导入后直接docker run会报Container command not found——因为启动命令一起丢了你必须自己补上。要迁移镜像用save/load只有需要容器当前状态快照时才用export/import。七、清理Docker 很能吃磁盘Docker 用久了磁盘会被悄悄吃掉因为停止的容器、悬空镜像、构建缓存都不会自动消失。dockersystemdf# 先看看占了多大非常有用dockercontainer prune# 删所有已停止的容器dockerimage prune# 删悬空镜像none 那些dockerimage prune-a# 删所有没被容器使用的镜像谨慎dockervolume prune# 删未被使用的卷⚠️ 可能删掉数据dockersystem prune# 以上一起清# 批量删除改了关键词后的必备操作dockerrm$(dockerps-aq)# 删所有容器docker volume prune一定要小心它删的是「没有被任何容器使用」的卷如果你停掉了数据库容器再执行数据卷会被当成没用的一起删掉。八、命令速查目的命令拉镜像docker pull nginx:1.27看本地镜像docker images起容器后台端口docker run -d -p 8080:80 --name web nginx看容器docker ps -a看日志docker logs -f --tail 100 web进容器docker exec -it web sh看详情docker inspect web看资源占用docker stats宿主机↔容器拷文件docker cp ./a.txt web:/tmp/构建镜像docker build -t app:v1 .停止/删除docker stop web docker rm web磁盘占用docker system df九、总结回头看Docker 的知识点其实就三块但每块都有必须踩过一次才记住的细节概念层镜像是只读的类容器是带可写层的实例。容器没有自己的内核它只是被隔离的进程——不理解这句OOM、命名空间、启动速度全都想不通。操作层run的-d/-p/--name/-m/--restart五个参数覆盖了 90% 的日常stop和kill的差别关系到会不会丢数据。构建层Dockerfile 的每一行都是一层。合并RUN、调整COPY顺序、用多阶段构建——这三条能把镜像从 GB 级压到几十 MB也是 Dockerfile 唯一真正的优化空间。最后给个务实的建议别一上来就去研究 Kubernetes。先在本地把「写 Dockerfile → build → run → 挂卷 → 看日志 → 进容器排障」这条链路走通五遍。等你发现「手动管十几个容器好累」的时候再去学编排那时你会明白它每一步在解决什么问题——反过来先学 K8s只会记住一堆不知道为什么要存在的概念。
阅读完成 · 觉得有帮助?