Docker 零基础:镜像、容器、仓库一张图讲透 + docker run 参数逐字拆解
本文面向完全零基础的读者:没装过 Linux、没写过一行配置、甚至没听过「容器」这个词都没关系。跟着敲一遍命令,你就能从零把 Docker 跑起来,并且真正看懂 docker run 后面每一个参数的含义。
全文约定:命令前带 $ 表示在终端输入的内容;# 开头的是注释;文中所有命令都可以直接复制粘贴执行。
—
目录
- 什么是 Docker?为什么需要容器?
- 一张图讲清:镜像、容器、仓库、Dockerfile 的分工
- 安装 Docker(Debian / Ubuntu)
- 第一个容器:docker run hello-world 到底发生了什么
- docker run 每个参数逐字拆解
- 常用命令速查表
- 镜像与 Docker Hub:pull、search、tag、国内加速
- 数据持久化:为什么容器删了数据就没了
- Dockerfile 最基础:FROM / RUN / COPY / CMD
- 常见坑与避坑表
—
一、什么是 Docker?为什么需要容器?
1.1 那个让所有程序员崩溃的经典场景
你一定听过这句话:
它通常发生在你把代码发给同事/客户/甲方之后:
- 你的电脑:Python 3.12、Node 18、MySQL 8.0.36,代码跑得飞快;
- 对方电脑:Python 3.8、Node 14、MySQL 5.7,直接报错,连启动都启动不了;
- 折腾一下午,最后发现是个「依赖版本对不上」的问题。
问题的根源是:程序不是孤立的,它依赖操作系统里的各种库(glibc、OpenSSL)、各种运行时(Python、Java、Node)、各种中间件(数据库、缓存)。你在自己机器上装好的这一大堆东西,就是程序的「运行环境」。
传统做法是让对方也去装一遍这堆东西——装错一个版本,前功尽弃。
1.2 容器的思路:连程序带环境一起打包
Docker 的解法非常朴素:
既然环境是问题所在,那就把程序 + 它需要的所有依赖 + 环境配置,全部打包进一个标准化的「箱子」,这个箱子在哪台机器上都能以完全相同的方式运行。
这个「箱子」就是容器(container)。
一个贴切的类比:
| 现实世界 | Docker 世界 |
| 集装箱(标准尺寸,船、火车、卡车都能运) | 容器(标准格式,任何装了 Docker 的机器都能跑) |
| 集装箱的图纸/模具 | 镜像(image) |
| 造集装箱的工厂生产线 | Dockerfile |
| 集装箱堆放场 / 集装箱租赁市场 | 仓库(registry,如 Docker Hub) |
集装箱的伟大之处不在于「装了货物」,而在于标准:全世界统一尺寸、统一接口,所以货不用倒来倒去,船到港直接换火车。容器同理——统一了「运行环境」这个接口的标准,软件从此告别「在我机器上能跑」。
1.3 容器 vs 虚拟机:重和轻的区别
很多读者会问:「这不就是虚拟机(Virtual Machine)吗?虚拟机不也能隔离环境?」
两者确实都能隔离环境,但原理完全不同:
| 对比项 | 虚拟机(VMware、VirtualBox 等) | 容器(Docker) |
| 模拟了什么 | 一整台硬件(CPU、内存、磁盘),上面再装一整套操作系统 | 只模拟了进程运行的环境(文件、网络、进程号),共用宿主机的操作系统内核 |
| 体积 | 一个系统镜像动辄 10 GB 起步 | 一个镜像通常几十 MB 到几百 MB |
| 启动速度 | 分钟级(要启动整个操作系统) | 秒级甚至毫秒级(本质是起一个进程) |
| 资源开销 | 每个 VM 要预留一块完整内存 | 多个容器可以共享内核,内存占用极小 |
| 隔离强度 | 硬件级,隔离最强(安全性最高) | 进程级,隔离稍弱,但足够日常使用 |
打个比方:
- 虚拟机 = 在楼里再盖一套完整的房子(带地基、带水电),独立但重;
- 容器 = 同一栋楼里的隔断房,共用大楼的管道(操作系统内核),但门是锁的(文件、进程、网络互相隔离),轻快、多而不挤。
所以日常开发、跑服务、跑数据库,用容器;需要跑一个完全不同的操作系统内核(比如在 Linux 上跑 Windows),才需要虚拟机。
1.4 先记住一句话
容器 = 一个被标准化打包、随时可以运行的「程序 + 环境」箱子;Docker 就是造箱子、运箱子、开箱子的那个平台。
—
二、一张图讲清:镜像、容器、仓库、Dockerfile 的分工
新手最容易懵的就是这四个词。我们用「蛋糕」类比一次,就再也不混了。
2.1 四件套类比表
| 概念 | 英文 | 蛋糕类比 | 在 Docker 里的定义 | 是否「活的」 |
| 镜像 | Image | 蛋糕模具 + 配方(一个模板) | 一个只读的「程序+环境」模板包 | ❌ 死的,就是个文件 |
| 容器 | Container | 用模具烤出来的那个蛋糕(可以切开吃) | 镜像运行起来的一个实例(一个进程) | ✅ 活的,有运行状态 |
| 仓库 | Registry | 模具商店(去那儿买模具) | 存放和分发镜像的服务器(如 Docker Hub) | ❌ 是个存储/分发服务 |
| Dockerfile | Dockerfile | 配方说明书(一步一步教你怎么做模具) | 一个文本文件,描述如何「构建」出一个镜像 | ❌ 是个文本文件 |
2.2 一张文字图
你写的配方 模具(只读模板) 蛋糕(活的实例)
┌──────────────── build ┌────────────────┐ run ┌──────────────────┐
│ Dockerfile │ ────────► │ Image 镜像 │ ───────► │ Container 容器 │
│ (文本文件) │ 构建 │ (如 nginx:1.27) │ 运行 │ (进程, 可多个) │
└────────────────┘ └───────┬────────┘ └────────┬─────────┘
│ │
上传/下载 一个镜像可以
▼ 跑出很多容器
┌────────────────┐
│ Registry 仓库 │ 例如:同一个 nginx:1.27
│ (如 Docker Hub) │ 可以开 3 个容器
└────────────────┘ 监听不同端口
2.3 用四句话总结分工
- Dockerfile:你写的「建造图纸」,描述镜像里装了什么、怎么装。
- Image(镜像):按图纸建出来的「成品模板」,只读,不可更改;可以有多层(layer),相同层可复用。
- Container(容器):镜像「跑起来」的样子。同一张镜像可以同时跑 10 个容器,互不影响(就像同一块模具烤 10 个蛋糕)。
- Registry(仓库):存放镜像的「云盘/商店」。官方的叫 Docker Hub;你也可以自建仓库(如 Harbor、私有 registry),国内各大云厂商也提供镜像加速器。
2.4 一个容易混淆的点:镜像 vs 容器到底啥关系?
一句话:
镜像是静态的「模板」,容器是动态的「进程」;容器 = 镜像 + 运行时的读写层 + 配置(端口、环境变量、挂载目录……)。
所以你会看到这样的现象:
- 删掉一个容器,镜像还在,可以再
docker run 一个一模一样的;
- 但容器里没挂载卷的数据,随容器一起没了(因为容器只是「临时工」,镜像才是「资产」);
- 容器里跑出来的新文件,如果想保留下来,必须
docker commit 成镜像,或用挂载卷(第八节细讲)。
—
三、安装 Docker(Debian / Ubuntu)
本文以 Debian 11+ / Ubuntu 20.04+ 为例,两者命令几乎一致。CentOS/RHEL 思路相同,只是源换成 dnf,此处不展开。
3.1 方式一:官方一键脚本(最省事,推荐新手)
Docker 官方提供了一个一键安装脚本 get.docker.com,它会自动识别你的发行版、配源、装最新版 Docker Engine。
# 1. 先更新一下系统包索引(建议,不是必须)
sudo apt update
# 2. 执行官方一键安装脚本(会提示输入你的 sudo 密码)
curl -fsSL https://get.docker.com | sudo bash
# 3. 安装完成,看版本验证(能输出版本号就说明装好了)
docker --version
你会看到类似输出:
Docker version 27.x.x, build xxxx
如果网络环境访问 docker 官方源很慢或失败,见下文「方式二」手动配源,或直接用国内镜像源。
3.2 方式二:手动添加官方 apt 源(更稳、可控制版本)
适合公司内网、想锁定 Docker 版本的场景。
# 1. 更新包索引
sudo apt update
# 2. 安装前置依赖(允许 apt 使用 https 源)
sudo apt install -y ca-certificates curl gnupg
# 3. 添加 Docker 官方的 GPG 密钥
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# 4. 把 Docker 仓库写入 apt 源
# 注意把 "noble" 换成你的发行版代号:
# Ubuntu 24.04=noble, 22.04=jammy, 20.04=focal
# Debian 12=bookworm, 11=bullseye
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu noble stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# 5. 更新索引并安装
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 6. 验证
docker --version
3.3 关键一步:把用户加入 docker 组(普通用户免 sudo)
默认情况下,只有 root(或 sudo)能操作 Docker daemon。但每个容器命令都敲 sudo 很烦,而且 sudo docker 创建出来的文件 owner 是 root,后面容易出权限坑。正确做法是把你自己的用户加入 docker 组:
# 1. 把当前用户加入 docker 组
sudo usermod -aG docker $USER
# 2. 让组生效:必须退出当前终端重新登录(或执行下面这句立刻生效)
newgrp docker
# 或者更彻底:注销账号重新登录
# 3. 不加 sudo 直接跑,能输出版本即成功
docker --version
# 4. 终极验证(第四节细讲)
docker run hello-world
⚠️ 注意:加入 docker 组 ≈ 拥有了 root 权限(因为能起特权容器直接读写宿主机文件系统)。这是 Docker 的设计使然,在个人电脑上无所谓,在生产服务器上请注意:不要把 docker 组随便给不受信任的账号。
3.4 启动 Docker 服务(大多数发行版装完已自启)
# 查看 docker 服务状态(正常应显示 active (running))
sudo systemctl status docker
# 如果没在跑,启动它
sudo systemctl start docker
# 设置开机自启(一般默认已开)
sudo systemctl enable docker
—
四、第一个容器:docker run hello-world 到底发生了什么
4.1 直接跑
docker run hello-world
第一次执行,你会先看到它去下载镜像:
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
4f53...: Pull complete
Digest: sha256:xxxx...
Status: Downloaded newer image for hello-world:latest
Hello from Docker!
This message shows that your installation appears to be working correctly.
...
之后它会打印一大段「Hello from Docker!」,然后容器自动退出。
4.2 逐字拆解这条命令
docker run hello-world 一共两部分:
| 部分 | 含义 |
docker | 调用 Docker 客户端(CLI) |
run | 子命令:创建并启动一个新容器(注意:run = create + start,是复合动作) |
hello-world | 镜像名(省略了 :latest 标签,等价于 hello-world:latest) |
4.3 它背后到底干了什么(分 5 步)
- 本地找镜像:Docker 先看本机有没有
hello-world:latest 这个镜像——第一次没有。
- 从仓库拉取:没找到就去 Docker Hub(默认的公共仓库)下载这个镜像(只有几 KB,所以很快)。
- 创建容器:基于这个镜像创建一个新的容器实例(容器 ID 是随机的一串十六进制,如
a1b2c3d4...)。
- 启动容器:容器启动后,执行镜像里定义好的启动命令——
hello-world 镜像的启动命令就是「打印一段欢迎文字」。
- 主进程退出 → 容器停止:那段文字打印完,容器里唯一的进程结束了,容器随之停止并退出(Exited)。
所以你现在用 docker ps 是看不到它的——ps 只显示正在运行的容器。要看「死掉」的容器:
# 看最近创建的所有容器(包括已停止的)
docker ps -a
你会看到类似:
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
a1b2c3d4e5f6 hello-world "/hello" 2 minutes ago Exited (0) 2 minutes ago loving_turing
| 列名 | 含义 |
| CONTAINER ID | 容器唯一 ID(短 ID) |
| IMAGE | 用的哪个镜像 |
| COMMAND | 容器里跑的启动命令 |
| CREATED | 容器创建时间 |
| STATUS | 状态:Up 5 seconds(运行中)/ Exited (0) ...(已退出,0 表示正常退出) |
| PORTS | 端口映射(本例没有) |
| NAMES | 容器名(没指定 --name 时,Docker 会随机起一个「形容词_人名」,如 loving_turing) |
💡 关键认知:容器是「一次运行」,不是「一个常驻的东西」。主进程一退出,容器就停了。后面你会看到 -d(后台运行)、--restart always(挂了自动拉起)这些参数,都是为了让容器「一直活着」。
4.4 清理一下
# 删掉这个已经退出的容器(它只占几 KB,不删也无所谓)
docker rm $(docker ps -a -q)
# 解释:ps -a -q 列出所有容器 ID(-q 只输出 ID),rm 删掉
—
五、docker run 每个参数逐字拆解
这是全文最核心的一节。docker run 是 Docker 里使用频率最高的命令,后面跟的参数决定了这个容器「怎么活」。下面逐个拆解,每个参数都配一个 nginx 例子,你可以真的敲一遍验证。
5.0 先准备一个通用基底命令
后面每个例子都以 nginx 镜像为主角。nginx 是一个极轻量的 Web 服务器,镜像里自带一个 hello 页面,默认监听 80 端口。
# 先确保拉下 nginx 镜像(第一次会下载,约 50~100 MB)
docker pull nginx
每次跑完一个验证例子后,记得先 docker stop <容器名> 再 docker rm <容器名>,避免端口/名字被占用(或者直接用 --rm,见 5.6)。
5.1 -d:后台运行(detached)
含义:让容器在后台运行,终端立刻返回,不被容器「占住」。
# -d = --detach,后台运行
docker run -d --name web1 nginx
| 参数 | 全称 | 含义 |
-d | --detach | 容器在后台跑,终端不被占用 |
--name web1 | — | 给容器起名 web1(方便后续 stop/logs/rm 引用,见 5.4) |
不加 -d 时(前台模式),终端会一直显示 nginx 的日志,按 Ctrl+C 容器才会停。-d 则是「扔到后台」,你可以接着干别的事。
验证:
# 应该能看到 web1 在运行(STATUS 是 Up)
docker ps
# 访问它(宿主机 80 端口被 nginx 占用,浏览器打开 http://127.0.0.1 能看到 nginx 欢迎页)
curl http://127.0.0.1
⚠️ 如果你本机 80 端口被别的服务占了,会报 port is already allocated,先 docker rm -f web1 释放,或改用 -p 8080:80(见 5.2)。
5.2 -p 8080:80:端口映射(宿主机端口 : 容器端口)
含义:把宿主机的 8080 端口「转发」到容器的 80 端口。访问 http://宿主机IP:8080 就等于访问容器里的 80 端口。
格式:-p 宿主机端口:容器端口,或 -p 宿主机IP:宿主机端口:容器端口(绑定到特定网卡)。
# 先清掉上一个(释放 80 端口)
docker rm -f web1
# -p 8080:80 宿主机8080 → 容器80
docker run -d -p 8080:80 --name web2 nginx
验证:
# 访问 8080,而不是 80
curl http://127.0.0.1:8080
# 能看到 nginx 欢迎页 = 端口映射成功
为什么需要端口映射?因为容器有独立的网络命名空间,容器里的 80 端口不等于宿主机的 80 端口。-p 就是 Docker 帮你建的一条「隧道」。
常见写法对照表:
| 写法 | 效果 |
-p 8080:80 | 宿主机 8080 → 容器 80(最常用) |
-p 80:80 | 宿主机 80 → 容器 80(容器直接占宿主机 80) |
-p 127.0.0.1:8080:80 | 只有本机能通过 8080 访问(更安全,不暴露到外网) |
-p 3000:80 -p 3001:80 | 可以映射多个端口(同一容器多个服务) |
-p 8080:80/udp | 指定协议为 UDP(默认 TCP) |
⚠️ 顺序千万别写反:是「宿主机:容器」,不是「容器:宿主机」。写反了(-p 80:8080)会导致映射到不存在的端口,服务打不开,且不一定报错,极难排查。
5.3 -v /data:/data:挂载卷(volume / bind mount)
含义:把宿主机的目录 /data 和容器里的目录 /data 绑在一起。容器内对 /data 的读写,实际发生宿主机的 /data 上,容器删了数据还在。
# 宿主机建个目录(可选,不建 Docker 也会自动建)
sudo mkdir -p /data
# -v 宿主机路径:容器内路径
docker run -d -p 8080:80 -v /data:/data --name web3 nginx
| 参数 | 全称 | 含义 |
-v | --volume | 挂载。左边是宿主机路径,右边是容器内路径 |
冒号 : | — | 分隔「宿主机 : 容器」 |
验证(往容器里写文件,看宿主机有没有):
# 进容器(第五节第 5 个参数会讲 exec,这里先直接用)
docker exec -it web3 sh -c 'echo "hello from container" > /data/test.txt'
# 在宿主机上看(应该能看到这个文件!)
cat /data/test.txt
# 输出: hello from container
这就是数据持久化的核心机制,第八节会系统讲。注意:-v 和第八节的「命名卷」是两回事,-v 宿主机绝对路径:容器路径 叫绑定挂载(bind mount),-v 卷名:容器路径 叫命名卷(named volume)。新手先用绑定挂载最直观。
5.4 -e KEY=value:设置环境变量
含义:给容器里的程序注入一个环境变量。很多软件靠环境变量配置(数据库密码、时区、管理员邮箱等)。
# -e 传入多个环境变量,用空格分隔多个 -e
docker run -d -p 8080:80 \
-e NGINX_PORT=80 \
-e TZ=Asia/Shanghai \
--name web4 nginx
| 参数 | 全称 | 含义 |
-e | --env | 设置环境变量,格式 KEY=value |
验证(进容器看环境变量):
# 容器里 echo 刚才设的变量
docker exec web4 sh -c 'echo $TZ'
# 输出: Asia/Shanghai
经典例子:起 MySQL 容器,密码就是靠 -e MYSQL_ROOT_PASSWORD=xxx 传的(不设密码 MySQL 容器会直接拒启动)。
5.5 --name:给容器起名
含义:不指定时 Docker 会随机起个「形容词_人名」的名字(如 loving_turing),不好记。--name 让你自定义一个易读的名字。
docker run -d -p 8080:80 --name my-nginx nginx
| 参数 | 含义 |
--name my-nginx | 容器名为 my-nginx |
起名的好处:后续所有命令都能用名字代替那一长串容器 ID。
# 用名字操作,不用记 ID
docker stop my-nginx
docker logs -f my-nginx
docker rm my-nginx
命名规则:小写字母、数字、连字符(-),不能以数字开头。名字全局唯一,重复会报错。
5.6 --rm:容器退出后自动删除
含义:容器停止时自动删除,不留垃圾。
# 注意:--rm 和 -d 一般一起用;前台模式下 Ctrl+C 后也会自动删
docker run --rm -p 8080:80 nginx
| 参数 | 含义 |
--rm | 容器退出(Exited)后自动删除,省去手动 docker rm |
适合什么场景:临时任务、调试、跑一次性脚本。不适合长期运行的服务(长期服务通常配 --restart always,见 5.7)。
验证:
# 跑一个马上退出的容器,加 --rm
docker run --rm hello-world
# 跑完再看,应该找不到这个容器了(被自动删了)
docker ps -a | grep hello
# (无输出 = 已被自动清理)
5.7 --restart always:重启策略(挂了/开机自动拉起)
含义:告诉 Docker「这个容器该一直活着」——进程崩了自动重启,Docker daemon 重启(如服务器重启)后也自动拉起。
docker run -d -p 8080:80 --name web5 --restart always nginx
| 参数 | 全称 | 含义 |
--restart | 重启策略 | 指定容器退出后 Docker 如何处理 |
--restart 可选值对照表:
| 值 | 含义 | 适用场景 |
no(默认) | 不自动重启 | 一次性任务 |
on-failure[:max-retries] | 仅非 0 退出码(异常)时重启,可限制重试次数 | 可能偶尔崩溃的服务 |
always | 只要容器退出就重启(含正常退出),且 daemon 启动时拉起 | 长期运行的服务(最常用) |
unless-stopped | 同 always,但如果你手动 docker stop 过,daemon 重启后不自动拉起 | 需要手动控制的服务 |
验证(杀掉容器里的 nginx 进程,看 Docker 会不会救活它):
# 找到容器内 nginx 主进程 PID 并杀掉
docker exec web5 sh -c 'kill 1'
# 过 1~2 秒再看,容器又活了(被自动重启)
docker ps | grep web5
# STATUS 会显示 Up 2 seconds(重启时间很短)
5.8 组合起来:一条「生产可用」的 docker run
把上面所有参数串起来,就是一条完整的、可上生产的命令:
docker run -d \
-p 8080:80 \
-v /opt/nginx/html:/usr/share/nginx/html \
-e TZ=Asia/Shanghai \
--name prod-nginx \
--restart always \
nginx
逐行拆解:
| 片段 | 作用 |
-d | 后台运行 |
-p 8080:80 | 宿主机 8080 → 容器 80 |
-v /opt/nginx/html:/usr/share/nginx/html | 把宿主机目录挂进容器,改 HTML 不用重建镜像(第八节细讲) |
-e TZ=Asia/Shanghai | 时区设为上海(日志时间戳正确) |
--name prod-nginx | 命名,方便管理 |
--restart always | 挂了自动拉起,开机自启 |
nginx | 用的镜像 |
这条命令值得你抄下来当模板。以后起任何服务,照这个结构套参数即可。
5.9 参数速记表(放一边随时查)
| 参数 | 全称 | 一句话含义 | 新手要不要 |
-d | --detach | 后台运行 | ✅ 几乎必用 |
-p 宿:容 | --publish | 端口映射 | ✅ 要对外提供服务必用 |
-v 宿:容 | --volume | 挂载目录(持久化) | ✅ 存数据必用 |
-e K=V | --env | 传环境变量 | 视软件而定 |
--name X | — | 自定义容器名 | ✅ 强烈建议 |
--rm | — | 退出自动删 | 临时任务用 |
--restart X | — | 重启策略 | ✅ 常驻服务必用 |
-it | --interactive --tty | 分配伪终端(配 exec 用) | 调试用(见速查表) |
--network X | — | 指定网络 | 进阶 |
--memory 512m | — | 限制内存 | 进阶 |
—
六、常用命令速查表
把下面这张表存下来,日常 90% 的操作都在这了。
6.1 容器(container)相关
| 命令 | 作用 | 备注 |
docker ps | 看正在运行的容器 | 最常用 |
docker ps -a | 看所有容器(含已停止) | -a = all |
docker ps -n 5 | 看最近 5 个容器 | -n 限制条数 |
docker run [参数] 镜像 | 创建并启动容器 | 见第五节 |
docker start 容器名 | 启动已停止的容器 | 容器还在,只是停了 |
docker stop 容器名 | 停止运行中的容器 | 先 SIGTERM 优雅停,10s 后强杀 |
docker restart 容器名 | 重启容器 | = stop + start |
docker rm 容器名 | 删除容器(必须先停止) | 删的是容器,镜像还在 |
docker rm -f 容器名 | 强制删除(没停也删) | -f = force |
docker exec -it 容器名 sh | 进入容器内部 shell | 调试神器,见下 |
docker logs 容器名 | 看容器日志 | 加 -f 实时跟踪 |
docker logs -f --tail 50 容器名 | 实时看最近 50 行日志 | 排查问题第一招 |
docker inspect 容器名 | 看容器的全部详细信息(JSON) | 查 IP、挂载、端口全靠它 |
docker top 容器名 | 看容器里有哪些进程 | 类似 ps |
docker rename 旧名 新名 | 改容器名 | 很少用 |
docker kill 容器名 | 立即强杀(SIGKILL) | 类似 kill -9 |
exec -it 逐字拆解(高频中的高频):
docker exec -it my-nginx sh
| 部分 | 含义 |
exec | 在已运行的容器里执行一条命令 |
-i | --interactive:保持标准输入打开(让你能输入) |
-t | --tty:分配一个伪终端(让你看到命令行提示符) |
my-nginx | 目标容器 |
sh | 要执行的命令(进 shell;有 bash 的镜像可以写 bash) |
进去后你就「站在」容器里面了,ls、cat、ps 随便用。按 Ctrl+P Ctrl+Q 可以「脱离但不退出容器」,直接输入 exit 则是退出 shell(容器还活着)。
6.2 镜像(image)相关
| 命令 | 作用 | 备注 |
docker images | 看本地所有镜像 | 可加 -q 只输出 ID |
docker pull 镜像名:标签 | 从仓库下载镜像 | 见第七节 |
docker rmi 镜像名 | 删除本地镜像 | 被容器使用时需先删容器或 -f |
docker rmi -f $(docker images -q) | 清空所有镜像 | 慎用! |
docker search 关键词 | 在 Docker Hub 搜索镜像 | 结果较多,配合 --filter |
docker tag 源 目标 | 给镜像打标签(改名) | 第七节细讲 |
docker build -t 名称:标签 . | 从 Dockerfile 构建镜像 | 第九节细讲 |
docker push 镜像名:标签 | 把镜像推送到仓库 | 需先登录 |
docker login | 登录 Docker Hub / 私有仓库 | 交互输入账号密码 |
6.3 网络(network)/ 卷(volume)相关
| 命令 | 作用 | 备注 |
docker network ls | 看所有网络 | 默认有 bridge/host/none |
docker network create 名称 | 创建一个网络 | 容器间通信进阶用 |
docker network inspect 名称 | 看网络详情(含连接的容器 IP) | 查容器 IP |
docker volume ls | 看所有命名卷 | 绑定挂载不算命名卷 |
docker volume create 名称 | 创建一个命名卷 | |
docker volume inspect 名称 | 看卷详情(含宿主机实际路径) | |
docker volume rm 名称 | 删除卷 | 数据会真丢,慎用 |
docker volume prune | 删除所有未被使用的卷 | 加 -f 免确认 |
6.4 清理 / 系统相关(重要!)
| 命令 | 作用 | 破坏性 |
docker system df | 看 Docker 占用磁盘的空间分布 | 只读,安全 |
docker system prune | 清理:已停止的容器 + 未使用的网络 + 悬空镜像(dangling) | 中(不删有用镜像/卷) |
docker system prune -a | 同上 + 所有未被容器使用的镜像 | 高(镜像会没) |
docker system prune -a --volumes | 同上 + 所有未被使用的卷 | 极高(数据可能没) |
💡 磁盘告急时先跑 docker system df 看是什么占了空间,再决定用哪个 prune。--volumes 千万别随手加。
6.5 一条命令的「执行链条」记忆法
pull(拉镜像) → run(起容器) → ps(看状态) → logs(看日志) → exec(进去调) → stop(停) → rm(删容器) → rmi(删镜像)
记住这条链,90% 的操作场景你都知道该敲什么了。
—
七、镜像与 Docker Hub:pull、search、tag、国内加速
7.1 Docker Hub:全球最大的公共镜像仓库
Docker Hub(https://hub.docker.com)是 Docker 官方的公共镜像仓库,几乎所有你听过的软件都有官方镜像:nginx、mysql、redis、node、python……
一个镜像的完整地址长这样:
[仓库地址/]/[命名空间/]/镜像名[:标签]
↓ ↓ ↓ ↓
hub.docker.com library nginx latest (可省略,默认 latest)
- 省略仓库地址 = 默认
docker.io(即 Docker Hub);
- 官方镜像的命名空间固定是
library(也常省略,直接写 nginx 就是 library/nginx);
- 社区/个人镜像才有
用户名/镜像名 的形式(如 jwilder/nginx-proxy)。
7.2 docker pull:拉取镜像
# 拉取最新版 nginx(:latest 可省略)
docker pull nginx
# 拉取指定版本(强烈建议生产环境锁版本!)
docker pull nginx:1.27
# 拉取一个完整地址的镜像(社区镜像)
docker pull bitnami/mysql:8.0
逐字拆解:
| 部分 | 含义 |
pull | 从仓库下载镜像到本地 |
nginx | 镜像名(= library/nginx) |
:1.27 | 标签(tag),指定版本,缺省为 latest |
7.3 docker search:搜索镜像
# 搜索名字里含 "nginx" 的镜像
docker search nginx
# 只看官方(带 "OFFICIAL" 标记)且星标数高的
docker search --filter is-official=true --filter stars=1000 nginx
输出示例(节选):
NAME DESCRIPTION STARS OFFICIAL
nginx Official build of Nginx. 20000+ [OK]
tutum/nginx nginx with SSL for HTTPS (uses nginx:stable) 120
...
| 列 | 含义 |
| NAME | 镜像名 |
| DESCRIPTION | 描述 |
| STARS | 社区点赞数(越高越靠谱) |
| OFFICIAL | 是否 Docker 官方维护 |
新手找镜像技巧:优先选 [OK](官方)或 stars 高的。
7.4 tag 是什么:latest vs 版本号
tag(标签)就是镜像的「版本标记」,同一个镜像名可以有多个 tag。
# 看同一个镜像的所有 tag(去 hub.docker.com 页面看,或)
docker pull nginx:1.27
docker pull nginx:1.25
docker pull nginx:stable
# 现在本地有了三个不同标签的 nginx
docker images | grep nginx
latest 的坑(重要):
latest 不代表「最新版本」,它只是 Docker Hub 上一个固定的标签名,由镜像维护者手动决定什么时候更新它。今天 latest 可能是 1.27,明天上游发了 1.28,latest 才变——但你 docker pull 过的本地 latest 不会自动变。
| 场景 | 建议 tag | 原因 |
| 学习/尝鲜 | latest 或 stable | 无所谓 |
| 生产环境 | 具体版本号(如 nginx:1.27.3) | 可复现、可回滚、latest 变了你不知道 |
| 需要长期支持 | stable / 大版本(1.27) | 补丁自动更新,但主版本不变 |
docker tag:给镜像改个名/打个标
# 把本地 nginx:1.27 复制一份标签,叫 my-nginx:v1
# (注意:不是"移动",原标签还在,相当于多起一个别名)
docker tag nginx:1.27 my-nginx:v1
# 之后 push 到私有仓库时会用到这个 tag
docker images | grep my-nginx
类比:docker tag 就像给同一本书贴了第二个书名标签,书(镜像层)还是那本,多了一个叫法。
7.5 国内镜像加速配置
痛点:国内直接访问 Docker Hub 经常慢、超时、connection reset。
解法:配置镜像加速器(mirror)——国内云厂商提供的 Docker Hub 加速节点,Docker 会优先从它拉取,拉不到再回源。
⚠️ 说明:各大云厂商的加速器地址会变化/需要登录获取,且 Docker 23+ 默认要求加速器为 HTTPS 并支持鉴权,部分旧加速器可能失效。下面是通用配置方法,具体地址请以你使用的云厂商当前提供的为准(阿里云/腾讯云/华为云控制台搜「镜像加速器」都有个人专属地址)。
配置方法(修改 /etc/docker/daemon.json):
# 1. 编辑 daemon 配置(没有就新建)
sudo tee /etc/docker/daemon.json <<'EOF'
{
"registry-mirrors": [
"https://你的专属加速地址",
"https://备用加速地址"
]
}
EOF
# 2. 重载配置并重启 docker
sudo systemctl daemon-reload
sudo systemctl restart docker
# 3. 验证(能看到 Mirrors 字段即生效)
docker info | grep -A 5 "Registry Mirrors"
# 4. 再拉一个镜像试试
docker pull nginx
如果加速器也不行(有些公共加速器已停服),备选方案:
- 用公司/学校内网的 registry 镜像(如清华 TUNA、中科大 USTC 等学术源,地址以各源官网当前公布的为准);
- 手动在能访问的机器上
docker pull + docker save 导出成 tar,再 docker load 导入(离线搬运法):
# 能访问的机器:导出
docker save nginx:1.27 -o nginx-1.27.tar
# 传到目标机器后:导入
docker load -i nginx-1.27.tar
—
八、数据持久化:为什么容器删了数据就没了
8.1 一个残酷的事实
容器是「一次性的」。容器里产生的数据,如果你没挂载卷,容器一删就灰飞烟灭,而且无法恢复。
为什么?回忆第二节的图:
容器 = 镜像(只读) + 容器读写层(临时) + 配置
↑ 这部分是临时的,删容器就没了
镜像层是只读的,容器运行时的所有写操作都落在上面的「容器读写层」——这一层没有持久化,docker rm 的瞬间它和容器一起被销毁。
真实痛点:
- 你起个 MySQL 容器,灌了 3 天业务数据;
- 为了升级镜像,
docker rm 旧容器、docker run 新容器;
- 数据没了。哭都来不及。
8.2 解法:-v 挂载卷
把「容器里存数据的那个目录」指向宿主机的一个目录(或命名卷),数据就落在容器之外,容器删了数据还在。
┌─────────────── 宿主机 ───────────────┐
│ /opt/mydata/ │ ← 真实数据存在这儿
│ ├── db/ │
│ └── conf/ │
│ │ │
│ ▼ (-v 挂载绑定) │
├─────────── Docker ───────────────────┤
│ ┌─── 容器 ───┐ │
│ │ /data │◄── 容器里看到的就是它 │
│ │ (读写) │ 删容器不影响 │
│ └────────────┘ │
└──────────────────────────────────────┘
8.3 完整例子:nginx 挂载 html 目录(改网页不用重建容器)
场景:用 nginx 跑一个静态网站。如果网页文件(HTML)放在镜像里,每次改一行文案都要重新 build 镜像、重建容器,太蠢。正确做法:把 HTML 目录挂在宿主机,改文件刷新浏览器立即生效。
第 1 步:准备宿主机的网站目录
# 建一个目录放网页
sudo mkdir -p /opt/nginx/html
# 写一个首页
sudo tee /opt/nginx/html/index.html <<'EOF'
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="utf-8"><title>我的第一个 Docker 网站</title></head>
<body>
<h1>Hello, Docker!</h1>
<p>我是从宿主机 /opt/nginx/html 挂载进来的。</p>
</body>
</html>
EOF
第 2 步:起 nginx,把宿主机目录挂到容器默认网页目录
# nginx 镜像里默认网页根目录是 /usr/share/nginx/html
# 把宿主机的 /opt/nginx/html 挂过去
docker run -d \
-p 8080:80 \
-v /opt/nginx/html:/usr/share/nginx/html \
--name site \
--restart always \
nginx
第 3 步:验证
# 访问,能看到你写的中文页面
curl http://127.0.0.1:8080
# 输出里包含 "Hello, Docker!"
第 4 步:改文件,不重建容器,立即生效
# 改宿主机里的文件(加一行)
sudo bash -c 'echo "<p>—— 刚刚加的,秒生效!</p>" >> /opt/nginx/html/index.html'
# 再访问,立刻看到新内容(不用 restart,不用重建容器)
curl http://127.0.0.1:8080
第 5 步:删容器,数据还在
# 删掉容器
docker rm -f site
# 宿主机上的文件还在
ls /opt/nginx/html/
# index.html 还在
# 重新起容器,挂回同一目录,网站秒恢复
docker run -d -p 8080:80 -v /opt/nginx/html:/usr/share/nginx/html --name site nginx
curl http://127.0.0.1:8080 # 内容还在!
8.4 两种挂载方式对比
| 方式 | 写法示例 | 数据实际存哪 | 特点 |
| 绑定挂载(bind mount) | -v /opt/nginx/html:/usr/share/nginx/html | 你指定的宿主机目录 | 直观,宿主机上能看到文件,适合静态资源/配置文件 |
| 命名卷(named volume) | -v mydata:/var/lib/mysql | Docker 管理的路径(/var/lib/docker/volumes/...) | 由 Docker 统一管理,适合数据库(性能/权限更省心),宿主机上不好直接看 |
查看命名卷在宿主机的真实位置:
docker volume inspect mydata
# 看 "Mountpoint" 字段
8.5 数据持久化心法
凡是「容器删了你还想要的东西」(数据库、上传的文件、日志、配置),一律 -v 挂出来。
凡是「镜像自带、改了就重建镜像的」(程序本体、依赖库),别挂。
—
九、Dockerfile 最基础:FROM / RUN / COPY / CMD
前面一直在「用别人造好的镜像」,这一节教你自己造一个。
9.1 Dockerfile 是什么
Dockerfile 就是一个纯文本文件,一行一条指令,告诉 Docker「怎么一步步构建出一个镜像」。你可以把它理解为镜像的「配方/流水线」。
Dockerfile(配方) --docker build--> 镜像(成品) --docker run--> 容器(运行的实例)
9.2 四个最核心的指令(先只记这四个)
| 指令 | 一句话含义 | 类比(做菜) |
FROM 镜像 | 基于哪个基础镜像开始构建(必须是第一行) | 「以 XX 面为底」 |
RUN 命令 | 构建时在镜像里执行一条 shell 命令(装软件、建目录……) | 「往面里加料、和面、发酵」 |
COPY 源 目标 | 把你本机的文件拷进镜像 | 「把你做的馅料包进去」 |
CMD [...] | 容器启动时默认执行的命令(可被 run 覆盖) | 「出锅后默认怎么吃」 |
补充两个常用但不必深究的:
| 指令 | 含义 |
WORKDIR /path | 设置工作目录(后续 RUN/COPY 的相对路径基于它) |
EXPOSE 80 | 声明容器会用到哪个端口(只是声明,不真正映射,真正映射靠 -p) |
9.3 完整例子:把本地 index.html 打包成 nginx 镜像
场景:你本地写好了一个 index.html,想把它「焊死」进一个 nginx 镜像,以后用这个镜像起容器,页面就自带了。
第 1 步:建一个工作目录
mkdir -p ~/docker-site && cd ~/docker-site
第 2 步:写 index.html
cat > index.html <<'EOF'
<!DOCTYPE html>
<html lang="zh">
<head><meta charset="utf-8"><title>Dockerfile 实战</title></head>
<body><h1>这个页面是被 COPY 进镜像的!</h1></body>
</html>
EOF
第 3 步:写 Dockerfile(核心!)
cat > Dockerfile <<'EOF'
# 1. 基础镜像:从官方 nginx 开始(拉 nginx:1.27 作为底子)
FROM nginx:1.27
# 2. 把本机的 index.html 拷进镜像的网页根目录
# (覆盖掉镜像里自带的默认欢迎页)
COPY index.html /usr/share/nginx/html/index.html
# 3. 声明容器会用到 80 端口(文档作用,真正映射靠 -p)
EXPOSE 80
# 4. 容器启动时默认执行的命令
# (nginx 官方镜像的默认 CMD 就是启动 nginx,这里显式写出来教学)
CMD ["nginx", "-g", "daemon off;"]
EOF
逐行拆解:
| 行 | 指令 | 作用 |
FROM nginx:1.27 | FROM | 以 nginx:1.27 为基础镜像,所有后续操作都基于它 |
COPY index.html /usr/share/nginx/html/index.html | COPY | 把构建上下文(Dockerfile 所在目录)里的 index.html 拷到镜像内 /usr/share/nginx/html/ |
EXPOSE 80 | EXPOSE | 声明 80 端口(提示使用者,不强制) |
CMD ["nginx", "-g", "daemon off;"] | CMD | 容器启动时运行 nginx,且 daemon off; 让它在前台跑(容器要求主进程在前台,否则容器会立刻退出) |
⚠️ 两个新手必踩的坑:
COPY 的源路径是相对于 Dockerfile 所在目录的,index.html 必须和 Dockerfile 在同一个目录下,否则 build 报错 file not found。
- 容器里的主进程必须在前台运行,否则「主进程一退出容器就停」。
nginx 默认是守护进程(后台),所以要加 daemon off;。
第 4 步:构建镜像
# 在 Dockerfile 所在目录执行
# -t 给镜像打标签:名字叫 my-site,版本 v1
docker build -t my-site:v1 .
逐字拆解:
| 部分 | 含义 |
build | 从 Dockerfile 构建镜像 |
-t my-site:v1 | --tag:给构建出的镜像起名 my-site,标签 v1 |
. | 构建上下文(context):当前目录。Docker 会把整个目录打包发给守护进程,COPY 只能从这个目录里取文件 |
构建过程你会看到分层的输出:
Step 1/4 : FROM nginx:1.27
---> xxxxxxx
Step 2/4 : COPY index.html /usr/share/nginx/html/index.html
---> yyyyyyy
Step 3/4 : EXPOSE 80
---> zzzzzzz
Step 4/4 : CMD ["nginx","-g","daemon off;"]
---> [Building] aaaaaaa
Successfully built aaaaaaa
Successfully tagged my-site:v1
第 5 步:用刚构建的镜像起容器
docker run -d -p 8080:80 --name my-site my-site:v1
# 访问,看到你被 COPY 进去的页面
curl http://127.0.0.1:8080
# 输出包含 "这个页面是被 COPY 进镜像的!"
第 6 步:验证镜像里确实有你的文件
# 进容器看文件
docker exec my-site cat /usr/share/nginx/html/index.html
# 能看到你写的 HTML 内容
9.4 build 的小知识:构建缓存
Docker 是分层构建的,每条指令一层。如果某一层没变(比如你只改了 COPY 之后的东西),之前的层直接复用缓存,构建飞快。所以指令顺序讲究:
# 好习惯:变化少的放前面,变化多的放后面
FROM nginx:1.27
RUN apt-get update && apt-get install -y xxx # 很少变,靠后变化少时命中缓存
COPY index.html /usr/share/nginx/html/ # 经常变,放后面
CMD ["nginx","-g","daemon off;"]
原则:把「容易变」的指令放后面,这样改一点东西不至于全部重新构建。
9.5 五个指令的分工一句话总结
| 指令 | 谁执行 | 什么时候 |
FROM | 指定起点 | 构建时(声明) |
RUN | 在镜像构建时执行 | build 阶段 |
COPY | 把文件拷进镜像 | build 阶段 |
CMD | 容器启动时的默认命令 | run 阶段 |
EXPOSE | 声明端口 | 构建时(文档) |
记忆口诀:build 时干活的用 RUN/COPY,跑起来执行的用 CMD,定起点的用 FROM。
—
十、常见坑与避坑表
新手 99% 的问题都能在下表里找到。遇到报错先对号入座。
| # | 现象 / 报错 | 原因 | 解法 |
| 1 | Error: ... port is already allocated / bind: address already in use | 端口被占(宿主机已有服务,或之前的容器没删干净) | sudo lsof -i:8080 或 sudo ss -lntp \| grep 8080 找出占用者;换端口 -p 8081:80,或 docker rm -f 旧容器 |
| 2 | Error response from daemon: pull access denied / 拉镜像超时 / connection reset by peer | 网络问题 / Docker Hub 访问慢(国内常见) | 配镜像加速(第七节);换网络/代理;docker pull 重试;或离线 save/load 搬运 |
| 3 | Permission denied(容器内写文件被拒) | 容器内进程用户(如 nginx 的 nginx 用户)对挂载目录没写权限 | 宿主机目录 chmod 给权限,或 chown 给对应 UID;或 -v 目录:/path 时确认目录属主;数据库类可加 --user 调整 |
| 4 | 容器起不来 / 起来就 Exited (1) | 启动命令报错、配置错误、端口冲突、健康检查失败 | 第一招永远是 docker logs 容器名 看报错;再看 docker inspect 里的 Error 字段 |
| 5 | 容器数据丢了(删容器后数据库/文件没了) | 数据没挂卷,存在容器临时层里 | 所有要保留的数据用 -v 挂出来(第八节);已丢的只能从备份恢复 |
| 6 | docker ps 里找不到刚 run 的容器 | 容器已退出(ps 只显示运行中的) | 用 docker ps -a 看全部,docker logs 查为啥退了 |
| 7 | curl http://127.0.0.1:8080 打不开,但容器是 Up 状态 | 端口映射写反 / 容器内服务没监听该端口 / 防火墙 | 检查 -p 顺序(宿:容);docker exec 容器名 ss -lntp 看服务实际监听;查宿主机防火墙 |
| 8 | docker: permission denied while trying to connect to the Docker daemon socket | 当前用户不在 docker 组 | sudo usermod -aG docker $USER 然后重新登录(第三节) |
| 9 | docker build 报 COPY failed: file not found | COPY 的文件不在 Dockerfile 同级目录 / 被 .dockerignore 忽略 | 确认文件路径;检查 .dockerignore;COPY 源是相对「构建上下文」(docker build . 的那个 .) |
| 10 | 容器里时区不对、日志时间差 8 小时 | 默认 UTC 时区 | run 时加 -e TZ=Asia/Shanghai;或在 Dockerfile 里 RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime |
| 11 | docker rm 删不掉,报 device or resource busy | 容器还在运行 | 先 docker stop,或 docker rm -f 强制删 |
| 12 | 磁盘越来越满 | 悬空镜像、停止的容器、日志、构建缓存堆积 | docker system df 查占用;docker system prune(注意别带 --volumes);定期清理 |
| 13 | latest 镜像「突然变了」,线上行为不一样 | latest 标签被上游更新了,你重新 pull 拉到了新版本 | 生产环境锁定具体版本号(第七节 7.4) |
| 14 | sudo docker 创建的文件,普通用户改不了 | 用 sudo 起的容器,文件属主是 root | 少用 sudo docker,把用户加进 docker 组;或 chown 修正 |
| 15 | 容器内访问外网/宿主机服务连不上 | 容器网络隔离 | 宿主机服务用 --network host 或映射端口;Linux 访问宿主机可用 host.docker.internal(部分发行版需配 extra_hosts) |
10.1 万能排错三板斧
遇到任何「容器不对劲」,按顺序来:
# 第一斧:看日志(80% 的问题这里就有答案)
docker logs --tail 100 -f 容器名
# 第二斧:看容器详情(状态、退出码、挂载、网络、Error 字段)
docker inspect 容器名
# 第三斧:进去看(容器还在跑时,进到里面亲自检查)
docker exec -it 容器名 sh
# 进去后:ps aux(进程在不在?) / cat 配置文件 / 手动跑启动命令看报错
退出码速查:Exited (0) = 正常退出;Exited (1) = 一般错误;Exited (137) = 被 OOM 或 kill -9;Exited (139) = 段错误/崩溃。看到非 0 就去看 logs。
—
结语
把全文浓缩成一张「心智地图」,记住它就够用了:
┌──────────────┐
│ 痛点:环境不一致 │
└─────────────┘
▼
┌──────────────┐ 构建 ┌──────────────┐
│ Dockerfile │ ────────► │ Image 镜像 │
│ (配方) │ build │ (只读模板) │
└──────────────┘ └──────┬───────┘
│ run
▼
┌──────────────┐ pull/push ┌──────────────┐
│ Registry 仓库 │ ◄─────────── │ Container 容器 │
│ (镜像商店) │ │ (活的进程) │
└──────────────┘ └──────────────┘
│ -v 挂载
▼
宿主机目录(数据持久化)
最后送你一句可以贴在屏幕上的话:
镜像是模板,容器是运行,仓库是商店,Dockerfile 是配方;
数据要留命,就给它 -v;服务要常在,就给它 --restart always;
出了问题别慌,logs、inspect、exec 三板斧。
祝你玩得开心,早日告别「在我机器上能跑」。
(完)