Docker 怎么用
把你写好的项目装进一个"盒子",让它在任何人、任何机器上跑出一样的结果——从怎么装、怎么打包,到怎么把这个盒子送到别人手上。
📅 更新于 2026-09-16
本文校对于 Docker Engine @ 29.8.1(2026-09-15 发布)· Docker Desktop @ 4.91.0 · 官方源:docs.docker.com
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 排错对照表 | 照着报错原文找原因 |
先记住两个数字
这四个数字解决新手最常见的两个恐慌:"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."——多个容器共用同一个内核,所以能在更少的硬件上跑更多应用。
顺着"共用内核"这条线,四个错想就都能破解了:
uname -r 看到的是宿主机的内核版本,别拿它判断"容器里装的是哪个 Linux 版本"。docker run -it 镜像 sh 进去看(03.5)。"装 Docker"到底装了什么:一个客户端,一个干活的
你敲的 docker 命令本身不会打包、不会运行任何东西——它只是把你的话转述给一个在后台常驻的"干活程序"。这个结构叫客户端-服务端(client-server):
官方对这个"干活的"有个名叫 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 Engine | v29.8.0 | 那个真正干活的(含 dockerd) |
| Docker CLI | 随附 | 你敲的 docker 命令 |
| Docker Compose | v5.5.1 | 一条命令起多个容器(05.3) |
| Docker Buildx | v0.37.0 | 负责"照着 Dockerfile 造镜像" |
| containerd | v2.3.4 | 再下面一层真正拉起容器的运行时 |
在 Mac 和 Windows 上,这个包里还额外含一个小型的 Linux 虚拟机。原因就是上面说的"共用内核":Mac 和 Windows 的内核不是 Linux,跑不了 Linux 容器,所以必须在中间垫一个真的 Linux。你平时感觉不到它的存在,但下面几件事都是它的表现:
- Mac 上 Docker Desktop 必须开着,
docker命令才能连上后台(这是最常见的"我命令没错啊"); - Mac 上没有
dockerd的官方独立安装——官方静态二进制在 macOS 上只发客户端,"Nodockerdis included; you supply a runtime elsewhere"(docs.docker.com); - Linux 上没有这层虚拟机,所以 Linux 也能反过来装 Engine 本体。
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 Desktop | Mac 内核不是 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
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):
- 系统:Windows 10 64 位企业版/专业版/教育版 22H2(build 19045),或 Windows 11 64 位企业版/专业版/教育版 23H2(build 22631)及以上;
- 不支持的两种情况:服务器版 Windows("Docker Desktop is not supported on server versions of Windows");以及 Home 版只能跑 Linux 容器,不能用原生的 Windows 容器;
- WSL 版本 2.1.5 或更高(若要用 Enhanced Container Isolation 则需 2.6+)。WSL2 是 Windows 上的"Linux 子系统"——微软提供的一台轻量 Linux,Windows 上的 Docker 就是靠它跑起来 Linux 容器的;
- BIOS/UEFI 里开启硬件虚拟化(CPU 上那项"允许跑虚拟机"的功能,多数机器默认是开的),至少 8 GB 内存。
装法:官方给的是安装器(Docker Desktop Installer.exe)与 Microsoft Store。命令行静默安装的写法是:
"Docker Desktop Installer.exe" install --user
如果你习惯用 winget,包标识符是 Docker.DockerDesktop:
winget install -e --id Docker.DockerDesktop
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 时最现实的问题。官方许可页给的免费范围是四类,逐字如下:
- 小企业:"fewer than 250 employees AND less than $10 million in annual revenue"——员工少于 250 人,并且年营收低于 1000 万美元(两个条件都要满足);
- 个人使用;
- 教育用途;
- 非商业开源项目。
需要付费的情形:更大的组织里的专业使用、政府机构(注意政府实体不在免费之列)、以及超出免费范围的商业使用。覆盖它的订阅是 Docker Pro / Team / Business。
| 订阅 | 月付(每人每月) | 年付(每人每月) | 用户数 | 什么时候你需要它 |
|---|---|---|---|---|
| Personal | $0 | $0 | 1 | 个人学习、开源、小于门槛的公司 |
| Pro | $11 | $9 | 1 | 个人商用,或想要更大的 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 组;单人工作站上没问题。对安全更敏感的场景,用 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 命令,只是不带那个图形界面。
| 替代品 | 安装命令 | 它是什么 | 你该在什么情况下考虑 |
|---|---|---|---|
| Colima | brew install colima(另需 brew install docker) | 命令行式的轻量 Linux 虚拟机,支持 Docker/containerd 运行时 | 只想要命令行、不想装图形界面;README 明说 Docker 客户端要另装 |
| Podman Desktop | brew install --cask podman-desktop | 另一套容器引擎(Podman)的桌面工具,可跑无 root 容器 | 组织里对 root 权限敏感时。注意官方文档把 Homebrew 这条路标为"不推荐",建议用 .dmg |
| OrbStack | brew install --cask orbstack | Mac 上的轻量容器/虚拟机替代,自称可直接替换 Docker Desktop | 嫌 Docker Desktop 慢、吃内存。它官网明确声明与 Docker 公司无关联 |
| Lima | brew install lima 然后 limactl start | Linux 虚拟机的通用管理工具(CNCF 孵化项目),常被别的工具当底座 | 你要的不只是容器,而是"一台能 ssh 进去的 Linux" |
上表里"它是什么"这类描述来自各项目自己的官网与仓库;brew install 这些命令来自 Homebrew 官方 cask 页。Docker 官方文档里没有这些命令,也没有推荐过任何替代品。版本层面:Colima 与 Lima 都是活跃更新的开源项目,Podman Desktop 的 Homebrew 安装会连带装上 Podman 引擎。
装错了会怎样:两个真实的坑
先 apt install docker.io,后来又按教程加了官方仓库装 docker-ce。两套包抢同一个可执行文件,之后升级、排错都会出现莫名其妙的版本错乱。
按 02.3 第 1 步把 docker.io 等包先卸掉。顺带说:Ubuntu 24.04 自带的 docker.io 当前是 29.1.3,而官方仓库是 29.8.1——落后得不算离谱,但官方明确不为这个包背书。
两个后台都在跑。官方建议用 Desktop 时先把 Engine 停掉,否则会出现资源争抢和 "port already in use" 这类难查的冲突。
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."——第一个是你电脑(宿主机)的端口,第二个是容器里的端口。
把这条命令读成一句话:"把这一面(主机)的 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 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 ["node","server.js"]。对方敲 docker run myapp:1.0 node other.js,实际跑的是 node other.js——CMD 被整个替换掉了。
镜像里写 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 MB | Alpine 打底 |
数字来自 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"。
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 跑起来之后,让对方确认这三件事
docker ps 应该有它;如果 docker ps -a 里状态是 Exited,那就是启动即退出,看 09 排错表。
docker port myapp 看他本机哪个端口接到了容器哪个端口,然后用那个端口访问。
如果映射没错、容器也活着但访问不到,让他按 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 之后数据库空了Bind for 0.0.0.0:8080 failed: port is already allocateddocker ps -a 找旧容器停掉删掉,或换个主机端口(比如 -p 8081:80)。macOS/Linux 上可以用 lsof -i :8080 看是谁占的;Windows 上官方建议用 resmon.exe 的"侦听端口"或 netstat -aon | findstr "listening"。Cannot connect to the Docker daemonsudo systemctl is-active docker。还有一种情况是 DOCKER_HOST 被误设了:env | grep DOCKER_HOST,有的话 unset DOCKER_HOST。permission denied while trying to connect to the Docker daemon socketdocker 组。修法:sudo usermod -aG docker $USER 然后 newgrp docker 或注销重登(02.8)。顺带记住:进这个组等于拿 root 权限。exec format errordocker 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 镜像 手动进去试。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 .dockerignoreCOPY ../config .,上下文之外的路径拿不到),要么被 .dockerignore 排除了。顺带说清出处:这句报错原文不在 Docker 官方文档里,是社区里流通的说法;官方文档给的是机制——COPY 的源只能是构建上下文或 --from= 指定的阶段。另外用 -f 指定 Dockerfile 时,上下文不会跟着变。/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.1 | docker 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 limit | Docker 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 也能达到同样效果——它只是声明 |
EXPOSE | Dockerfile 里声明"这个容器会监听某端口"的指令 | 它是给人看的文档性声明。别当它是防火墙规则——它不发布任何端口 |
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 commit 可以把容器存成一个新镜像,但这是不推荐的做法——正确的路径是改 Dockerfile 重新构建,因为只有 Dockerfile 是可复现、可进 git、可审查的。容器里手动改过的东西,别人永远不知道。COPY 进去的文件、RUN 时写进层的文件、以及环境变量。所以推之前检查 .dockerignore、检查 Dockerfile 里有没有写死密钥。想只给特定的人,用私有仓库(Docker Hub 免费账户可以建 1 个私有仓库;ghcr.io 首次推送默认就是私有的)。docker images 看它到底多大,再 docker image history 镜像名 看哪一层占的。常见三个原因:基础镜像选得太全(python 默认标签约 415 MB,-slim 只要约 46 MB);把 node_modules、.git 拷进去了(.dockerignore 没写);RUN 里装了编译工具但没删。再进一步就是多阶段构建(04.8)。docker build 还是 docker buildx build?docker build 就行——官方说明它 "is a wrapper around Buildx",而且 docker buildx build 的别名列表里就包含 docker build,两个在大多数场景下等价。需要多架构构建、或者要自定义构建器实例时,才用完整的 docker buildx build。BuildKit(现在的构建引擎)已经是默认的,不用手动开。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参考来源与证据边界
一手来源(本次逐页核对)
- docs.docker.com/engine/ — Docker Engine 的定义与三部分组成(daemon / API / CLI)
- docs.docker.com/get-started/docker-overview/ — 镜像与容器的官方定义("read-only template")、客户端-服务端架构、容器删除后数据消失
- docs.docker.com/.../what-is-a-container/ — 容器共用内核、容器 vs 虚拟机
- docs.docker.com/.../what-is-an-image/ — 镜像不可变、镜像由层组成
- docs.docker.com/desktop/release-notes/ — Docker Desktop 4.91.0(2026-09-14)及其内含组件版本
- docs.docker.com/engine/release-notes/29/ — Docker Engine 29.8.1(2026-09-15)
- docs.docker.com/desktop/setup/install/mac-install/ — macOS 安装要求
- docs.docker.com/desktop/setup/install/windows-install/ — Windows 版本门槛、WSL 要求、安装器参数、Windows 容器限制
- docs.docker.com/engine/install/ubuntu/ — Ubuntu apt 仓库的 deb822 写法、需卸载的 unofficial packages
- docs.docker.com/engine/install/debian/ / rhel / fedora — 各发行版命令与 GPG 指纹
- docs.docker.com/engine/install/binaries/ — 静态二进制步骤、macOS 只提供 client
- docs.docker.com/engine/security/rootless/ — rootless 模式的机制、前置与命令
- docs.docker.com/engine/install/linux-postinstall/ — docker 用户组、权限修法、"docker 组等于 root"警告
- docs.docker.com/subscription/desktop-license/ — Docker Desktop 免费范围(250 人 / 1000 万美元)
- docker.com/pricing — 各档价格
- docs.docker.com/reference/cli/docker/container/run/ —
run全部参数语义、端口方向、detach 的退出行为 - docs.docker.com/reference/cli/docker/version/ —
docker version与docker --version的分工 - docs.docker.com/reference/dockerfile/ — FROM / WORKDIR / COPY / RUN / EXPOSE / CMD / ENTRYPOINT 的官方定义
- docs.docker.com/reference/cli/docker/init/ —
docker init的支持模板、生成的四个文件、覆盖警告 - docs.docker.com/build/building/context/ — 构建上下文与 .dockerignore 的机制
- docs.docker.com/build/building/multi-stage/ — 多阶段构建
- docs.docker.com/build/cache/ 与 cache/invalidation — 层缓存与失效规则
- docs.docker.com/build/building/multi-platform/ — 多架构构建、QEMU 模拟、必须 push 的原因
- docs.docker.com/build/builders/ — 默认构建器与 BuildKit
- docs.docker.com/engine/storage/volumes/ 与 bind-mounts — 卷与绑定挂载
- docs.docker.com/engine/network/port-publishing/ — 端口发布的默认行为与安全提示
- docs.docker.com/compose/ 与 compose/how-tos/networking/ — Compose 定义、服务名互相访问
- docs.docker.com/reference/compose-file/version-and-name/ —
version字段已废弃 - docs.docker.com/.../build-tag-and-publish-an-image/ — 镜像名结构、推送前必须认证
- save / load / export / import — 四条命令的官方描述
- docs.docker.com/docker-hub/download-rate-limit/ 与 usage/pulls — Docker Hub 拉取限流的数字与 429 行为
- docs.docker.com/build/building/secrets/ — 为什么构建参数会留在镜像里、secret 挂载的做法
- docs.docker.com/engine/daemon/troubleshoot/ — daemon 连接问题排查(
DOCKER_HOST等) - docs.docker.com/guides/nodejs/ — 0.0.0.0 监听、官方 .dockerignore、官方 Node 三阶段写法
- docs.docker.com/guides/python/ — 官方 Python 指南(uvicorn、slim/dhi 镜像、glob 式 .dockerignore)
- hub.docker.com/_/python 与 _/node、_/alpine、_/ubuntu — 各镜像体积与标签说明(2026-09-16 抓取)
- docs.github.com/.../working-with-the-container-registry — ghcr.io 认证方式与 scope
- docs.aws.amazon.com/.../getting-started-cli 与 ECR Public 认证 — ECR 登录与推送
- cloud.google.com/artifact-registry/pricing 与 推送指南 — Artifact Registry 免费额度与镜像名格式
- learn.microsoft.com/.../container-registry-skus — ACR 三档与价格示例
- distribution.github.io/distribution/about/deploying/ — 自建 registry 的部署与"仅测试用"警告
- goharbor.io — Harbor 的定位与安装前提
- flask.palletsprojects.com/.../externally-visible-server —
--host=0.0.0.0的作用(用于补 Docker 文档的空白) - get.docker.com 与 github.com/docker/docker-install — 便捷脚本的适用警告
- formulae.brew.sh/cask/docker-desktop — Homebrew cask 的 macOS 版本要求(明确区别于 Docker 官方文档)
- github.com/microsoft/winget-pkgs/.../Docker.DockerDesktop — winget 包标识符与版本(非 Docker 官方文档)
- github.com/abiosoft/colima、podman-desktop.io、orbstack.dev、github.com/lima-vm/lima — 四个替代品各自的官方说明
本文自己做的核对
除官方文档外,本次还从 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。官方文档只写了这些问题的原因与修法,没有写这些报错字符串。