🟡 进阶

Docker 怎么用

把你写好的项目装进一个"盒子",让它在任何人、任何机器上跑出一样的结果——从怎么装、怎么打包,到怎么把这个盒子送到别人手上。

📅 更新于 2026-09-16

本文校对于 Docker Engine @ 29.8.1(2026-09-15 发布)· Docker Desktop @ 4.91.0 · 官方源:docs.docker.com

5
种装机方式
8
条分享渠道
10
个必踩的坑
2
秒记住端口方向

0030 秒速览:三句话、一张处境表、两个数字

读完这一篇,你能干成三件具体的事:在自己机器上装好 Docker 并能跑起来、把自己写的项目打包成一个可以到处搬的镜像、以及把这个镜像用四种方式之一送给别人——包括"他那边一条命令就跑起来"的那种。它不教你 Linux 系统管理,也不讲 Kubernetes(管理成百上千个容器的另一套工具,跟本文不是一回事)。

🔑 先记住三句话(后面全是细节)

① 镜像(image)是那个盒子,容器(container)是盒子被打开后正在跑的那个东西。盒子是锁死的、只读的,改不了;每次跑都从盒子新开一个(01)。② 你要送出去的从来不是"你的项目",而是这个盒子——所以先得写一份叫 Dockerfile 的说明书,让 Docker 按它把项目装进盒子(04)。③ 送出去有四条路:推到"镜像仓库"(registry,公共或私有)、存成一个 tar 文件发过去、把源码和说明书给他让他自己打包、以及多建几种"机型"的盒子让 Intel 和苹果芯片都能跑(06)。

按你现在的处境,直接跳到那一节

你现在的状态直接看这里看完你会知道
完全没装过,只想先装上02 怎么装Mac / Windows / Linux 各一条路,以及 Docker Desktop 要不要钱
装好了,想先看它能干什么03 先跑起来一条 docker run 里每个参数是什么,以及"跑起来了但浏览器打不开"怎么破
我有一个自己的项目,想打包它04 打包 → 05 运行一份能直接改的 Dockerfile,以及镜像从 400 MB 瘦下来的办法
我想把它给同事 / 客户 / 朋友06 分享 → 07 对方怎么做四条路各自的命令,和"对方那边到底敲什么"
对方是苹果芯片,我是 Intel(或反过来)06.5 跨芯片为什么跑不起来,以及一条 --platform 命令解决
跑出错来了09 排错对照表照着报错原文找原因

先记住两个数字

3.7 MBalpine 镜像(一个迷你 Linux 的全部)
约 400 MBpython:3.14 默认标签——因为里面塞了一整套编译工具
46 MB换成 python:3.14-slim 之后的体积
100 次 / 6 小时没登录 Docker Hub 时,同一个 IP 的拉取上限

这四个数字解决新手最常见的两个恐慌:"Docker 是不是很占磁盘"(看第一个),以及"我拉镜像怎么被限速了"(看第四个)。镜像体积的更多数字与出处见 04.6。

01它是什么、解决什么问题:从一个你肯定遇到过的场景说起

先看现场:为什么"在我电脑上能跑"是一句废话

你写了一个小网站,在自己电脑上跑得好好的。你把它发给朋友——他那边报错。你们对着屏幕排查两小时,最后发现是:他的 Python 是 3.9,你的是 3.13;他少装了一个包;你代码里写了一个只在 Mac 上存在的路径。

这不是谁水平不行,这是"环境"这件事本身的问题。你的项目要跑起来,除了代码,还依赖一大堆代码之外的东西:语言版本、依赖包、系统库、环境变量、目录路径。这些加起来叫运行环境。

传统解法是"写一份安装文档,让对方照着配"。问题是:文档会过期、机器会不一样、没人愿意照着一份二十步的文档配环境。Docker 的思路是换个方向——既然环境太难复制,那就把环境和代码一起打包,做成一个能整体搬运的东西。

盒子、正在跑的盒子,和一句话的官方定义

Docker 把项目打包成的那个东西叫镜像(image)。官方对它的定义是一句值得背下来的话:

📖 官方原话

"An image is a read-only template with instructions for creating a Docker container."——镜像是一份只读的模板,里面带着"怎么造出一个容器"的说明。(docs.docker.com/get-started/docker-overview)

这句话里最该抓住的词是 read-only(只读)。镜像做好之后就锁死了,你没法"进去改一改"——想改只能重新做一份新的。而容器(container)是另一回事,官方的定义是 "A container is a runnable instance of an image"——容器是一个镜像跑起来的实例。

打个比方(这个比方后面会用到):

⚠️ 这个比方到哪里就不准了

装系统要几十分钟、几十 GB;起一个容器常在 1 秒内完成,因为容器不是又装了一套操作系统(下面马上讲)。另一个不准的地方:装了系统的机器关机后里面的文件还在,而容器被删掉时,里面的改动会一起消失——除非你提前把它接到外面(见 05.2)。这两个差异都是新手最容易踩的坑,先记着。

它不是虚拟机——四个最常见的错想,一次破掉

"Docker 就是虚拟机吧?"这是新手最常见的理解,也是最诱人的一个误解:两者都给人"隔离出一个独立环境"的感觉,而且都要求你先装一个不小的软件。但它们在原理上完全不同。

虚拟机(VM)容器(Docker)为什么这个差别对你重要
里面装了什么一整套操作系统:自己的内核、驱动、系统程序只是一个被隔离起来的进程,共用你本机的操作系统内核决定了容器启动是"秒级"而不是"分钟级"
体积官方说法:几十 GB("tens of GBs")官方说法:镜像通常是几十 MB 级("tens of MBs")决定了"多带几个盒子"是不是负担
能不能换系统内核能,Linux VM 里能跑另一个版本的内核不能,容器和宿主机共用内核这就是为什么 Mac/Windows 上必须有个 Linux 虚拟机打底(02)
隔离强度硬件级,更硬进程级,更轻决定了"能不能拿它当安全沙箱"的答案:不要(见下面第 ④ 条)

官方对这条边界的表述很直接(docs.docker.com):"If you run multiple containers, they all share the same kernel, allowing you to run more applications on less infrastructure."——多个容器共用同一个内核,所以能在更少的硬件上跑更多应用。

顺着"共用内核"这条线,四个错想就都能破解了:

错想 ①:容器是一个小虚拟机,里面有完整的 Linux。
不是。容器里只有你要的程序和它需要的那些文件,内核用的是你本机的。推论:你在容器里 uname -r 看到的是宿主机的内核版本,别拿它判断"容器里装的是哪个 Linux 版本"。
错想 ②:镜像是一个压缩包,里面是"我的项目文件"。
不止。镜像里装的是一整棵文件系统树:一个精简的 Linux(比如 Alpine 那 3.7 MB)、你的代码、依赖包、以及"启动时该跑哪条命令"的说明。所以你没法"解开压缩包看看我的文件在哪"——要用 docker run -it 镜像 sh 进去看(03.5)。
错想 ③:容器里的东西改了,镜像也跟着改了。
镜像只读,容器里改的只是容器自己那一层。而且删掉容器时,这一层就没了。官方原话:"When a container is removed, any changes to its state that aren't stored in persistent storage disappear."——没存到持久存储里的改动,会随容器一起消失。(docs.docker.com)
错想 ④:容器隔离得很好,所以往里面跑不明来路的东西很安全。
别这么用。容器是和宿主机共用内核的进程级隔离,官方定位是"cost-effective alternative to hypervisor-based virtual machines"(比虚拟机省钱的替代品),不是安全边界。要跑可疑程序,那还是虚拟机或一次性云主机合适。

"装 Docker"到底装了什么:一个客户端,一个干活的

你敲的 docker 命令本身不会打包、不会运行任何东西——它只是把你的话转述给一个在后台常驻的"干活程序"。这个结构叫客户端-服务端(client-server):

docker
客户端(你敲的命令)
→
dockerd
守护进程(真正干活的)
→
镜像 / 容器 / 卷 / 网络
它管理的对象

官方对这个"干活的"有个名叫 dockerd 的进程(daemon,守护进程:在后台一直跑着、不跟你直接打交道的程序),它 "listens for Docker API requests and manages Docker objects",也就是接收请求、管理镜像和容器这些对象。

为什么这点值得单独讲?因为新手最常见的一类报错——Cannot connect to the Docker daemon——意思就是"客户端还在,但干活的没在跑"。你敲的命令没错,是后台那个程序没起来。记住这个结构,这类报错以后你会自己判断。

验证这个结构最直接的一条命令是 docker version,它会把 Client 和 Server 两段分别打出来:只看得到 Client 段、Server 段报错,说明客户端在、后台没起来。而 docker --version 只打一行客户端版本号,看不出后台死没死。

那 Docker Desktop 又是什么?

你在 Mac 或 Windows 上装的那个带图形界面的大块头,叫 Docker Desktop。它不是"另一个 Docker",而是一个把整套东西打包在一起的安装包。看它的官方发布说明最清楚(Docker Desktop 4.91.0,2026-09-14)里面装的是:

里面的组件版本(Desktop 4.91.0 里)它是干嘛的
Docker Enginev29.8.0那个真正干活的(含 dockerd)
Docker CLI随附你敲的 docker 命令
Docker Composev5.5.1一条命令起多个容器(05.3)
Docker Buildxv0.37.0负责"照着 Dockerfile 造镜像"
containerdv2.3.4再下面一层真正拉起容器的运行时

在 Mac 和 Windows 上,这个包里还额外含一个小型的 Linux 虚拟机。原因就是上面说的"共用内核":Mac 和 Windows 的内核不是 Linux,跑不了 Linux 容器,所以必须在中间垫一个真的 Linux。你平时感觉不到它的存在,但下面几件事都是它的表现:

✅ 一句话分清三个名字

Docker Engine = 服务端 + 客户端 + API 的那套核心(Linux 上你装的就是它);Docker Desktop = 给桌面用户的一站式安装包(含 Engine、Compose,Mac/Win 上还含一台 Linux 虚拟机);Docker Compose = 其中一个组件,管"一条命令起一整套服务"。

02怎么装:五种装法,先对号入座

Docker 的安装方式之所以看起来乱,是因为它在不同平台上装的根本不是同一个东西:Mac 和 Windows 上装的是 Docker Desktop(内含引擎),Linux 服务器上装的是 Docker Engine 本体。先看这张表,再往下找你那一行。

