>

本文面向完全零基础的读者:没装过 Linux、没写过一行配置、甚至没听过「容器」这个词都没关系。跟着敲一遍命令,你就能从零把 Docker 跑起来,并且真正看懂 docker run 后面每一个参数的含义。

> >

全文约定:命令前带 $ 表示在终端输入的内容;# 开头的是注释;文中所有命令都可以直接复制粘贴执行。


目录


一、什么是 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)❌ 是个存储/分发服务
DockerfileDockerfile配方说明书(一步一步教你怎么做模具)一个文本文件,描述如何「构建」出一个镜像❌ 是个文本文件

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/mysqlDocker 管理的路径(/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.27FROM以 nginx:1.27 为基础镜像,所有后续操作都基于它
COPY index.html /usr/share/nginx/html/index.htmlCOPY把构建上下文(Dockerfile 所在目录)里的 index.html 拷到镜像内 /usr/share/nginx/html/
EXPOSE 80EXPOSE声明 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% 的问题都能在下表里找到。遇到报错先对号入座。

#现象 / 报错原因解法
1Error: ... port is already allocated / bind: address already in use端口被占(宿主机已有服务,或之前的容器没删干净)sudo lsof -i:8080 或 sudo ss -lntp \| grep 8080 找出占用者;换端口 -p 8081:80,或 docker rm -f 旧容器
2Error response from daemon: pull access denied / 拉镜像超时 / connection reset by peer网络问题 / Docker Hub 访问慢(国内常见)配镜像加速(第七节);换网络/代理;docker pull 重试;或离线 save/load 搬运
3Permission denied(容器内写文件被拒)容器内进程用户(如 nginx 的 nginx 用户)对挂载目录没写权限宿主机目录 chmod 给权限,或 chown 给对应 UID;或 -v 目录:/path 时确认目录属主;数据库类可加 --user 调整
4容器起不来 / 起来就 Exited (1)启动命令报错、配置错误、端口冲突、健康检查失败第一招永远是 docker logs 容器名 看报错;再看 docker inspect 里的 Error 字段
5容器数据丢了(删容器后数据库/文件没了)数据没挂卷,存在容器临时层里所有要保留的数据用 -v 挂出来(第八节);已丢的只能从备份恢复
6docker ps 里找不到刚 run 的容器容器已退出(ps 只显示运行中的)用 docker ps -a 看全部,docker logs 查为啥退了
7curl http://127.0.0.1:8080 打不开,但容器是 Up 状态端口映射写反 / 容器内服务没监听该端口 / 防火墙检查 -p 顺序(宿:容);docker exec 容器名 ss -lntp 看服务实际监听;查宿主机防火墙
8docker: permission denied while trying to connect to the Docker daemon socket当前用户不在 docker 组sudo usermod -aG docker $USER 然后重新登录(第三节)
9docker build 报 COPY failed: file not foundCOPY 的文件不在 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
11docker rm 删不掉,报 device or resource busy容器还在运行先 docker stop,或 docker rm -f 强制删
12磁盘越来越满悬空镜像、停止的容器、日志、构建缓存堆积docker system df 查占用;docker system prune(注意别带 --volumes);定期清理
13latest 镜像「突然变了」,线上行为不一样latest 标签被上游更新了,你重新 pull 拉到了新版本生产环境锁定具体版本号(第七节 7.4)
14sudo 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 三板斧。

祝你玩得开心,早日告别「在我机器上能跑」。

(完)

— 感谢阅读 —

一起交流

分享你的思考,让讨论更进一步。