如果你刚开始接触 Docker,多半会有这样一种体验:教程看了不少,名词背了一堆,可真到自己搭环境的时候,总觉得哪里没串起来。我最初折腾那阵就是这状态——服务在本机跑得好好的,换台机器就起不来,版本、依赖、系统库各掉各的链子。后来花了一周时间,把镜像、容器、仓库、数据卷、网络这几块用实际命令一步步跑通,才总算把 Docker 用顺了。
这篇算是我自己的快速入门总结笔记,偏实战、少废话。适合刚接触 Docker、想把核心概念一次搞清楚的同学;也适合有一定基础但知识点比较零散、查过就忘的人,当速查手册翻。我会先把最容易被搞混的底层逻辑讲明白,再带你把第一个容器跑起来,然后是写 Dockerfile、数据持久化、容器网络,最后一节是我实际踩过的一堆坑,建议直接收藏。
1. 为什么镜像和容器总被搞混——先把最核心的模型讲清楚
很多人第一次听 Docker 时,最容易懵的就是镜像和容器到底啥关系。这里先说结论:镜像是只读的模板,容器是模板运行出来的实例。不理解这句话,后面所有命令都是死记硬背,换个场景就不会用了。
1.1 镜像:一份只读的“文件系统快照+启动说明”
镜像不是一台运行的机器,它只是一堆静态文件的组合,外加一些启动元数据。你可以把镜像理解成菜谱加半成品食材:里面已经准备好了运行一个应用所需要的所有东西——基础操作系统文件、运行环境、依赖库、应用代码、默认配置。
Docker 镜像还有一个很重要的特性:分层存储。拉取一个镜像时,你会发现它是一层一层下载的,因为镜像是按层构建的。底层可能是一个精简的 Linux 文件系统,上面叠加了运行环境的安装层,再上面是依赖拷贝层,最上面才是应用代码层。这样做有两个好处:一是多个镜像可以共用底层,节省磁盘空间;二是再次拉取时,如果本地已经有某一层,这一层就不用重新下载。
镜像的名字由“仓库地址/镜像名:标签”组成,标签用于区分版本。比如nginx:1.25和nginx:latest是不同版本。如果不写标签,默认取latest,但生产环境我强烈建议锁定具体版本,后面避坑清单还会提到。
1.2 容器:镜像运行后多出来的那一层“可写层”
当你执行docker run时,Docker 会在镜像之上叠一层可写层,然后根据镜像里记录的启动命令,把某个进程拉起来。这个“可写层 + 进程”的组合,就是容器。
平时改容器里的文件、写日志,都是在可写层里进行的,镜像本身永远不会变。所以容器删了之后,你的修改也跟着没了;但只要镜像还在,随时可以再run一个全新的容器出来。这一点怎么强调都不过分,很多人一开始没想明白,后面才会被数据丢失吓一跳。
容器本质上是宿主机上的一个被隔离的进程,有自己的文件系统、网络空间和进程视图。进程结束了,容器也就停了。比如跑一个hello-world镜像,它打印完一段欢迎语就退出,你会看到容器状态变成Exited,这不是故障,是它该干的事干完了。
1.3 仓库:镜像的“应用商店”
镜像还有一个存放和分发的地方,叫仓库。Docker 官方的公共仓库叫 Docker Hub,里面可以 pull 到大量现成镜像,比如 Nginx、MySQL、Redis。除了公共仓库,还可以自己搭私有仓库,团队内部经常这么干。
平时跟仓库打交道的命令就两个:docker pull是拉镜像到自己机器,docker push是把自己构建的镜像推上去。刚开始学习,只需要会pull就行。
1.4 容器和虚拟机的关键区别
这里补一个无数新手问过的问题:容器和虚拟机到底差在哪?虚拟机是在宿主机之上模拟了一整套硬件,里面跑一个完整的操作系统;容器则是直接共享宿主机的内核,只做进程和文件系统层面的隔离。
| 对比项 | 虚拟机 | 容器 |
|---|---|---|
| 虚拟化层次 | 硬件级虚拟化 | 操作系统级隔离 |
| 启动速度 | 分钟级 | 秒级 |
| 镜像体积 | 几个 GB 起步 | 几十 MB 到几百 MB 常见 |
| 资源占用 | 高,每台虚拟机都要独立分配 | 低,多个容器共享宿主机内核 |
| 隔离强度 | 更强 | 相对弱,但够用 |
所以容器快、轻,适合打包应用和微服务;虚拟机更重,但隔离更彻底,适合多租户和强安全场景。两者不是替代关系,实际基建里经常混着用。
2. 新机器上从零跑通第一个容器——装好、验证、配置镜像源
理论说太多容易困,直接动手。
2.1 安装与验证:先确认两件事
不同系统的安装方式差别很大,我不展开写,只给个通用思路:Linux 上用系统自带的包管理器安装 Docker 引擎和命令行工具,比如基于 apt 的发行版可以sudo apt install docker.io,基于 yum/dnf 的发行版安装docker-ce相关包;macOS 上直接装桌面版;Windows 上建议先装好 WSL2,再在 WSL 内部安装,这样体验最顺。
装完先别急着跑容器,花十秒钟验证环境:
docker --version docker info第一条确认命令行工具装好了,第二条确认 Docker 守护进程已经在跑。如果docker info报错,大概率是服务没启动,Linux 上可以试试sudo systemctl restart docker。
2.2 把当前用户加进 docker 组,省去每次 sudo
Linux 用户很快会发现:不带 sudo 跑docker会提示权限不足。原因是 Docker 守护进程默认以 root 运行,它通过/var/run/docker.sock通讯。一个省心的办法是把当前用户加入docker组:
sudo usermod -aG docker $USER newgrp docker重新登录后再验证一次docker ps。这里必须提醒一句:能操作 docker 的账号,基本等于有了宿主机 root 权限,因为容器可以挂载宿主机目录。所以个人电脑这么做没事,公司的生产服务器上要慎重,不能随便把开发者都加进 docker 组。
2.3 拉镜像前处理一下镜像源问题
官方镜像仓库在部分网络环境下的拉取速度很不理想,甚至经常超时。推荐在开始学习前就配好镜像缓存仓库,Docker 的术语叫 registry mirror,意思是让 Docker 从你指定的镜像仓库地址拉取。
编辑/etc/docker/daemon.json(Windows 桌面版在设置界面填,macOS 同理):
{ "registry-mirrors": ["https://mirror.example.com"] }mirror.example.com换成你自己实际使用的镜像仓库地址,通常在你使用的云服务商控制台里能找到,一般叫“容器镜像服务”,开通后会给你一个专属地址。改完重启 Docker:
sudo systemctl restart docker验证是否生效:
docker info | grep -A 2 "Registry Mirrors"能看到你配置的地址就说明生效了。不要乱填未经验证的地址,宁可先不配,也别让 Docker 连不上仓库。
2.4 跑两个有代表性的容器
第一个容器选最小的:
docker pull hello-world docker run hello-world会看到一串英文输出,内容是 Docker 在教你“我的运行原理是这样的”。这个镜像只有几 KB,适合验证整个链路能走通。它跑完就退出了,因为容器里的进程本身只负责打印。
第二个跑 Nginx,这才像回事:
docker run -d --name web-demo -p 8080:80 nginx这里拆开解释一下:
-d:后台运行,不加的话终端会被绑住,日志全程刷屏。--name web-demo:给容器起个名字,后续操作就不用记那一长串容器 ID。-p 8080:80:端口映射,宿主机 8080 端口转发到容器内 80 端口。等会curl的就是宿主机的 8080。nginx:镜像名,本地没有会自动 pull。
跑完之后用docker ps看容器状态,再打开浏览器访问http://localhost:8080,看到 Nginx 欢迎页就说明通了你。因为容器里的 Nginx 进程是常驻的,所以容器不会退出,这正好和上面的 hello-world 形成对比。
2.5 高频命令速查表
新手最先要记住的就是下面这些,不用背,用多了自然就熟了:
| 命令 | 作用 | 常用配合参数 |
|---|---|---|
docker ps | 查看正在运行的容器 | -a看所有含已退出的 |
docker pull <镜像> | 拉取镜像 | 可加:tag指定版本 |
docker run <镜像> | 创建并启动容器 | -d后台、-p映射端口、--name命名 |
docker stop <容器> | 停止容器 | 容器名或前几位 ID 都行 |
docker start <容器> | 启动已停止的容器 | 重启容器不丢文件系统 |
docker rm <容器> | 删除容器 | 删除后容器内可写层丢失 |
docker rmi <镜像> | 删除镜像 | 有容器正在使用时会失败 |
docker exec -it <容器> bash | 进入容器命令行 | -it是交互式加伪终端 |
docker logs -f <容器> | 跟随查看日志 | -f类似tail -f |
docker system prune | 清理悬空资源 | -a会清理所有未使用镜像 |
docker exec是调试神器,容器里面出了任何问题,先进去手动跑一遍命令看报错,比在外面猜快得多。
3. 自己做一个能访问的镜像——Dockerfile 每条指令为什么这么写
用别人现成的镜像只是开始,真正让 Docker 发挥价值的是把自己的应用打包成镜像。这一步的核心就是写 Dockerfile。
3.1 一个完整可跑的最小示例
我从一个简单的 Python Web 应用开始讲。项目目录里先放两个文件。
requirements.txt:
flask==3.0.0app.py:
from flask import Flask app = Flask(__name__) @app.route("/") def index(): return "Hello from Docker" if __name__ == "__main__": app.run(host="0.0.0.0", port=8000)然后写 Dockerfile:
FROM python:3.12-slim WORKDIR /app COPY requirements.txt . RUN pip install --no-cache-dir -r requirements.txt COPY app.py . ENV PYTHONUNBUFFERED=1 EXPOSE 8000 CMD ["python", "app.py"]构建并运行:
docker build -t my-flask-app . docker run -d -p 8000:8000 --name flask-demo my-flask-app curl http://localhost:8000能输出Hello from Docker说明整个流程通了。注意app.run里写的host="0.0.0.0"不能省,否则 Flask 默认只监听容器内的 localhost,宿主机通过映射端口访问时会失败。
3.2 高频指令背后的逻辑
Dockerfile 看着像一串命令,其实每一行都有讲究。
FROM python:3.12-slim指定基础镜像。选slim版本是为了体积可控,它去掉了很多用不到的编译工具和文档。如果你是新手,优先选官方镜像的slim变体,别一上来就追求基于 Alpine 的极致瘦身,Alpine 用的 C 库和常见发行版不一样,有些依赖装起来会遇到兼容问题。
WORKDIR /app设置工作目录。很多人习惯用RUN cd /app,这是不对的,因为 Dockerfile 里每条RUN都是独立 shell,cd只对当前这条指令生效。WORKDIR是声明式的,后续指令都会在这个目录下执行。
COPY requirements.txt .优先拷贝依赖清单,而不是直接COPY . .。这是利用 Docker 的构建缓存:Docker 构建镜像是一层一层来的,如果某一层没有变化,后续会复用缓存。依赖文件不常变,把它单独放在前面,那么以后改业务代码时,pip install这一层可以直接走缓存,构建会快很多。
RUN pip install --no-cache-dir -r requirements.txt负责装依赖。RUN每执行一条就会生成一个新层,所以能合并的尽量合并,但也不要为了减少层数把一堆命令硬塞进一行,可读性同样重要。
ENV PYTHONUNBUFFERED=1设置环境变量。Python 应用经常遇到日志不输出的问题,后面避坑清单会详细讲。
EXPOSE 8000只是声明“我这个容器里的服务用 8000 端口”,它不会自动映射到宿主机。真正做映射的是docker run -p 8000:8000。很多新手以为写了 EXPOSE 就能访问,错就错在这。
CMD ["python", "app.py"]是容器启动后执行的命令。这里用的 JSON 数组格式,叫 exec 形式,会让python直接作为容器内的 1 号进程;如果写成CMD python app.py这种 shell 形式,实际会多套一层 shell,后面处理停止信号时就容易出问题。
3.3 CMD 和 ENTRYPOINT 怎么选
这两个指令长得像,新手经常混。简单说:CMD是启动命令加默认参数,可以被docker run后面的命令覆盖;ENTRYPOINT是固定入口,不容易被覆盖,配合CMD时,CMD的内容会作为参数传给ENTRYPOINT。
| 写法 | 效果 | 适合场景 |
|---|---|---|
CMD ["python", "app.py"] | 启动时执行 python app.py,可被 docker run 后的命令替换 | 启动命令就是全部 |
ENTRYPOINT ["python"]+CMD ["app.py"] | 启动时执行 python app.py,docker run 后接参数会传给 python | 需要固定解释器/入口,参数灵活 |
ENTRYPOINT ["python", "app.py"] | 完全固定,docker run 后接的命令只是额外参数 | 单入口应用,不太需要覆盖 |
大部分入门场景用CMD就够了,等你想做“容器启动后可以传不同参数执行不同任务”时再研究ENTRYPOINT。
3.4 别忽略 .dockerignore 和构建上下文
执行docker build时,Docker 会把当前目录作为一个“构建上下文”打包发送给守护进程。如果你的目录里有个几百 MB 的依赖目录,构建过程会又慢又占空间;如果目录里有.env、密钥文件,还可能被一起拷进镜像,造成泄露。
所以每个项目都要建一个.dockerignore:
.git node_modules __pycache__ *.pyc .env dist tmp这个文件的作用和.gitignore类似,告诉 Docker 在构建上下文中排除哪些路径。养成这个习惯,等于从源头避免了镜像臃肿和密钥泄露两个问题。
4. 容器数据为什么会丢——三种持久化方式的对比与数据库实操
很多人的第一个“事故”是这样的:用 Docker 跑了个带数据库的服务,过两天docker rm把容器删了,数据全没了。原因前面提过——容器删除时,那个可写层跟着一起销毁。所以凡是需要保存的数据,必须放到容器之外。
4.1 先把三种持久化方式分清
Docker 提供了三种挂载数据的方式:bind mount、volume、tmpfs。傻傻分不清的话,看下面这张表就够了。
| 方式 | 数据存在哪 | 适合场景 | 注意点 |
|---|---|---|---|
| bind mount | 宿主机指定目录 | 开发调试、实时改代码 | 路径写错会报错;受宿主机权限影响 |
| volume | Docker 管理目录 | 数据库、生产数据 | 推荐;docker volume系列命令可管理 |
| tmpfs | 宿主机内存 | 临时缓存、敏感信息 | 容器停止后数据消失 |
bind mount 的写法是把宿主机路径和容器路径拼在一起,比如:
docker run -d -p 8000:8000 \ -v $(pwd)/app:/app \ my-flask-app这样宿主机当前目录下的app会直接挂进容器的/app,改代码不用重新构建镜像,开发阶段非常方便。但它太“直白”,生产环境更推荐 volume。
volume 的玩法和命名磁盘类似,创建一次就能复用:
docker volume create app-data docker run -d \ --mount source=app-data,target=/data \ nginx--mount比-v的语义更清晰,参数名也更好记。强烈建议新手直接习惯用--mount,不要死记-v冒号语法。
4.2 用 MySQL 容器演示一次完整持久化
我用数据库举例,因为数据库是最需要持久化的场景。先创建 volume,再启动带 volume 的 MySQL:
docker volume create mysql-data docker run -d --name mysql-db \ -e MYSQL_ROOT_PASSWORD=your-password \ -e MYSQL_DATABASE=appdb \ --mount source=mysql-data,target=/var/lib/mysql \ -p 3306:3306 \ mysql:8.0解释两个点:-e是向容器传入环境变量,MySQL 官方镜像第一次启动时会读取这些变量来初始化 root 密码和默认数据库;--mount source=mysql-data,target=/var/lib/mysql是把之前创建的 volume 挂到 MySQL 存放数据文件的目录。
注意:如果宿主机上已经装了 MySQL 并占用了 3306,这里的-p 3306:3306会冲突,改成-p 33060:3306就行。
等容器启动完成后,进去建一张表:
docker exec -it mysql-db mysql -uroot -p输入刚才设置的密码,执行:
USE appdb; CREATE TABLE demo (id INT PRIMARY KEY AUTO_INCREMENT, name VARCHAR(50)); INSERT INTO demo (name) VALUES ('hello');然后退出,删掉这个容器,再用同样的参数重新启动一个新容器,挂载同一个 volume:
docker stop mysql-db docker rm mysql-db docker run -d --name mysql-db \ -e MYSQL_ROOT_PASSWORD=your-password \ -e MYSQL_DATABASE=appdb \ --mount source=mysql-data,target=/var/lib/mysql \ -p 3306:3306 \ mysql:8.0进入容器查一下:
docker exec -it mysql-db mysql -uroot -p -e "USE appdb; SELECT * FROM demo;"能看到刚才的hello还在。这就是 volume 的价值:卷在,数据就在。
4.3 顺手给个备份命令
数据库容器最怕手滑删除,备份其实一条命令就能做。利用--volumes-from挂载原容器的 volume,再用一个小镜像把数据目录打包到当前目录:
docker run --rm \ --volumes-from mysql-db \ -v $(pwd):/backup \ alpine \ tar czf /backup/mysql-data.tar.gz /var/lib/mysql跑完当前目录下会多出mysql-data.tar.gz。以后想恢复,就反向解压回原目录。--rm表示这个辅助容器用完就删,不会留下垃圾进程。
5. 容器之间怎么通信——端口映射、自定义网络与排查顺序
跑起来了、数据也不丢了,接下来你大概率会遇到另一个问题:几个容器之间怎么互相访问?尤其是有数据库和应用,总不能每次都用宿主机 IP 去连。
5.1 为什么容器默认“与世隔绝”
每个容器默认有自己的网络命名空间,相当于一个独立小网络。外部访问不到容器里的端口,容器访问外部也要靠 NAT 转发。所以docker run nginx之后,你在宿主机浏览器里是访问不到的,必须加-p端口映射,宿主机那边开一个口,把流量转发进容器。
-p的完整格式是-p 宿主机IP:宿主机端口:容器端口。最常用的是只写端口不写 IP:
docker run -d -p 8080:80 nginx意思是宿主机所有网卡上的 8080 端口,转发到容器内 80 端口。如果你是本地开发,建议把访问范围限制在本地回环地址:
docker run -d -p 127.0.0.1:8080:80 nginx这样只有本机能访问,局域网里其他机器访问不到,安全一点。
如果你想偷懒,可以写-P(大写),Docker 会自动把容器里声明过的端口随机映射到宿主机的某个端口,然后用docker ps去查实际映射结果。这种方式适合临时验证,不太适合正式使用。
5.2 端口冲突怎么查
报错port is already allocated是最常见的。先看有哪些容器在跑,哪个占用了端口:
docker ps找到占用的容器后,要么改新容器的宿主机端口,要么停掉旧容器。也可以用系统的端口查看命令确认谁在监听:
ss -tlnp | grep 8080我的建议是:本地学习时,宿主机端口统一用“容器端口前面加个数字”的方式,比如 MySQL 3306 映射到 33060,Nginx 80 映射到 8080,Redis 6379 映射到 16379。这样一眼就能看出哪个容器对应哪个端口,不容易冲突。
5.3 自定义网络让容器用名字互访
容器之间的 IP 不是固定的,删掉重建就可能变,所以不能写死 IP。解决办法是建立一个自定义网络,让 Docker 的 DNS 自动解析容器名。
先建网络:
docker network create app-net再把两个容器都加进去:
docker run -d --name mysql-db \ --network app-net \ -e MYSQL_ROOT_PASSWORD=your-password \ mysql:8.0 docker run -d --name backend \ --network app-net \ -p 8000:8000 \ my-flask-app这时在backend容器里,可以直接用mysql-db这个名字访问数据库,不需要知道它的 IP:
docker exec -it backend ping mysql-db应用代码里配置数据库连接地址时,直接写mysql-db:3306就行。这个便利只对自定义网络生效,默认的bridge网络不支持容器名自动解析,所以正规做法还是建一个网络,而不是所有容器都丢在默认网络里。
Docker 网络有三种常见模式:bridge是默认的隔离网络,适合单机多容器通信;host让容器直接共享宿主机网络栈,性能好但隔离弱,而且-p会被忽略;none表示无网络。初学阶段把自定义bridge用熟就够了,其他模式等遇到具体需求再研究。
5.4 端口通了但访问失败,按这个顺序排查
-p写对了,浏览器还是打不开,我教大家一套排查顺序,别一上来就删容器重建。
- 容器是否还在运行:
docker ps,确认状态不是 Exited。 - 端口是否真的映射了:
docker port <容器名>,看宿主机端口和容器端口对应关系。 - 容器内服务是否在监听正确端口:
docker exec -it <容器名> bash,进去用netstat -tlnp看监听地址。很多应用默认监听 127.0.0.1,容器外部就访问不到,必须监听0.0.0.0。 - 宿主机防火墙是否拦截:如果你在云服务器上跑,还要去安全组放行对应端口。
这一套流程走完,80% 的访问问题都能定位。剩下的就是看应用日志,比如是不是启动时连数据库失败了。
6. 新手最容易踩的坑——我提前帮你踩了
最后这部分算是我个人总结的高频问题清单,每一个都有对应的解决思路,建议直接收藏。
6.1 容器时间永远不对,日志差八小时
新手第一个困惑往往是:容器里date显示 UTC 时间,日志和宿主机的对不上。原因很简单,很多基础镜像默认使用 UTC 时区。
解决方案有两种。第一种,运行容器时挂载宿主机的时区文件:
docker run -d \ -v /etc/localtime:/etc/localtime:ro \ nginx第二种,通过环境变量指定时区,具体时区标识请按你自己所在位置设置:
docker run -d \ -e TZ=你的时区标识 \ nginx如果你写的是自己的镜像,建议在 Dockerfile 里也补上时区设置,这样不管用的人有没有加-e,默认都是对的时间。
6.2 容器明明是活的,但 docker logs 什么都没有
Docker 的日志机制只收集进程输出到 stdout 和 stderr 的内容。很多应用默认把日志写到文件里,于是你在docker logs里什么都看不到。
解决办法是在应用层面开启标准输出。比如 Python 可以设置PYTHONUNBUFFERED=1,Java 可以关掉日志文件只留控制台,Nginx 官方镜像本来就做了日志到 stdout 的软链。自己写镜像时,也要注意把日志重定向到/dev/stdout或/dev/stderr。
排查这类问题时,先docker exec -it <容器> bash进去手动跑一下应用,能看到报错就说明应用本身没问题,只是输出位置不对。
6.3 挂载的目录里全是 root 归属的文件,宿主机用户改不动
一个常见的现象是:容器往挂载目录里写文件,生成的用户是 root,宿主机普通用户打不开也删不掉。原因是容器内进程默认以 root 运行,而挂载是直通宿主机文件系统的,所以写的文件归属会变成 root。
最简单的临时方案是在运行容器时指定当前用户:
docker run -d \ --user $(id -u):$(id -g) \ -v $(pwd)/data:/data \ some-image更好的方案是在 Dockerfile 里创建一个专用用户,用USER切换到它再启动应用。这不仅能解决权限问题,还是一手安全习惯:生产容器里不要用 root 跑应用进程,万一被攻破,影响面小很多。
6.4 docker stop 总是要等十几秒,像卡住了一样
docker stop的正常流程是:给容器的 1 号进程发 SIGTERM,等它优雅退出,倒计时十秒结束还没退,就发 SIGKILL 强杀。如果你看到每次停止都要卡一二十秒,多半是容器里的进程没有处理 SIGTERM。
最典型的情况是 Dockerfile 里写成了 shell 形式:
CMD python app.py这会让 sh 作为 1 号进程,而真正的 python 是 sh 的子进程,信号被 sh 接住却没有转发给 python,结果就是应用没法优雅退出,只能等强杀。
解决方法是改成 exec 形式:
CMD ["python", "app.py"]或者直接在运行参数里加--init,让 Docker 内置一个初始化进程来转发信号:
docker run --init --name flask-demo my-flask-app6.5 磁盘越来越小,全是没人用的悬空镜像
Docker 用久了,本地会堆积大量无标签镜像、停止的容器和构建缓存。先用这个命令看看到底是谁占的空间:
docker system df然后定期清理:
docker system prune如果加了-a,会把所有未被容器使用的镜像也一并清掉,本地空间的回收效果更明显。但注意:线上机器执行prune -a要小心,本地一时没用到的镜像可能被误删,重新拉一次又是一晚上工作量。
6.6 几个提升幸福感的小习惯
最后说几个零散但很管用的经验。第一,不要用latest标签做长期固定依赖,写死了版本号,换台机器或者几个月后重新构建,行为才可预期。第二,密钥和密码不要写进 Dockerfile,运行时用-e或专门的 secrets 机制注入。第三,改完代码记得重新docker build,很多人 run 了旧镜像半天查不出问题,最后才发现是新代码根本没进容器。第四,日志里出现奇怪的^M字符,多半是脚本文件用了 Windows 换行符,拉进 Linux 容器执行时就会报错,写干净的文件编码能从根上避免。
聊到这里,基本的单机 Docker 操作应该已经够你用了。我自己学习时最大的体会是:先别急着碰编排工具和复杂架构,老老实实把docker run、docker exec、docker logs这些命令行玩熟,把镜像构建和数据持久化的手感练出来,后面再看编排配置就会觉得很轻松。另一个习惯是,遇到任何异常先docker logs,再看文档,别一上来就docker rm -f,删容器永远不是排查问题的第一步。希望这份笔记能帮你少踩我当初踩过的那些坑,剩下的就靠你多跑几次命令了。