你在什么机器上该装哪个为什么是它跳到
Mac(Apple 芯片或 Intel)Docker DesktopMac 内核不是 Linux,必须有个小虚拟机打底;官方也只在 Mac 上提供 Desktop 这条路02.1
Windows 10/11(专业版及以上)Docker Desktop + WSL2同上;Home 版只能用 WSL2 后端跑 Linux 容器02.2
Ubuntu / Debian(尤其服务器)官方 apt 仓库装 docker-ce服务器不需要图形界面,直接装引擎本体,最干净02.3
Fedora / RHEL / Rocky官方 dnf 仓库同上,只是包管理器换了一个02.3
就想在随便一台测试机上先试试官方便捷脚本一条命令装全部,但官方明说不推荐用于生产02.4
没有包管理器的极简系统静态二进制官方定位"主要适合测试",没有自动安全更新02.5
不想让后台程序拿着 root 权限rootless 模式安全增强,适合单人工作站或共享机器02.6

02.1 Mac:Docker Desktop,两种装法

装法一:下载 .dmg 图形安装(官方文档给的路径)。官方页面提供两个独立下载:一个是给 Apple 芯片(M 系列)的,一个是给 Intel 芯片的——下错了不是装不上,是会跑得很难受。装法就是双击 Docker.dmg,把 Docker 图标拖进 Applications 文件夹。

装法二:Homebrew,一条命令:

brew install --cask docker-desktop
⚠️ 这条 brew 命令的来源要说清

Docker 官方安装页通篇没有 brew 命令(只给 .dmg)。上面这条来自 Homebrew 官方的 cask 页,那条 cask 还额外写明了要求 macOS >= 14;而 Docker 官方只写"支持当前及前两个主要版本"、从没给过具体数字。两者在 2026-09 事实上吻合,但你要知道数字的出处是 Homebrew 而不是 Docker。

另外两条官方给出的硬要求:至少 4 GB 内存;Apple 芯片上 Rosetta 2 现在是"推荐"而非必需("no longer strictly required")。手动装可以用 softwareupdate --install-rosetta。

✅ 装完第一件事:把它打开

官方安装步骤里明确要求安装后启动 Docker.app 并接受订阅协议。Mac 上"命令没问题但连不上后台",九成是因为这一步没做或后来退出了。

02.2 Windows:Docker Desktop,先看清门槛

Windows 上的要求比 Mac 具体得多,而且版本不对是装不上的。官方列出的门槛(docs.docker.com):

装法:官方给的是安装器(Docker Desktop Installer.exe)与 Microsoft Store。命令行静默安装的写法是:

"Docker Desktop Installer.exe" install --user

如果你习惯用 winget,包标识符是 Docker.DockerDesktop:

winget install -e --id Docker.DockerDesktop
⚠️ winget 这条也来自非官方文档

Docker 官方 Windows 安装页没有写 winget。这个包标识符来自微软官方的 winget-pkgs 仓库清单(当前清单里是 4.91.0)。顺带提醒:有些第三方 winget 镜像站还停在 4.17.0,别拿那种页面上的版本号当依据。

还有一条容易忽略的规则:要用原生 Windows 容器,必须用 all-users 模式安装("Windows containers are only supported in all-users installation mode."),也就是安装时不能加 --user。默认模式跑的是 Linux 容器。

02.3 Linux:官方 apt / dnf 仓库,命令全给你

Linux 上装的是 docker-ce——Docker 官方仓库里那个引擎包。注意一个陷阱:Ubuntu 自带的仓库里也有个叫 docker.io 的包,官方把它归为 "unofficial packages",装官方仓库之前要先卸掉,否则会打架。

Ubuntu / Debian(现代 deb822 写法)

官方文档现在用的是 /etc/apt/sources.list.d/docker.sources 这种新格式,不再是老教程里那行 docker.list。以下整段可直接复制:

# 1. 先卸掉发行版自带的、会和官方包打架的那些
sudo apt remove $(dpkg --get-selections docker.io docker-compose docker-compose-v2 \
  docker-doc docker-buildx podman-docker containerd runc | cut -f1)

# 2. 装基础工具,把官方签名密钥放好
sudo apt update
sudo apt install ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

# 3. 加官方仓库(Ubuntu;Debian 把 ubuntu 换成 debian,Suites 那行用 $VERSION_CODENAME)
sudo tee /etc/apt/sources.list.d/docker.sources <<EOF
Types: deb
URIs: https://download.docker.com/linux/ubuntu
Suites: $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}")
Components: stable
Architectures: $(dpkg --print-architecture)
Signed-By: /etc/apt/keyrings/docker.asc
EOF

# 4. 装
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

注意最后一行五个包的分工:docker-ce 是引擎本体,docker-ce-cli 是你敲的命令,containerd.io 是更下层拉起容器的运行时,后两个分别是 buildx(造镜像)和 compose(多容器)插件。少装后两个,你后面敲 docker compose 会报"不是已知命令"。

Debian 用户只需把 URIs 的 ubuntu 改成 debian;官方另提醒:在 Debian testing 或 Kali 这类衍生版上,要把 Suites 里的变量表达式换成字面的版本代号(例如 trixie)。

Fedora / RHEL

# Fedora
sudo dnf config-manager addrepo --from-repofile https://download.docker.com/linux/fedora/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker

# RHEL / Rocky(注意 config-manager 的参数写法与 Fedora 不同)
sudo dnf -y install dnf-plugins-core
sudo dnf config-manager --add-repo https://download.docker.com/linux/rhel/docker-ce.repo
sudo dnf install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo systemctl enable --now docker

出现签名确认时,官方给的指纹是 060A 61C5 1B55 8A7F 742B 77AA C52F EB6B 621E 9F35。

02.4 便捷脚本:快,但官方明说不推荐生产用

curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh

脚本头部注释里列了它的问题,逐条都值得看:不推荐用于生产环境("This script is not recommended for production environments.")、会不经确认就装依赖、不负责升级已有安装。它支持 Ubuntu/Debian、CentOS/Fedora/RHEL/Rocky、SLES 这些发行版,遇到已经停止维护(EOL,即官方不再发安全更新)的版本会直接拒绝。

✅ 什么时候用它合适

个人测试机、一次性虚拟机、CI 里临时起一个环境(CI = 每次提交代码后自动帮你跑构建和测试的那台服务器)。要是给别人维护的服务器,还是走 02.3 的仓库方式——因为仓库方式以后能用 apt upgrade 一起升级,脚本装的不能。

02.5 静态二进制:连包管理器都没有的时候

官方提供打包好的压缩包,当前版本的文件名形如 docker-29.8.1.tgz。官方文档没写死文件名格式(写的是 /path/to/<FILE>.tar.gz),这个格式是从下载目录里核实的。完整步骤:

