news 2026/10/9 5:38:53

Docker 入门:镜像、容器与 Dockerfile 一次讲透

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Docker 入门:镜像、容器与 Dockerfile 一次讲透
个人主页: > 我不会起名字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-a

docker ps的输出值得逐列看一遍,它是你日后排障的主要信息来源:

列含义
CONTAINER ID容器 ID 前 12 位,够用了
IMAGE来自哪个镜像
STATUSUp 3 minutes/Exited (0) 2 minutes ago
PORTS端口映射情况,排查"访问不了"先看这里
NAMES名字,--name指定的或随机生成

三、容器的五种状态与流转

容器不是「开」和「关」两个状态,它有五个:

created ──start──> running ──stop──> stopped │ ↑ │ pause unpause start ↓ │ │ paused ───────────────┘ rm → deleted

对应命令:

dockercreate# 创建但不启动(状态 created)dockerstart# 启动已停止的容器dockerrun# 相当于 create + start,一步到位dockerstop# 优雅停止,先发 SIGTERM,超时再 SIGKILLdockerkill# 直接 SIGKILL,不给程序清理机会dockerrestart# 重启dockerpause# 暂停:只冻结 CPU 调度dockerunpause# 恢复dockerrm# 删除已停止的容器dockerrm-f# 强制删除运行中的容器

stop和kill的区别值得单独记一下,因为踩过坑:

  • docker stop发的是SIGTERM,应用有机会做优雅退出——关闭连接、刷盘、清理临时文件。默认等 10 秒后若还没退出,才升级成 SIGKILL
  • docker kill发的是SIGKILL,进程直接被内核干掉,没有任何清理机会

生产环境一律用stop,除非容器已经卡死不响应。用kill删数据库容器,是有可能丢数据的。

两个「不是故障」的现象

1. 容器 Exit 后又自己活了

只要容器启动时带了--restart策略,退出后就会被拉起来:

dockerrun-d--restart=always nginx# 退出就重启,包括宿主机重启后dockerrun-d--restart=on-failure:3 nginx# 非 0 退出才重启,最多 3 次dockerrun-d--restart=unless-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. 声明维护者信息(用 LABEL,MAINTAINER 已废弃,见下文) LABEL maintainer="me@example.com" # 3. 设定工作目录,后续指令都在这个目录下执行 WORKDIR /src # 4. 先只拷依赖描述文件 —— 这是缓存优化的关键,见下文 COPY go.mod ./ RUN go mod download # 5. 再拷源码并编译 COPY . . RUN CGO_ENABLED=0 go build -o /out/app . # ---- 第二个阶段:只带走编译产物 ---- FROM alpine:3.20 WORKDIR /app COPY --from=builder /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 nginx@sha256: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—— 优先用COPY:

COPY app.conf /etc/app/ # 推荐 ADD app.tar.gz /opt/ # ADD 会自动解压 tar,还能拉 URL

ADD有两个隐式行为(自动解压、支持 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 数组)写CMD:

CMD ["/app/app"] # ✅ 进程 PID 1 是 /app/app,能收到 SIGTERM CMD /app/app # ❌ 实际 PID 1 是 /bin/sh,信号收不到

第二种写法会让 shell 变成 PID 1,docker stop发的 SIGTERM 被 shell 吃掉,应用收不到,结果就是每次停止都要等满 10 秒然后被 SIGKILL。这个坑非常典型。

其他常用指令:

ENV APP_ENV=prod # 环境变量,构建时和运行时都在 ARG VERSION=1.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_ENABLED=0 go build -o /out/app . FROM alpine:3.20 # 重新开始,前面的东西全不要 COPY --from=builder /out/app /app/app CMD ["/app/app"]

关键在于COPY --from=builder:它只从第一阶段拿走你指定的文件,编译器、源码、中间产物全部留在第一阶段被丢弃。

对比一下:单阶段构建的 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 的知识点其实就三块,但每块都有必须踩过一次才记住的细节:

  1. 概念层:镜像是只读的类,容器是带可写层的实例。容器没有自己的内核,它只是被隔离的进程——不理解这句,OOM、命名空间、启动速度全都想不通。
  2. 操作层:run的-d/-p/--name/-m/--restart五个参数覆盖了 90% 的日常;stop和kill的差别关系到会不会丢数据。
  3. 构建层:Dockerfile 的每一行都是一层。合并RUN、调整COPY顺序、用多阶段构建——这三条能把镜像从 GB 级压到几十 MB,也是 Dockerfile 唯一真正的优化空间。

最后给个务实的建议:别一上来就去研究 Kubernetes。先在本地把「写 Dockerfile → build → run → 挂卷 → 看日志 → 进容器排障」这条链路走通五遍。等你发现「手动管十几个容器好累」的时候,再去学编排,那时你会明白它每一步在解决什么问题——反过来先学 K8s,只会记住一堆不知道为什么要存在的概念。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/9 5:38:20

ThreadLocal系列(四):父子线程信息传递与TTL

父子线程信息传递与TTL前面介绍了ThreadLocal使用与内存泄漏防范&#xff0c;还从引用队列角度思考如何防范内存泄漏。 这篇文章是自己在实际中用到了RAG检索与回答用自定义线程池而不是tomcat线程池&#xff0c;防止tomcat线程池线程被占用导致无法处理其他请求。 其中用到了跨…

作者头像 李华
网站建设 2026/10/9 5:34:51

GitHub热榜解读:从AI Agent到本地优先,如何看趋势选项目

GitHub热榜的日榜&#xff08;2026-10-02&#xff09;出来了。我从几年前开始养成了每天早晨刷一遍热榜的习惯&#xff0c;不为别的&#xff0c;就想看看社区里最近在折腾什么。今天这份榜单挺有意思&#xff0c;AI Agent类的项目依然强势&#xff0c;但中间混进去好几个做本地…

作者头像 李华