curl -fsSL https://download.docker.com/linux/static/stable/x86_64/docker-29.8.1.tgz -o docker.tgz
tar xzvf docker.tgz
sudo cp docker/* /usr/bin/
sudo dockerd &
sudo docker run hello-world

看到 sudo dockerd & 了吗——这条方式只是临时把后台手动拉起来,官方页面根本没给注册成系统服务的步骤。加上没有自动安全更新,官方的定位是一句很直白的话:"mostly suitable for testing purposes"(主要适合测试)。

02.6 rootless 模式:不给后台 root 权限

前面 02.3 装完,你会发现自己老得敲 sudo(下面 02.8 讲怎么办)。那条路的代价是:谁进了 docker 组,谁就等于拿到了 root 权限——这是官方的原话("The docker group grants root-level privileges to the user.")。

rootless 模式("无 root 模式")是官方的替代方案:让后台程序和容器都以普通用户身份运行,装的过程中也不用 root。官方说明它 "executes the Docker daemon and containers inside a user namespace"(在用户命名空间里跑),目的是 "to mitigate potential vulnerabilities in the daemon and the container runtime"。安装一条命令:

dockerd-rootless-setuptool.sh install

如果提示找不到这个脚本,装一下附加包:sudo apt-get install -y docker-ce-rootless-extras。没有包管理器的机器可以用 curl -fsSL https://get.docker.com/rootless | sh。

装完还要设两个环境变量(脚本会提示你):

export PATH=/usr/bin:$PATH
export DOCKER_HOST=unix:///run/user/1000/docker.sock

前置条件里最容易漏的一个是:需要 newuidmap / newgidmap(通常在 uidmap 包里),以及 /etc/subuid、/etc/subgid 里给你的用户至少 65536 个从属 ID。

02.7 Docker Desktop 要不要钱?边界在这里

这是公司里用 Docker 时最现实的问题。官方许可页给的免费范围是四类,逐字如下:

需要付费的情形:更大的组织里的专业使用、政府机构(注意政府实体不在免费之列)、以及超出免费范围的商业使用。覆盖它的订阅是 Docker Pro / Team / Business。

订阅月付(每人每月)年付(每人每月)用户数什么时候你需要它
Personal$0$01个人学习、开源、小于门槛的公司
Pro$11$91个人商用,或想要更大的 Docker Hub 拉取额度
Team$16$15最多 100团队协作、集中管理
Business—$24不限需要 Hardened Docker Desktop 与 VDI 支持的企业

价格页面上写的是美元符号但没标币种,实际支付时以官方页面为准。Linux 服务器上装 Docker Engine 本身不涉及这套许可——收费针对的是 Docker Desktop 这个桌面产品。

02.8 装完怎么验:三条命令,分工不同

docker --version      # 只报客户端版本号
docker version        # Client 段 + Server 段;Server 报错 = 后台没起来
docker info           # 系统级信息:内核版本、容器数、镜像数、存储驱动、CPU/内存
docker run hello-world   # 官方的最小验证:拉一个几 KB 的镜像并跑一次

最后一条跑成功时,输出第一行是 Hello from Docker!,第二行是 This message shows that your installation appears to be working correctly.——这句话就是官方给"装好了吗"的答案。

"为什么我每条命令都要 sudo?"

原因官方写得很清楚:后台程序 "binds to a Unix socket, not a TCP port"(只在一个本地套接字上监听,不是网络端口),而且 "always runs as the root user"(永远以 root 身份跑)。所以普通用户默认没资格跟它说话。官方的解法是把你加进 docker 组:

sudo groupadd docker
sudo usermod -aG docker $USER
newgrp docker          # 让当前终端立刻生效;或者注销重登
docker run hello-world   # 不再需要 sudo 了
🚫 加进 docker 组 = 给这个人 root 权限

这不是夸张,是官方原话。所以:共享服务器上不要随便把人加进 docker 组;单人工作站上没问题。对安全更敏感的场景,用 02.6 的 rootless 模式。

如果你之前一直用 sudo 跑命令,之后可能会撞上 ~/.docker/ 目录的权限错误,官方给的修法是:

sudo chown "$USER":"$USER" /home/"$USER"/.docker -R
sudo chmod g+rwx "$HOME/.docker" -R

02.9 不用 Docker Desktop 的几条替代路(Mac 用户尤其值得看)

Docker Desktop 有许可限制、也比较吃资源,Mac 上很多人会换别的。这些工具的共同思路是:照样提供一台 Linux 虚拟机和兼容的 docker 命令,只是不带那个图形界面。

替代品安装命令它是什么你该在什么情况下考虑
Colimabrew install colima(另需 brew install docker)命令行式的轻量 Linux 虚拟机,支持 Docker/containerd 运行时只想要命令行、不想装图形界面;README 明说 Docker 客户端要另装
Podman Desktopbrew install --cask podman-desktop另一套容器引擎(Podman)的桌面工具,可跑无 root 容器组织里对 root 权限敏感时。注意官方文档把 Homebrew 这条路标为"不推荐",建议用 .dmg
OrbStackbrew install --cask orbstackMac 上的轻量容器/虚拟机替代,自称可直接替换 Docker Desktop嫌 Docker Desktop 慢、吃内存。它官网明确声明与 Docker 公司无关联
Limabrew install lima 然后 limactl startLinux 虚拟机的通用管理工具(CNCF 孵化项目),常被别的工具当底座你要的不只是容器,而是"一台能 ssh 进去的 Linux"
⚠️ 一张表里有两类来源,别混

上表里"它是什么"这类描述来自各项目自己的官网与仓库;brew install 这些命令来自 Homebrew 官方 cask 页。Docker 官方文档里没有这些命令,也没有推荐过任何替代品。版本层面:Colima 与 Lima 都是活跃更新的开源项目,Podman Desktop 的 Homebrew 安装会连带装上 Podman 引擎。

装错了会怎样:两个真实的坑

✗ 混装 docker.io 和 docker-ce

先 apt install docker.io,后来又按教程加了官方仓库装 docker-ce。两套包抢同一个可执行文件,之后升级、排错都会出现莫名其妙的版本错乱。

✓ 先卸干净再装官方仓库

按 02.3 第 1 步把 docker.io 等包先卸掉。顺带说:Ubuntu 24.04 自带的 docker.io 当前是 29.1.3,而官方仓库是 29.8.1——落后得不算离谱,但官方明确不为这个包背书。

✗ Linux 上同时开 Desktop 和 Engine

两个后台都在跑。官方建议用 Desktop 时先把 Engine 停掉,否则会出现资源争抢和 "port already in use" 这类难查的冲突。

✓ 用 context 切换,或者只用一套

Desktop 在 Linux 上用的是自己的 context(上下文,即"当前连哪个后台"的配置),默认叫 desktop-linux;docker context ls 看有哪些,docker context use default 切回 Engine。注意两边镜像和容器是不互通的——Desktop 存在自己虚拟机里的独立位置。

03先跑通:把别人的镜像跑起来

在打包自己的项目之前,先拿现成的镜像练手——这一步不需要你写任何东西,只需要理解一条命令。这条命令你理解透了,后面 80% 的 Docker 操作都是它的变体。

03.1 把一条 docker run 拆开看

下面这条是本文后面会反复出现的"标准长命令",先逐段拆一次:

docker run -d \
  --name my-web \
  -p 8080:80 \
  -v my-data:/app/data \
  -e NODE_ENV=production \
  --restart unless-stopped \
  nginx:alpine
这一段它的意思不写会怎样
docker run从镜像创建并启动一个容器(等于"创建 + 启动"两步合一)—
-d后台跑(detach),命令立刻返回,不占着你的终端前台占着终端,你一按 Ctrl+C 它就停
--name my-web给它起名,之后可以用名字操作它Docker 随机起一个名(形如 vibrant_cannon),你每次都得先 docker ps 查
-p 8080:80把你电脑的 8080 端口,接到容器里的 80 端口外面完全访问不到——容器默认不对外开任何端口
-v my-data:/app/data把一块叫 my-data 的存储挂到容器内的 /app/data容器删掉时里面的数据一起消失(05.2)
-e NODE_ENV=production给容器里的程序设一个环境变量(环境变量:程序启动时能读到的键值对配置)程序读不到配置,可能用默认值或直接报错
--restart unless-stopped意外退出时自动重启,但你主动停掉之后就不再拉起程序崩了就一直躺着
nginx:alpine用哪个镜像:名字 nginx + 冒号后的标签 alpine(标签 = 同一镜像的不同版本/变体)省略标签则默认 latest

03.2 端口方向:-p 8080:80 哪个在前?

这是新手第一个一定会背错的点。记住一个口诀就够了:像写信封一样,左边是"外面",右边是"里面"。官方文档的原文是 "The first 8080 refers to the host port. The second 80 refers to the container port."——第一个是你电脑(宿主机)的端口,第二个是容器里的端口。

✅ 30 秒记法

把这条命令读成一句话:"把这一面(主机)的 8080 号窗口,接到那一面(容器)的 80 号窗口。"所以浏览器里敲的是 http://localhost:8080,而不是 80。两个数字写反了程序不会报错,你只是永远连不上。

还有两个官方的补充规则值得一起记:不给 IP 时,Docker 默认在所有网卡上发布端口("publishes the port on all interfaces (address 0.0.0.0) by default");只写一个端口(如 -p 80)时,Docker 会在主机上随机挑一个端口——用 docker port 容器名 能查出来是哪个。

安全上还有一句官方提示:"Publishing container ports is insecure by default."——如果你只想让本机能访问,把 IP 写死:-p 127.0.0.1:8080:80。

03.3 "跑起来了,但浏览器打不开"——两个不同的原因

这是新手最经典的撞墙现场,而且有两个完全不同的原因,得分开查:

原因一:你没写 -p(外面这一侧不通)

官方原话:"By default, containers are secure and isolated from outside network traffic – they do not expose any of its ports by default."——容器默认不与外界通网络,一个端口都不开。所以你想从浏览器访问,必须显式写 -p。这一点也解释了一个常见困惑:Dockerfile 里写了 EXPOSE 3000 并不等于端口开了(04.3 会让很多人吃惊)。

原因二:程序自己只监听了"内线"(容器里面这一侧不通)

这个更隐蔽,因为它看起来完全正常:容器在跑、端口也映射了,但就是连不上。根因是容器里那个程序只监听了 127.0.0.1——而 127.0.0.1 的意思是"只接受来自我自己这台机器的连接"。在容器里,"我自己"就是那个容器,Docker 转发进来的流量对它来说是"外来者",直接被拒。

官方文档里最直接的一句出现在 Node.js 指南(讲调试端口时顺带说破):"Using 0.0.0.0 rather than localhost is necessary so the debugger is reachable from outside the container."——要用 0.0.0.0 而不是 localhost,才能从容器外面连进来。Flask 官方文档的说法是加 --host=0.0.0.0 后 "tells your operating system to listen on all public IPs"。

⚠️ 诚实说明:这是官方文档的空白区

Docker 官方文档没有系统性地讲解"Web 服务要监听 0.0.0.0"这件事,只在 Node 调试那一处顺带提了。上面的机制解释是把它和 Flask 官方文档的说法拼起来的结论。实战里最快的判断法是进容器看一眼它到底在听哪:

docker exec -it 容器名 ss -tlnp        # 列出容器里正在监听的端口和监听地址
docker exec -it 容器名 curl -s localhost:80   # 容器内自测:通 → 问题在映射,不通 → 问题在程序
docker port 容器名                    # 看主机端口到底映射到哪

03.4 日常看护:五条命令覆盖 90% 的情况

docker ps                 # 现在活着的容器
docker ps -a              # 包括已经退出的——排错第一步永远是它
docker logs -f my-web     # 跟着看日志(-f 表示持续输出)
docker stop my-web       # 优雅停机(先发 SIGTERM,默认等 10 秒再强杀)
docker rm my-web         # 删掉已停止的容器

三个容易忽略的细节:docker ps 默认只看运行中的,所以"我的容器不见了"的正确动作是 docker ps -a;docker logs 支持 --tail 100(只看最后 100 行)和 --since 42m(只看最近 42 分钟);docker stop 的超时对 Linux 容器默认 10 秒、Windows 容器 30 秒。

03.5 一个具体例子:跑一个网页服务器,然后进去看看

把前面学的一次串起来,从零开始跑一个 nginx(一个网页服务器软件):

# 1. 跑起来:本机 8080 → 容器 80,后台,起名叫 web
docker run -d --name web -p 8080:80 nginx:alpine

# 2. 确认它在跑
docker ps
# CONTAINER ID   IMAGE          STATUS         PORTS                    NAMES
# 3f2a1b9c8d7e   nginx:alpine   Up 3 seconds   0.0.0.0:8080->80/tcp    web

# 3. 浏览器打开 http://localhost:8080 —— 应该看到 nginx 欢迎页

# 4. 钻进容器里看看(-i 保持输入 -t 分配终端,合起来就是 -it)
docker exec -it web sh
# 现在你在容器内部了:
#   ls /usr/share/nginx/html     看到那个欢迎页文件
#   cat /etc/os-release          看到这是一个 Alpine Linux
#   exit                         退出(容器不会停)

# 5. 收工
docker stop web && docker rm web

第 4 步里有一个细节值得体会:exec 用的是 sh 而不是 bash。因为 Alpine 这种迷你 Linux 里根本没有 bash。官方文档的所有示例也都写 sh——这是很实用的一条经验:进不去容器时,先怀疑你要的 shell 不存在。

另外注意第 4 步和第 5 步的区别:exec 是在正在运行的容器里再开一个进程,退出它不影响容器;而 stop 是让容器停下来。

04打包自己的项目:Dockerfile 逐行拆

到这里正式进入本文的主线:把自己的项目做成一个能送人的镜像。要做的事只有两件——写一份说明书,然后让 Docker 照着它造。

04.1 说明书就是 Dockerfile,造的动作叫 build

这份说明书叫 Dockerfile(一个名字就叫这个的文件,没有后缀)。它是一份文本文件,每行一条指令,说的是:"从哪个底子开始、把哪些文件放进去、装什么、启动时跑什么。"

让 Docker 照着它造出镜像的动作叫 构建(build):

docker build -t myapp:1.0 .

这条命令里有三个东西必须看懂,否则后面会一直犯错:

部分意思为什么会写错
-t myapp:1.0给造出来的镜像打标签:名字 myapp,标签 1.0不打标签会得到一个没有名字的镜像(只能靠 ID 指它),也没法推送
末尾的 .构建上下文:告诉 Docker "把当前目录及以下的内容打包给构建器用"漏掉这个点是最常见的报错;它不是 Dockerfile 的位置
-f 路径(没写就用上下文里的 Dockerfile)指定用哪份说明书用 -f 只改说明书位置,不改上下文——这是"COPY 找不到文件"的头号原因
📖 构建上下文是什么,为什么它决定了哪些文件能用

官方定义:构建上下文是 "the set of files that your build can access"——你的构建过程能访问到的那些文件。Docker 会先把上下文打包送给构建器,然后 COPY 只能从这个包里拿东西。所以 COPY ../something 这种写法永远失败——上下文之外的路径根本不在包里。

04.2 不想从零写?docker init 能生成一份草稿

Docker 提供了一条生成说明书的命令:docker init,官方描述是 "Creates Docker-related starter files for your project"。它支持 ASP.NET Core、Go、Java、Node、PHP with Apache、Python、Rust,以及一个通用的 Other。生成四个文件:.dockerignore、Dockerfile、compose.yaml、README.Docker.md。

cd 你的项目
docker init        # 它会问你项目用什么语言、监听哪个端口
⚠️ 它只在 Docker Desktop 里能用

官方文档只有一句 "Docker Desktop provides the docker init CLI command."——提供者是 Docker Desktop。在纯 Docker Engine(Linux 服务器)或 Rancher Desktop / Podman 这类同类桌面工具下敲它,会得到 docker: 'init' is not a docker command。另外官方提醒它会覆盖已有文件且无法恢复("You can't recover overwritten files."),在有内容的目录里跑之前先确认一下。

它是很好的起点,但生成的草稿不等于理解的替代品——下面还是把每一行拆开讲,因为调错的时候你得知道该改哪。

04.3 最小例子:一份 Node.js 的 Dockerfile,逐行讲

假设你的项目长这样(一个简单的 Node 服务):

myapp/
├── package.json
├── package-lock.json
├── server.js
└── .dockerignore

在这份目录里新建一个名叫 Dockerfile 的文件,内容如下——这份可以直接抄,改掉端口和启动命令就行:

FROM node:24-alpine

WORKDIR /app

COPY package.json package-lock.json ./
RUN npm ci --omit=dev

COPY . .

EXPOSE 3000
CMD ["node", "server.js"]

再建一个 .dockerignore(下面 04.5 会给完整内容)。然后:

docker build -t myapp:1.0 .
docker run -d --name myapp -p 3000:3000 myapp:1.0

逐行解释这份说明书——每一行对应镜像里的一层,理解这点是理解缓存的前提:

行官方定义 / 它在做什么别当什么用 / 什么时候会写错
FROM node:24-alpine"initializes a new build stage and sets the base image"——从哪个底子开始。这里用的是 Node 24 的 Alpine 版(alpine 是个只有几 MB 的迷你 Linux)别以为它"装了一个操作系统给你改"——它是只读的起点。选底子直接决定最终体积(04.6)
WORKDIR /app把容器里的工作目录切到 /app,之后的 COPY/RUN/CMD 都在这个目录里发生目录不存在会自动创建。别用 RUN cd /app 代替它——那条只在那一条 RUN 里有效
COPY package.json package-lock.json ./把这两个文件从构建上下文复制进镜像的当前目录源路径相对构建上下文,不是相对 Dockerfile 所在目录
RUN npm ci --omit=dev在构建时执行命令,结果固化进镜像(这里装依赖,--omit=dev 表示不装开发依赖)和 CMD 别搞反:RUN 是造镜像时执行,CMD 是跑容器时执行。官方原话:"RUN actually runs a command and commits the result",而 CMD "doesn't execute anything at build time"
COPY . .把上下文里剩下的所有东西复制进 /app放在依赖安装之后是刻意的,见 04.7
EXPOSE 3000"informs Docker that the container listens on the specified network ports at runtime"——只是个声明重点:官方原话 "The EXPOSE instruction doesn't actually publish the port."——它不会真的开放端口,真正开放靠 docker run -p
CMD ["node","server.js"]容器启动时默认跑的命令,用于 "provide defaults for an executing container"一个 Dockerfile 里只能有一条 CMD;而且跑容器时你额外给参数,会覆盖掉它

CMD 和 ENTRYPOINT 到底差在哪

这是新手最容易混的一对,值得单独说清。区别不在"谁先谁后",而在用户给的参数是覆盖还是追加:

CMD:会被覆盖的默认值

镜像里写 CMD ["node","server.js"]。对方敲 docker run myapp:1.0 node other.js,实际跑的是 node other.js——CMD 被整个替换掉了。

ENTRYPOINT:会被追加的固定部分

镜像里写 ENTRYPOINT ["node"]。对方敲 docker run myapp:1.0 other.js,实际跑的是 node other.js——参数被追加在 ENTRYPOINT 后面,官方原话:"Command line arguments to docker run will be appended after all elements in an exec form ENTRYPOINT."

两个配套的坑也要知道:① 用 shell 形式写的 ENTRYPOINT 会忽略 CMD 和用户给的参数(官方原话:"ignores any CMD or docker run command line arguments"),所以想要"参数能追加"就必须用中括号那种 ["命令","参数"] 的写法(这叫 exec 形式)。② 想让 CMD 给 ENTRYPOINT 提供默认参数,两个都必须用中括号形式,官方原话是 "both the CMD and ENTRYPOINT instructions should be specified in the exec form"。

还有一个格式细节:中括号形式里必须用双引号,"you must use double-quotes around words, not single-quotes"——写成单引号不会报错,但参数会带上引号本身传进去,非常难查。

04.4 第二个例子:Python 项目

换成 Python,结构大同小异,关键是"先把依赖装上、再拷代码"这个顺序:

FROM python:3.14-slim

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

EXPOSE 8000
CMD ["python", "-m", "uvicorn", "app:app", "--host=0.0.0.0", "--port=8000"]

注意倒数第二行的 --host=0.0.0.0——这就是 03.3 讲的"必须监听 0.0.0.0 才能从外面连进来"。官方 Python 指南里的启动命令同样带着这个参数。

这里用的 python:3.14-slim 是"精简版"标签。为什么不用默认标签,看下面这张表就明白了。

04.5 镜像为什么动辄几百 MB(以及怎么瘦下来)

镜像标签压缩后体积为什么是这个数
alpine:latest约 3.7 MB一个为容器特制的迷你 Linux,连包管理器都是精简版
ubuntu:24.04约 29.8 MB完整的 Ubuntu 用户空间
python:3.14约 415 MB下面单独解释
python:3.14-slim约 46 MB去掉了一堆常用 Debian 包,只留跑 python 的最低需要
python:3.14-alpine约 20 MB换成 Alpine 打底,最小,但编译某些扩展会比较麻烦
node:24约 410 MB同上,基于 Debian + 常用工具
node:24-alpine约 58.6 MBAlpine 打底

数字来自 Docker Hub 的官方仓库页与它的接口(hub.docker.com/_/python),抓取日期 2026-09-16,都是压缩后的体积;解压到磁盘上会比这更大。为什么 python 默认标签这么胖?官方镜像页给了解释:它基于 buildpack-deps,而这个底子 "by design, has a large number of extremely common Debian packages"——故意塞了一堆常用包,目的是让基于它的镜像少装东西、整体更省。官方也承认:"This is an unfortunate side-effect of using the buildpack-deps image in the non-slim variants."

✅ 挑基础镜像的三句话

够用就好:纯运行用 -slim;要编译原生扩展可能得回默认标签。别为了小去换 Alpine 又撞上编译问题——体积省 30 MB 换来一下午,不值。体积自己量:docker image ls 看每个镜像的大小,docker image history 镜像名 看是哪一层占的。

04.6 .dockerignore:一个容易被跳过、但必须写的文件

它和 .gitignore 长得很像,作用不同:它决定哪些文件不进入构建上下文(也就是不发给构建器)。官方的理由是 "This helps avoid sending unwanted files and directories to the builder",从而加快构建。

一份可以直接用的 .dockerignore(内容合并自官方 Node 与 Python 两份指南):

node_modules/
dist/
.env
.env.*
.git
.gitignore
.DS_Store
npm-debug.log*
coverage/
db/
**/__pycache__
**/.venv
**/Dockerfile*
**/compose.y*ml
LICENSE
README.md

三条规则要知道:它在上下文根目录被查找;以 ! 开头表示"例外,要包含";最后一条匹配的规则说了算("the last line of the .dockerignore that matches a particular file determines whether it's included or excluded")。

🚫 .dockerignore 拦不住已经 COPY 进去的东西

它管的是"发给构建器的包裹里有没有这个文件"。如果 Dockerfile 里有 COPY . .,而你的 .env 没被 ignore 掉,那它就会进镜像——推送到公开仓库时连着密钥一起公开。这是本条最需要记住的后果。

04.7 为什么"我改了代码,镜像却没变"

先讲机制,因为它同时解释了"构建很慢"和"改了没生效"两件事。

Dockerfile 里每一行指令,都会在镜像里产生一层(官方原话:"Each instruction in this Dockerfile translates to a layer in your final image.")。Docker 会缓存这些层,规则是:某一层变了,它之后的所有层都要重做——"If a layer changes, all other layers that come after it are also affected."

把这句话套到我们的顺序上,就明白了为什么依赖安装要单独一行写在前面:

✗ 先拷全部代码,再装依赖
COPY . .
RUN npm ci

你只改了一个 server.js,COPY . . 这一层也变了,于是下面的 npm ci 必须重跑一遍。每次改代码都重装依赖。

✓ 先拷依赖清单,再装依赖
COPY package.json package-lock.json ./
RUN npm ci
COPY . .

只要依赖清单没变,装依赖那两层就一直命中缓存。改业务代码只重跑 COPY . . 这一层。这是 Dockerfile 里最值钱的一条经验。

还有一条会让人困惑的官方细节:改文件的修改时间不会让缓存失效——"The modification time of a file (mtime) is not taken into account",它看的是文件内容的校验和。而 RUN 那条更特别:"The cache for RUN instructions isn't invalidated automatically between builds"——它只看命令字符串是否一模一样。所以 RUN apt-get update 这种写法会一直用旧缓存,你以为更新了其实没有。

真到了需要彻底重来的时候:docker build --no-cache -t myapp:2.0 .。

04.8 多阶段构建:把"编译用的东西"留在门外

前端项目和编译型语言有一个共同问题:要造出成品,需要一整套工具链(编译器、构建工具、开发依赖);但跑起来的时候,这些工具一个都不需要。多阶段构建(multi-stage build)就是官方给的解法:在同一份 Dockerfile 里写多个 FROM,每个开一个新阶段,最后只把成品从前面阶段"拎"过去。

官方说法是它能让最终镜像里 "None of the build tools required to build the application are included in the resulting image"——构建用的工具一个都不会留在成品镜像里。

# ---- 第一阶段:builder —— 装全量依赖并编译 ----
FROM node:24-alpine AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build

# ---- 第二阶段:deps —— 只装生产依赖 ----
FROM node:24-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci --omit=dev

# ---- 第三阶段:runner —— 只带产物和运行时依赖 ----
FROM node:24-alpine AS runner
ENV NODE_ENV=production
ENV PATH=/app/node_modules/.bin:$PATH
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY --from=builder /app/dist ./dist
COPY package.json ./
EXPOSE 3000
CMD ["node", "dist/index.js"]

三段的关键在最后那三行 COPY --from=:从别的阶段(或别的镜像)里把文件搬过来。所以 runner 这一阶段里既没有编译工具,也没有开发依赖,只有"能跑起来的最小集合"。

给阶段起名字(AS builder)不只是为了好读,官方说它还带来一个好处:即使以后调整了指令顺序,"the COPY doesn't break"——用名字引用比用序号引用稳。

另一个实用点:可以用 --target 只构建到某个阶段,官方 Node 指南里的例子是这样的:

docker build --target builder -t myapp:dev .     # 只造到 builder,方便调试
docker build -t myapp:prod .                      # 造完整的(默认最后一个阶段)
docker image ls                                   # 自己量:两个镜像分别多大
⚠️ 别指望官方给你体积对比数字

Docker 官方文档从头到尾没有给过任何"多阶段能把镜像从 X MB 降到 Y MB"的数字——多阶段构建页、Node 指南、Python 指南都没有。本文也不编这个数。正确做法是自己量:用上面最后那条 docker image ls,把单阶段和多阶段的镜像都造一遍,比一比。你自己的项目省多少,只有你自己的项目知道。

04.9 到这里你手上有什么

走到这一步,你有了一个存在本机、有名字、有标签的镜像。用 docker images 能看到它:

docker images
# REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
# myapp        1.0       a1b2c3d4e5f6   5 seconds ago    62MB

但注意:这个镜像现在只有你自己看得见。它躺在你本机,别人拉不到、也拿不到。怎么送出去是第 06 节的事。中间还有一节——先把它跑起来跑对。

05把自己的镜像跑起来:数据、配置,和"一条命令起一整套"

05.1 最短的一步:跑你自己的镜像

docker run -d --name myapp -p 3000:3000 myapp:1.0
docker logs -f myapp

和 03 跑 nginx 完全一样,只是镜像名换成了你自己造的。如果你看到容器起来又立刻退出(docker ps -a 里状态是 Exited (0)),大概率是说明书最后那行 CMD 写的命令跑完就返回了——这是最常见的"启动即退出"原因,具体见 09 排错表。

05.2 数据会丢:卷(volume)解决什么问题

官方对这件事有一句反复出现的话:"When a container is destroyed, the writable layer is destroyed with it."——容器被销毁时,它那一层可写的数据一起销毁。数据库跑在容器里、用户表存在容器里,docker rm 一下就全没了。

解法是把数据放到容器外面。Docker 有两种挂法,名字和用途都不一样:

具名卷(named volume)绑定挂载(bind mount)
写法-v my-data:/app/data-v /Users/你/项目/src:/app/src(或 -v ./src:/app/src)
数据放在哪Docker 自己的存储目录里新建一个目录,"completely managed by Docker"你主机上的那个真实目录,"mounted from the host into a container"
官方推荐的用途持久化数据:数据库、上传的文件——"the preferred mechanism for persisting data"开发时把源码挂进去、共享配置文件、让容器把产物写到主机上
什么时候别用它你想在主机上直接看到/编辑那些文件时(它藏在 Docker 目录里)生产环境:官方提醒带 bind mount 的容器换到没有同样目录结构的机器上会失败("may fail if run on a different host without the same directory structure")
删容器时卷不会被删(具名卷不随容器消失)主机上的文件当然还在

一条经验法则:生产用卷,开发用绑定挂载。开发时把源码挂进去,改文件不用重新造镜像;跑数据库时用卷,容器删了数据还在。

回到那条长命令:-v my-data:/app/data 里的 my-data 只有名字、没有斜杠,所以它是具名卷,数据由 Docker 管理。

05.3 Compose:把一堆 docker run 收进一个文件

一个真实项目往往不止一个容器:一个 Web 服务 + 一个数据库。用 docker run 起它们,你得记住两条长命令、记住启动顺序、还得手工建一个网络让它们能互相找到。Compose 就是干这个的。

官方定义:"Docker Compose is a tool for defining and running multi-container applications."——定义并运行多容器应用的工具。它把整套东西写进一个 YAML 文件(一种缩进表示层级的配置格式),然后一条命令起全套。

文件名有讲究:官方现在推荐 compose.yaml("The default path for a Compose file is compose.yaml (preferred) or compose.yml"),同时也兼容老名字 docker-compose.yml。另外一件常被旧教程带偏的事:文件开头的 version: 字段已经废弃——官方把它标为 "obsolete",写了会收到警告,不写完全没问题。

# compose.yaml —— 不要写 version:
name: myapp

services:
  web:
    build: .                       # 用当前目录的 Dockerfile 现场构建
    image: myapp:1.0               # 构建出来的镜像叫什么名字
    ports:
      - "8080:3000"                # 主机 8080 → 容器 3000
    environment:
      DB_HOST: db                  # 直接用服务名当主机名,Compose 内置了 DNS
      DB_PORT: "5432"
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}   # 从同目录 .env 文件读
    depends_on:
      db:
        condition: service_healthy # 等数据库真的能用了再启动
    volumes:
      - ./src:/app/src             # 开发期把源码挂进去,改完不用重造镜像
    restart: unless-stopped

  db:
    image: postgres:17-alpine
    environment:
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
    volumes:
      - db-data:/var/lib/postgresql/data   # 具名卷:数据库的数据必须活过容器
    healthcheck:                   # 健康检查:每隔几秒跑一条命令,确认它真的能用了
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 10s

volumes:
  db-data:
⚠️ 这份示例的来源

Docker 官方 getting-started 里的双服务例子用的是 Web + redis(结构包含 healthcheck、depends_on、具名卷、develop.watch),官方并没有一份"Web + 数据库"的逐字原文。上面这份是按官方字段说明改编的,字段语义都有官方依据(build、image、ports、environment、volumes),但组合方式属于本文的整理。你可以把它当成起点,不要当成官方范本引用。

配套命令,这五条覆盖日常:

docker compose up -d            # 后台起整套(-d 不加的话,你按 Ctrl+C 会全停)
docker compose up -d --build    # 改了 Dockerfile 后重建再起
docker compose ps               # 看这一套的状态
docker compose logs -f web      # 盯着某个服务的日志
docker compose down             # 收摊:删容器和网络,保留具名卷
docker compose down -v          # 连卷一起删(数据也没了,慎用)

两个容易踩的点:up 不加 -d 时,官方说 "When the command exits, all containers are stopped"——你退出它就全停;down 默认不删具名卷(官方:"Networks and volumes defined as external are never removed",具名卷会留着),想删得加 -v。这条设计救过很多人的数据库。

还有一个 Compose 特有的便利:服务之间可以直接用服务名互相访问。上面 web 里写 DB_HOST: db 就是这个意思——Compose 建了一个网络,每个服务用自己的名字在里面注册,官方建议 "you should always reference services by name, not IP address"。

06分享给别人:四条路,各有各的适用场合

到这一步该问一个关键问题了:你要把它给谁、以什么方式给?答案不同,做法完全不同。先看这张总表,再往下看细节。

方式对方要做什么适合什么场合代价
A. 推到镜像仓库一条 docker pull,或 docker run 直接用长期给多人用;要能被反复拉取、能更新要注册账号、要推送;公开仓库等于公开你的镜像
B. 存成 tar 文件发过去docker load 一条命令内网机器、对方没有账号、只给一次文件大、传起来慢、没法自动更新
C. 给他源码 + Dockerfiledocker compose up --build对方要改代码、要看清楚里面是什么对方要装 Docker、要能联网拉基础镜像
D. 多架构一起打照常用 A 或 B对方是苹果芯片而你是 Intel(或反过来)构建慢(要模拟另一种架构)

06.1 先看懂镜像名:为什么它长得像网址

推送之前必须先看懂镜像名的结构,因为它决定了"推到哪去"。官方给的格式是 [HOST[:PORT_NUMBER]/]PATH[:TAG]:

部分含义省略时
HOST哪个镜像仓库(registry)默认是 Docker 官方的公共仓库 docker.io
PATH在仓库里的路径:命名空间/仓库名Docker Hub 上不写命名空间时,回落到官方镜像所在的 library
TAG标签,人可读的版本标识默认 latest

官方给的例子最能说明问题:nginx 其实等价于 docker.io/library/nginx:latest;而 ghcr.io/dockersamples/example-voting-app-vote:pr-311 则指明了"去 ghcr.io 这个仓库、找 dockersamples 这个命名空间"。

⚠️ latest 不等于"最新版"

官方只规定了它是省略标签时的默认值,从没保证它指向最新版本。把 latest 当成"跟着我更新"的标签,是很多"为什么我拉到的是旧的"问题的根源。给别人用的镜像,老老实实打一个明确的版本号。

06.2 A 路:推到镜像仓库(8 个渠道逐个说)

推之前必须做的一件事是登录。官方原话:"Before you're able to push an image to a repository, you will need to be authenticated."——没登录就推,会得到 requested access to the resource is denied。登录的通用写法:

echo "$DOCKERHUB_TOKEN" | docker login -u 你的用户名 --password-stdin

为什么用 --password-stdin 这种绕一圈的写法?官方解释:"Using STDIN prevents the password from ending up in the shell's history, or log-files."——避免密码出现在你的命令历史和日志里。直接用 -p 密码 会留在 ~/.zsh_history 里。

① Docker Hub:默认的那个

echo "$DOCKERHUB_TOKEN" | docker login -u 你的用户名 --password-stdin
docker tag myapp:1.0 你的用户名/myapp:1.0
docker push 你的用户名/myapp:1.0
docker pull 你的用户名/myapp:1.0

建仓库的两条规则值得提前知道:名字只能是小写字母、数字、连字符和下划线,长度 2–255;而且建好之后不能改名("You can't rename a Docker Hub repository once it's created.")。官方还建议:拿不准可见性时选私有。

② GitHub Container Registry(ghcr.io):和代码放一起

export CR_PAT=你的令牌
echo $CR_PAT | docker login ghcr.io -u 你的GitHub用户名 --password-stdin
docker tag myapp:1.0 ghcr.io/你的用户名/myapp:1.0
docker push ghcr.io/你的用户名/myapp:1.0

认证要用个人访问令牌(PAT,一串代替密码的授权字符串):官方明确 "GitHub Packages only supports authentication using a personal access token (classic)",需要 write:packages 权限(拉取则要 read:packages)。一个小坑是:在网页上勾 write:packages 会顺带勾上一个范围很大的 repo 权限,官方为此专门给了一个链接 https://github.com/settings/tokens/new?scopes=write:packages。另外首次推送的包默认是私有的。

费用方面:官方说 GitHub Packages 公开包免费,而且容器这部分目前更宽松——"Container image storage and bandwidth for the Container registry is currently free."(变更前会提前至少一个月通知)。

③④ 云厂商的三个:ECR / Artifact Registry / ACR

仓库登录 + 推送费用要点
Amazon ECR(私有)aws ecr get-login-password --region 区域 | docker login --username AWS --password-stdin 账号.dkr.ecr.区域.amazonaws.com 然后 docker push 到那个域名新客户首年每月 500 MB 私有存储免费;公开仓库有每月的免费额度(官方写 50 GB)
ECR Public(公开)aws ecr-public get-login-password --region us-east-1 | docker login --username AWS --password-stdin public.ecr.aws登录时固定用 us-east-1(官方明确要求),令牌有效期 12 小时
Google Artifact Registrygcloud auth configure-docker 区域-docker.pkg.dev,镜像名格式是 区域-docker.pkg.dev/项目ID/仓库名/镜像名每月 0.5 GiB 存储免费额度(挂在整个计费账号上,跨项目共享);超出约 $0.10/GiB·月
Azure ACRaz acr login --name 你的仓库名,镜像名是 你的仓库名.azurecr.io/镜像名没有免费档,三档都按天收费(Basic 官方示例是每天 $0.167,含 10 GB)

选哪个基本是"你已经在用哪家云"决定的:代码在 GitHub 上就顺手用 ghcr.io,云主机在 AWS 上就用 ECR,公司有 GCP 就用 Artifact Registry。没有云端偏好、只是想公开分享,Docker Hub 是最省事的。

⑤ Quay.io

Red Hat 家的仓库,官方博客的说法是 "Quay offers free accounts for public repositories, and paid plans for private repositories"——公开仓库免费、私有仓库要付费。它在国内用的人不算多,选它的典型理由是"公司本来就买 Red Hat 的东西"。本站在核对时 quay.io 的定价页打不开,所以具体限额这里不给数字,请以你看到的官网为准。

⑥ 自建 registry:一条命令起一个自己的仓库

不想把镜像放到别人服务器上时(内网、客户现场、公司自管),可以自己起一个:

# 起一个本地仓库服务(带上持久化目录)
docker run -d -p 5000:5000 --restart=always --name registry \
  -v /mnt/registry:/var/lib/registry \
  registry:3

# 推给它(注意镜像名里要带上地址和端口)
docker tag myapp:1.0 localhost:5000/myapp:1.0
docker push localhost:5000/myapp:1.0
docker pull localhost:5000/myapp:1.0
🚫 这样起出来的仓库只能用于内网测试

官方原话:这些初始配置 "only appropriate for testing",而且 "A production-ready registry must be protected by TLS and should ideally use an access-control mechanism."——生产可用必须加 TLS(加密传输)和访问控制。另外注意版本:官方文档现在用的是 registry:3;registry:2 还能拉,但官方支持列表里已经没有 2.x 了。

需要更完整的能力(权限、审计、图形界面)时,业界常用 Harbor——官方自述是一个"open source registry that secures artifacts with policies and role-based access control"(用策略和基于角色的访问控制保护制品的开源仓库),是 CNCF(管理云原生开源项目的基金会)的毕业项目。它的最低要求是 2 CPU / 4 GB 内存 / 40 GB 磁盘,推荐 4 CPU / 8 GB / 160 GB。

推之前的三个自查

🚫 公开推送 = 公开一切进去过的东西

官方对密钥这件事说得很直白:"Build arguments and environment variables are inappropriate for passing secrets to your build",因为它们 "persist in the final image"——会留在最终镜像里,而且镜像层是可以被翻出来的。所以推之前请确认:.env 有没有被 .dockerignore 排除;Dockerfile 里有没有 RUN 写死过密码或令牌;COPY . . 有没有把不该带的目录带进去。官方推荐的做法是用 secret 挂载,只在构建那一瞬间可用:

RUN --mount=type=secret,id=aws \
    AWS_SHARED_CREDENTIALS_FILE=/run/secrets/aws \
    aws s3 cp ...

# 构建时这样传:
docker build --secret id=aws,src=$HOME/.aws/credentials -t myapp:1.0 .
⚠️ Docker Hub 有拉取次数限制,数字有两套口径

官方文档给的表格是:未登录时每个 IPv4 地址(或 IPv6 /64 网段)每 6 小时 100 次;已登录的免费账户 200 次;付费账户不限。超额时仓库会返回 429 状态码,正文是 "You have reached your pull rate limit. You may increase the limit by authenticating and upgrading"。

但 Docker 的定价页上写的是另一个数——Personal 档旁边标注的是 "100 Docker Hub pulls/hr"(每小时 100 次)。两个数字差了约三倍,官方没有解释。本文以文档页的 200 次/6 小时为准,但你知道有这处矛盾,就不会在撞上限流时怀疑人生。CI 服务器、公司共用出口 IP 的办公室最容易撞上这条。

06.3 B 路:打成一个 tar 文件,直接发给他

对方没有账号、机器不能上外网、或者只是临时给一次——用这条路最干净。官方对 docker save 的说明是它 "Save one or more images to a tar archive",而且内容里 "Contains all parent layers, and all tags + versions"——包含全部底层和标签。

# 导出方(你)
docker save -o myapp.tar myapp:1.0

# 文件太大就顺手压缩(load 能直接读 gzip 压缩过的)
docker save myapp:1.0 | gzip > myapp_1.0.tar.gz

# 接收方(他)
docker load < myapp.tar
docker load --input myapp.tar
docker load < myapp_1.0.tar.gz

# 载入之后照常跑
docker run -d -p 8080:3000 --name myapp myapp:1.0

官方说明 docker load 能读 "even if compressed with gzip, bzip2, xz or zstd"(这四种都是常见的压缩格式),而且 "restores both images and tags"——连标签一起恢复,所以对方载入后镜像名和你这边一模一样。

顺便说清两个容易混的命令

你在搜索时一定会看到 docker export 和 docker import,它们不是发给别人的正确工具:

06.4 C 路:把源码和说明书一起给他

这是四条路里对新手最友好、也最透明的一条:你把项目目录打包发过去,他一条命令既构建又运行。对方能看到全部内容,也能自己改。

你要发的就是这几样:

myapp/
├── Dockerfile          # 说明书
├── .dockerignore       # 别把 node_modules、.env 发过去
├── compose.yaml        # 一条命令起全套
└── (你的项目源码)

他那边只需要两条命令:

docker compose up --build -d
# 或者不用 compose:
docker build -t myapp:1.0 .
docker run -d -p 8080:3000 myapp:1.0

compose.yaml 里的 build: . 就是"用这个目录的 Dockerfile 现场构建"的意思;官方说明 build 字段 "specifies the build configuration for creating a container image from source",而且 image 字段在有 build 时可以省略。

✅ 什么时候该选 C 而不是 A

对方要读代码、要改代码、要审查你往镜像里放了什么,或者他只是想学——C 路唯一的问题是他得能联网拉基础镜像(node:24-alpine 这种还是要从 Docker Hub 下来)。如果对方机器完全离网,那只能走 B 路。

06.5 D 路:跨芯片——"我这跑得好好的,他那报 exec format error"

这是分享环节最容易被忽略、发作起来最懵的一个坑。场景:你在 M 系列芯片的 Mac 上构建,镜像里的程序是 arm64 架构的;对方的电脑或服务器是 Intel/AMD 的 x86_64(也叫 amd64)。他那边跑起来直接报错,甚至报的是一串看不懂的 exec format error。

为什么不能凑合跑?官方把根因说得很直接:"the code that's running inside the container must be compatible with the host's architecture",举的例子就是 "you can't run a linux/amd64 container on an arm64 host (without using emulation)"——架构必须对得上,除非用模拟。(架构就是 CPU 的指令集类型:Apple 芯片是 arm64,多数 Intel/AMD 电脑和云服务器是 amd64。)

解法是构建出"多架构镜像",让同一个名字下面同时装着两个版本,对方拉的时候自动挑对的那个:

# 只给对方那一种架构构建(省时间)
docker buildx build --platform linux/amd64 -t 你的用户名/myapp:1.0 --push .

# 两种架构一起构建,推成同一个标签
docker buildx build --platform linux/amd64,linux/arm64 \
  -t 你的用户名/myapp:1.0 --push .

# 想先做一个能同时构建多平台的构建器:
docker buildx create --name container-builder --driver docker-container --bootstrap --use

三个关键细节:

06.6 最后怎么选:一句话

回到本节开头那张表:给很多人长期用 → A;给一个人只给一次、他那不能上外网 → B;对方要改代码或要看清楚里面是什么 → C;芯片不一样 → 在任何一条上加 D。A 路还有个附带的判断:代码在 GitHub 上就用 ghcr.io,云主机在哪家就用那家的仓库,纯公开分享用 Docker Hub。

07对方收到之后:从他那边看完整流程

这一节换个视角:假设镜像已经送到了,对方手上该敲什么。把这一段直接发给对方,通常比你在聊天里解释半小时有用。

07.1 前提:他得先有 Docker

镜像再小,对方机器上也得有 Docker 才能跑。这一步没法省——按 02 让他装(Mac/Windows 装 Docker Desktop,Linux 服务器装 Engine)。装完让他跑 docker run hello-world 验证,看到 "Hello from Docker!" 就说明环境好了。

07.2 从仓库拉:三条命令

# 公开仓库:直接拉
docker pull 你的用户名/myapp:1.0

# 私有仓库:先登录(把仓库地址也带上)
echo "$TOKEN" | docker login ghcr.io -u 用户名 --password-stdin
docker pull ghcr.io/你的用户名/myapp:1.0

# 跑起来
docker run -d --name myapp -p 8080:3000 \
  -e DB_HOST=host.docker.internal \
  你的用户名/myapp:1.0

私有仓库最常见的报错是 requested access to the resource is denied——十有八九是没登录,或者镜像名里的用户名写错了。让对方先 docker login 再试。

07.3 从 tar 文件:两条命令

docker load < myapp_1.0.tar.gz
docker run -d --name myapp -p 8080:3000 myapp:1.0

07.4 从源码:一条命令

docker compose up --build -d

07.5 跑起来之后,让对方确认这三件事

1
容器是"活着"的吗

docker ps 应该有它;如果 docker ps -a 里状态是 Exited,那就是启动即退出,看 09 排错表。

2
端口映射对不对

docker port myapp 看他本机哪个端口接到了容器哪个端口,然后用那个端口访问。

3
是不是卡在"监听地址"上

如果映射没错、容器也活着但访问不到,让他按 03.3 的办法进容器看一眼 —— 十有八九是镜像里那个程序只监听了 127.0.0.1。

07.6 一个完整的交付话术(可以直接复制给人)

✅ 给对方的说明模板

① 先装 Docker(Mac/Windows 装 Docker Desktop,装完跑 docker run hello-world 确认);
② 拉镜像:docker pull 你的用户名/myapp:1.0(如果是私有仓库,先 docker login);
③ 跑起来:docker run -d -p 8080:3000 --name myapp 你的用户名/myapp:1.0;
④ 访问 http://localhost:8080;
⑤ 出问题先跑 docker ps -a 和 docker logs myapp,把输出发我。

08十个必踩的坑(按踩到概率排序)

① 数据没了:docker rm 之后数据库空了
因为没挂卷。容器里的改动随容器一起销毁(05.2)。修法:数据库、上传目录一律挂具名卷;docker compose down 默认不删具名卷,但加 -v 会删,别顺手加。
② 端口被占:Bind for 0.0.0.0:8080 failed: port is already allocated
你本机的 8080 已经被别的东西(很可能是上一次没删掉的容器)占了。修法:docker ps -a 找旧容器停掉删掉,或换个主机端口(比如 -p 8081:80)。macOS/Linux 上可以用 lsof -i :8080 看是谁占的;Windows 上官方建议用 resmon.exe 的"侦听端口"或 netstat -aon | findstr "listening"。
③ 连不上后台:Cannot connect to the Docker daemon
客户端在,后台没跑。Mac/Windows 上先看 Docker Desktop 是不是开着;Linux 上看服务状态 sudo systemctl is-active docker。还有一种情况是 DOCKER_HOST 被误设了:env | grep DOCKER_HOST,有的话 unset DOCKER_HOST。
④ 权限不足:permission denied while trying to connect to the Docker daemon socket
Linux 上没把用户加进 docker 组。修法:sudo usermod -aG docker $USER 然后 newgrp docker 或注销重登(02.8)。顺带记住:进这个组等于拿 root 权限。
⑤ 换了台机器就跑不了:exec format error
架构不匹配——对方拉到的镜像不是他机器的架构。修法:见 06.5,用 docker buildx build --platform 出多架构镜像;临时应急可以在对方那边跑 docker run --platform linux/amd64 镜像(走模拟,慢)。
⑥ 容器起来又立刻退出:docker ps -a 显示 Exited (0)
容器里"排在第一位的那条命令"执行完就返回了,容器也就随之结束。官方给的典型反例是把 service nginx start 当作启动命令——它启动完服务就返回了,"the root process returns and the detached container stops as designed"。修法:让主程序在前台跑(比如 nginx -g 'daemon off;'),或者干脆换成直接跑应用进程。排查动作:docker logs 容器名 看它说了什么;docker run -it --entrypoint sh 镜像 手动进去试。
⑦ 镜像拉不下来,或者报 429
两种可能:网络不通,或者撞了 Docker Hub 的拉取限流(未登录 100 次/6 小时/IP,见 06.2)。修法:先 docker login(登录后额度更高),或者配置内网镜像加速——官方机制是 pull-through cache("a Registry responds to all normal docker pull requests but stores all content locally"),在 /etc/docker/daemon.json 里加:{"registry-mirrors": ["https://你的镜像源地址"]},改完重启 Docker 生效。
⑧ 构建时 COPY failed: file not found in build context or excluded by .dockerignore
要么文件不在构建上下文里(比如你写了 COPY ../config .,上下文之外的路径拿不到),要么被 .dockerignore 排除了。顺带说清出处:这句报错原文不在 Docker 官方文档里,是社区里流通的说法;官方文档给的是机制——COPY 的源只能是构建上下文或 --from= 指定的阶段。另外用 -f 指定 Dockerfile 时,上下文不会跟着变。
⑨ 改了代码,镜像还是旧的
层缓存。见 04.7:某层变了它后面的层全要重做,RUN 只看命令字符串不看内容。修法:docker build --no-cache 强制重建;开发期干脆别重建——用绑定挂载把源码挂进去(05.2)。
⑩ 磁盘满了:/var/lib/docker 越用越大
先量再看:docker system df 会按"镜像/容器/卷/构建缓存"分类告诉你能回收多少(RECLAIMABLE 那列)。清理:docker system prune 默认删掉所有已停止的容器、没被使用的网络、悬空镜像和构建缓存——但默认不动卷(官方解释是为了防止重要数据被删掉),要连卷一起删得显式加 --volumes。加 -a 才会连没在用的完整镜像一起删。

09排错对照表:照着报错找

你看到的最可能的原因先敲这两条
Cannot connect to the Docker daemon后台没跑(Mac/Win 上是 Desktop 没开)docker version(看 Server 段在不在)、Mac/Win 上确认 Docker Desktop 开着
permission denied ... docker.sock用户不在 docker 组groups(有没有 docker)、sudo usermod -aG docker $USER
port is already allocated主机端口被占docker ps -a、lsof -i :端口号
exec format error镜像架构和机器对不上uname -m、docker image inspect 镜像名 --format '{{.Architecture}}'
容器状态 Exited (0)启动命令跑完就返回了docker ps -a、docker logs 容器名
容器状态 Exited (1) 或其它非 0程序自己报错退出了docker logs 容器名(多数时候答案就在最后几行)
浏览器打不开 localhost:端口没写 -p,或程序只监听了 127.0.0.1docker port 容器名、docker exec -it 容器名 ss -tlnp
requested access to the resource is denied没登录,或私有仓库权限不够docker login 仓库地址、检查镜像名里的命名空间
COPY failed: file not found in build context文件不在上下文里,或被 .dockerignore 排除确认构建命令末尾的路径、检查 .dockerignore
429 / pull rate limitDocker Hub 限流docker login、配置 registry mirror
改了 Dockerfile 但行为没变用了缓存docker build --no-cache、docker image history 镜像名 看每层是什么
磁盘告警镜像/缓存堆积docker system df、docker system prune
📖 一个排查习惯,比这张表更值钱

遇到问题时的固定动作顺序:① docker ps -a 看容器是死是活 → ② docker logs 看它最后说了什么 → ③ docker exec -it 容器 sh 进去手动试一次。这三步能解决绝大多数"跑不起来",而且每一步输出的信息都比报错本身具体得多。

10一页纸速查

装完到分享:最短路径

# 1. 验证装好了
docker run hello-world

# 2. 对着自己的项目写一份 Dockerfile(见 04.3),然后造镜像
docker build -t myapp:1.0 .

# 3. 本地跑起来确认
docker run -d --name myapp -p 8080:3000 myapp:1.0
docker logs -f myapp

# 4. 推到仓库分享
docker login -u 你的用户名
docker tag myapp:1.0 你的用户名/myapp:1.0
docker push 你的用户名/myapp:1.0

# 5. 对方那边
docker pull 你的用户名/myapp:1.0
docker run -d -p 8080:3000 你的用户名/myapp:1.0

日常命令速查(附"什么时候用")

命令干什么什么时候用
docker ps / docker ps -a列出运行中 / 全部容器排错永远从 -a 开始——活着的不一定是你以为的那个
docker images列出本机镜像确认镜像造出来了、看看体积
docker logs -f 容器看日志(-f 持续跟)容器起来就挂时,第一现场
docker exec -it 容器 sh进容器内部想确认里面到底有什么、网络通不通
docker stop 容器 / rm 容器停 / 删容器停是优雅停机(先发一个"请你关闭"的信号,默认等 10 秒再强杀),删是清场
docker rmi 镜像删镜像腾空间;运行中容器的镜像要先停容器
docker system df看磁盘占用分类磁盘告警时的第一条命令
docker system prune清理不用的东西大扫除;默认不动卷,别乱加 --volumes
docker compose up -d / down起 / 停一整套项目不止一个容器时
docker compose logs -f 服务名看某个服务的日志不知道是哪个容器出问题时,逐个看
docker save / docker load镜像存成 tar / 从 tar 载入离线交付、内网机器
docker buildx build --platform ... --push构建多架构镜像对方芯片和你不一样时

11名词对照表(回头查用)

名词一句话定义为什么重要 / 何时用 · 别当什么用
镜像(image)一份只读的模板,里面是"怎么造出一个容器"的完整说明和文件它是你唯一能分享出去的东西。别以为它是压缩包——它是整棵文件系统树
容器(container)镜像跑起来的那个实例真正在干活的是它。别以为改它等于改镜像——镜像改不了
Docker Engine核心那套:后台 dockerd + 客户端 + 接口Linux 服务器装的是它。别和 Docker Desktop 混——后者是个含引擎的桌面安装包
Docker Desktop桌面一键安装包,内含引擎、Compose,Mac/Win 上还含一台 Linux 虚拟机Mac/Win 上"装 Docker"实际装的是它。它有许可限制,公司规模超线要付费
守护进程(daemon / dockerd)后台常驻、真正干活的程序"Cannot connect to the Docker daemon"就是它没跑。别把它当成图形界面程序——它是服务
Dockerfile一份文本说明书,每行一条指令,说明怎么把项目装进镜像它是你项目的"环境说明书",能进 git。别把它当脚本——它不能被"执行",只能被 build
构建上下文(build context)你发给构建器的那个文件夹,通常是构建命令末尾的 .它决定了 COPY 能用哪些文件。别以为它跟着 -f 走——-f 只换说明书
镜像层(layer)Dockerfile 每一条指令在镜像里留下的一层文件改动缓存命中与否、镜像为什么变大,都要回到这一层想。别以为改了文件就一定会重建——mtime 不算数
标签(tag)镜像名冒号后面那部分,用来区分版本或变体不写标签默认是 latest。别把 latest 当成"自动最新"——官方只规定它是默认值
镜像仓库(registry)存放和分发镜像的服务器,docker push 推到它,docker pull 从它拉分享镜像的正路。别和 git 仓库混——它存的是镜像,不存源码历史
命名空间(namespace)镜像名里用户名/组织名那一段,如 你的用户名/myapp决定了镜像归谁、能不能推。推失败最常见的原因就是这段不对或没权限
具名卷(named volume)由 Docker 管理的一块持久存储,写法是 名字:/容器内路径生产数据的标准做法,容器删了它还在。别拿它当"能直接编辑的文件目录"——它藏在 Docker 的目录里
绑定挂载(bind mount)把主机上的真实目录挂进容器,写法带斜杠开发时挂源码热更新最方便。别在生产用它——换台机器目录结构就对不上了
端口映射(-p)把主机端口接到容器端口,格式是 主机:容器不写它外面访问不到。别以为 Dockerfile 里的 EXPOSE 也能达到同样效果——它只是声明
EXPOSEDockerfile 里声明"这个容器会监听某端口"的指令它是给人看的文档性声明。别当它是防火墙规则——它不发布任何端口
CMD / ENTRYPOINT镜像的默认启动命令 / 固定入口区别在用户给的参数是覆盖还是追加。别用 shell 形式写 ENTRYPOINT——那样会吞掉用户参数
Compose用一份 YAML 定义并运行多个容器的工具多服务项目的标准做法。别写已废弃的 version: 字段——会被警告
架构(architecture)CPU 的指令集类型:arm64(苹果芯片)或 amd64(Intel/AMD)跨机器跑不动、exec format error 的根因。别以为镜像"能跑就到处能跑"
多架构镜像同一个标签下装着多种架构的版本,拉取时自动选分享给不同芯片的人时的正解。别以为它能留在本地——必须推到仓库
QEMU 模拟让一种架构的机器跑另一种架构程序的翻译层让你能在 Mac 上构建 amd64 镜像。别指望速度和原生一样——官方明说会慢很多
rootless 模式让后台和容器以普通用户身份运行的安装方式共享机器、对安全敏感时的选择。别以为它和普通模式完全等价——路径和环境变量要额外配
个人访问令牌(PAT)代替密码的一串授权字符,可以限定权限范围登录 ghcr.io 必须用它。别把它提交进任何仓库
构件(artifact)仓库里存的东西的统称:镜像、标签、签名、清单都算Harbor 那类仓库的文档里会频繁出现这个词
拉取限流(rate limit)仓库对单位时间内拉取次数的限制Docker Hub 未登录是 100 次/6 小时/IP。CI 和共用出口 IP 的办公室最容易撞上

12常见问题

我只想跑个小脚本,需要上 Docker 吗?
大概率不需要。Docker 的价值在于"环境要被复制很多次、或者要送到别人的机器上"。一次性的本地脚本,用虚拟环境(venv/conda)就够了,还更快。反过来,只要你开始出现"发给别人跑不起来""服务器上和本地不一样""要同时跑两个版本的依赖"这类症状,Docker 就开始回本了。
镜像和容器到底哪个能改?
容器能改,镜像不能。你在容器里装的包、写的文件,都只属于这个容器;docker commit 可以把容器存成一个新镜像,但这是不推荐的做法——正确的路径是改 Dockerfile 重新构建,因为只有 Dockerfile 是可复现、可进 git、可审查的。容器里手动改过的东西,别人永远不知道。
Docker 和虚拟机我该用哪个?
要隔离强度(跑不明来路的程序、做安全测试)用虚拟机;要快速复制和分发应用环境用容器。两者不冲突:Mac 上跑 Docker 本身就是"在虚拟机里跑容器"。另外,容器共用内核这件事决定了它不能跑"另一种内核"的系统——Linux 容器里跑不了 Windows 程序。
我把镜像推到了公开仓库,别人能看到里面的代码吗?
能看到镜像里的一切——包括你 COPY 进去的文件、RUN 时写进层的文件、以及环境变量。所以推之前检查 .dockerignore、检查 Dockerfile 里有没有写死密钥。想只给特定的人,用私有仓库(Docker Hub 免费账户可以建 1 个私有仓库;ghcr.io 首次推送默认就是私有的)。
为什么我的镜像 1 GB 多?
先 docker images 看它到底多大,再 docker image history 镜像名 看哪一层占的。常见三个原因:基础镜像选得太全(python 默认标签约 415 MB,-slim 只要约 46 MB);把 node_modules、.git 拷进去了(.dockerignore 没写);RUN 里装了编译工具但没删。再进一步就是多阶段构建(04.8)。
对方是 Windows 电脑,我给 Linux 镜像他能跑吗?
能——但要他装 Docker Desktop。Windows 上的 Docker Desktop 内含一台 Linux 虚拟机,所以你能跑 Linux 容器。反过来不行:Windows 容器需要 Windows 的那套后端,而且 Home 版只能跑 Linux 容器。
我应该用 docker build 还是 docker buildx build?
日常用 docker build 就行——官方说明它 "is a wrapper around Buildx",而且 docker buildx build 的别名列表里就包含 docker build,两个在大多数场景下等价。需要多架构构建、或者要自定义构建器实例时,才用完整的 docker buildx build。BuildKit(现在的构建引擎)已经是默认的,不用手动开。
公司里用 Docker Desktop 会被追着要钱吗?
看规模:员工少于 250 人并且年营收低于 1000 万美元的公司在免费范围内;政府机构不在免费之列;更大的组织搞商业使用需要订阅。如果只是用 Linux 服务器上的 Docker Engine,那不走这套许可。拿不准时看官方许可页,别靠同事转述。
为什么 docker compose down 之后我的数据库还是空的?
检查两件事:数据库在 compose.yaml 里有没有挂具名卷(挂到 /var/lib/postgresql/data 这种数据目录);以及你是不是用了 down -v——那个 -v 会把卷也删掉。默认的 down 是保留卷的,这是官方刻意设计的。

99一句话总结

🎯 核心要点

Docker 做的事只有一件:把"跑起来需要的一切"和代码一起打包成镜像,从而让环境可以像文件一样被复制和传递。所以它的使用路径永远是同一条:装好引擎(Mac/Win 用 Desktop、Linux 用官方仓库)→ 写一份 Dockerfile 让 Docker 造出镜像 → 本地跑通、把数据接到卷上 → 再决定用仓库推送、tar 文件、还是发源码把镜像送出去。真要记的细节只有四条:-p 是"主机:容器"、数据和容器同生共死所以必须挂卷、EXPOSE 不发布任何端口、以及跨芯片要建多架构镜像。

98参考来源与证据边界

一手来源(本次逐页核对)

本文自己做的核对

除官方文档外,本次还从 download.docker.com 的仓库与静态二进制目录直接确认了当前版本号(docker-ce_29.8.1-1~ubuntu.24.04~noble_amd64.deb、docker-29.8.1.tgz),从 Docker Hub 的镜像接口读取了各镜像标签的实际体积(上表所有 MB 数字),并从 packages.ubuntu.com 核对了 Ubuntu 自带 docker.io 的版本(24.04 上是 29.1.3)。

📖 证据边界与时效说明

时效:Docker 属于演进很快的对象——本文校对日期为 2026-09-16,当时的版本是 Docker Engine 29.8.1 与 Docker Desktop 4.91.0。版本号、镜像体积、各云厂商的免费额度与价格都会变,用之前请以官方页面为准。

三处已知的矛盾,本文都保留了两边:① Docker Hub 免费账户的拉取额度,文档写 200 次/6 小时、定价页写 100 次/小时,官方没有解释;② macOS 最低版本,官方只写"当前及前两个主要版本",具体数字(>= 14)来自 Homebrew cask 页;③ registry 镜像官方文档推荐 registry:3,但 registry:2 仍可拉取,官方没有正式的停止支持声明。

两处官方文档的空白,本文用其他一手源补齐并已标注:① "Web 服务要监听 0.0.0.0"这件事,Docker 官方只在 Node 指南的调试章节顺带提过,机制解释借用了 Flask 官方文档;② "多阶段构建能省多少体积"官方从未给过任何数字,因此本文不提供数字,只提供自己测量的方法。

三处社区流通、但非官方文档原文的报错文本(本文引用时已标注):COPY failed: file not found in build context or excluded by .dockerignore、permission denied while trying to connect to the Docker daemon socket、平台不匹配时的 exec format error。官方文档只写了这些问题的原因与修法,没有写这些报错字符串。

⌂