安装好 Docker 之后,真正需要理解的并不是几十条 docker 命令,而是 Docker 如何组织应用程序。
Docker 的核心模型其实很简单:应用程序及其运行环境被打包成 image,Docker 根据 image 创建 container;container 中需要长期保留的数据放到 volume 或宿主机目录中;不同 container 通过 Docker network 通信;当一个应用包含多个 container 时,再使用 Docker Compose 把这些配置组织起来。
flowchart LR
A[Dockerfile / Registry]
B[Image]
C[Container]
D[Volume / Bind Mount]
E[Docker Network]
F[Other Containers]
A --> B
B -->|docker run| C
C --> D
C --> E
E --> F
理解这几个对象之间的关系以后,大多数 Docker 命令就只是对这些对象进行创建、查看、启动和删除。
本文重点介绍这些概念以及它们在实际部署中的使用方式,不把 Docker 写成一份命令手册。
1. Image 与 Container
Docker 最重要的两个概念是 image 和 container。
Image 是一个只读的软件模板,其中包含应用程序以及运行它所需要的文件。例如:
nginx:alpine
python:3.13-slim
postgres:18
都表示 Docker image。Image 通常从 Registry 获取:
docker pull nginx:alpine
也可以根据 Dockerfile 自己构建:
docker build -t myapp:1.0 .
而 container 是根据 image 创建出来的运行实例。例如:
docker run nginx:alpine
大致经历:
flowchart LR
A[nginx:alpine Image]
B[Create Container]
C[Start Container]
D[Running Process]
A -->|docker run| B
B --> C
C --> D
这里有一个很容易混淆的地方:
docker run不是“运行一个已有 container”,而是根据 image 创建一个新的 container,并启动它。
因此:
docker run nginx:alpine
执行两次,会创建两个不同的 container。如果一个 container 已经存在,只是被停止了,应该使用:
docker start container_name
而不是重新执行 docker run。
1.1 一个典型的 docker run
例如运行一个 Nginx:
docker run -d \
--name web \
--restart unless-stopped \
-p 8080:80 \
nginx:alpine
这里虽然有很多参数,但实际上只描述了几个问题:
-d--detach: 让 container 在后台运行。--name web:将 container 命名为web。--restart unless-stopped:如果 Docker daemon 或机器重新启动,自动恢复这个 container;如果管理员主动停止它,则保持停止。-p 8080:80: 将宿主机的 TCP 8080 端口映射到 container 的 TCP 80 端口。nginx:alpine才是创建这个 container 使用的 image。
所以 docker run 可以理解成:
docker run [container 的运行配置] image
而不是单纯的“启动 Docker”。
1.2 Container 的生命周期
平时真正经常使用的 container 命令并不多。
docker ps # 查看正在运行的 container
docker ps -a # 查看包括已经停止的 container:
docker stop web # 停止
docker start web # 启动
docker logs web # 查看日志
docker logs -f web # 持续查看日志
docker exec -it web sh # 进入 container
docker rm web # 删除 container
docker rm -f web # --force ,如果 container 还在运行也强制删除
docker exec -it web bash # 在container中执行bash命令
日常维护中,基本就是围绕:
run
ps
logs
exec
stop
start
rm
这些操作。
2. Container 不应该保存重要数据
Container 有自己的文件系统。
例如进入一个 container:
docker exec -it web sh
然后在里面创建:
/data/test.txt
这个文件确实存在,但它属于这个 container 的 writable layer。
如果 container 被删除:
docker rm -f web
再根据相同 image 创建一个新的 container,刚才的数据并不会自动回来。
这并不是 Docker 的缺陷,而是 container 的设计方式:
Container 应该可以随时删除和重新创建,真正重要的数据应该独立于 container 生命周期。
flowchart TD
A[Image]
A --> B[Container]
A --> C[New Container]
B -->|删除| D[Container Writable Layer 消失]
E[Persistent Storage]
B --> E
C --> E
Docker 中最常用的持久化方式有两种:
- bind mount
- volume
2.1 Bind Mount
Bind mount 是把宿主机上的一个真实目录直接挂载进 container。例如:
Host Container
/data/nginx/html <--> /usr/share/nginx/html
可以写成:
docker run -d \
--name web \
-p 8080:80 \
--mount type=bind,source=/data/nginx/html,target=/usr/share/nginx/html \
nginx:alpine
之后 container 对 /usr/share/nginx/html 的访问,实际上就是访问宿主机/data/nginx/html
Docker 也支持更简短的 -v:
-v /data/nginx/html:/usr/share/nginx/html
对于简单场景两种写法都很常见,但 --mount 的含义更加明确。
Bind mount 很适合:
- 配置文件;
- 静态文件;
- 开发时的源代码;
- 需要直接从宿主机读取和修改的数据。
例如:
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf:ro
这里的 :ro 表示read only,container 只能读取文件,不能修改宿主机上的配置。
2.2 Volume
另一种方式是让 Docker 自己管理存储位置。例如:
docker volume create postgres-data
然后:
docker run -d \
--name postgres \
--mount type=volume,source=postgres-data,target=/var/lib/postgresql/data \
postgres
这里不需要关心 postgres-data 在宿主机具体对应哪个目录。
从使用角度,可以把两者简单区分为:
| Bind Mount | Volume | |
|---|---|---|
| 宿主机路径 | 自己指定 | Docker 管理 |
| 是否方便直接查看文件 | 是 | 通常不需要 |
| 配置文件 | 很适合 | 一般不需要 |
| 数据库数据 | 可以 | 通常更适合 |
| 与宿主机目录结构耦合 | 较强 | 较弱 |
因此一个很实用的原则是:
希望自己直接管理文件时使用 bind mount;主要由应用自己管理的数据使用 volume。
例如:
services:
app:
volumes:
- ./config:/app/config
database:
volumes:
- database-data:/var/lib/database
volumes:
database-data:
这也是很多长期运行的 Docker 服务常见的组织方式。
3. Docker Network 与端口映射
Docker container 默认拥有自己的网络环境。
假设一个 Nginx container 在内部监听:
0.0.0.0:80
这并不意味着宿主机的 TCP 80 端口自动变成这个 Nginx。
如果希望通过宿主机访问,需要显式发布端口:
docker run -d \
-p 8080:80 \
nginx:alpine
这里8080是 Host Port,80是 Container Port。因此:
flowchart LR
A[Browser]
B[Docker Host<br/>:8080]
C[Nginx Container<br/>:80]
A --> B
B -->|Port Mapping| C
访问:
http://host-ip:8080
才会被 Docker 转发到 Nginx container 的 80 端口。
如果只希望本机访问:
docker run -d \
-p 127.0.0.1:8080:80 \
nginx:alpine
这在前面还有 Nginx、Caddy 等反向代理时非常有用。
Container 之间如何通信
端口映射主要解决的是:
Docker 外部如何访问 container。
而 container 之间通信,则主要依靠 Docker network。例如创建一个 network:
docker network create app-network
启动数据库:
docker run -d \
--name db \
--network app-network \
postgres
再启动应用:
docker run -d \
--name app \
--network app-network \
myapp
此时 app 不需要知道 db 的具体 IP。
可以直接访问:
db:5432
因为同一个 user-defined Docker network 中的 container 可以通过名字进行 DNS 解析。
flowchart LR
subgraph Docker Network
A[App<br/>app]
B[PostgreSQL<br/>db:5432]
end
A -->|db:5432| B
这一点非常重要。
Docker container 的 IP 不是一个应该写死在配置文件里的东西。Container 被重新创建后 IP 可能变化,但是 service/container name 可以保持稳定。
因此不要写:
postgres://172.18.0.3:5432
应该写:
postgres://db:5432
同样需要注意:
container 中的
127.0.0.1永远表示这个 container 自己。
例如 Nginx 和 Nextcloud 分别运行在两个 container 中:
Nginx Container
Nextcloud Container
那么 Nginx 中:
proxy_pass http://127.0.0.1:8080;
指向的是 Nginx container 自己的 8080 端口,而不是 Nextcloud。
如果两个 service 在同一个 Docker network 中,应该使用:
proxy_pass http://nextcloud:8080;
这也是理解 Docker 网络最重要的一点之一。
4. 为什么需要 Docker Compose
如果只有一个 container,docker run 很方便。
但稍微复杂一点的服务,很快会变成:
docker run \
--name app \
--network app-network \
--restart unless-stopped \
-e DATABASE_HOST=db \
-e DATABASE_USER=app \
-v ./config:/app/config \
-p 8000:8000 \
myapp:latest
如果还存在:
PostgreSQL
Redis
Nginx
Worker
那么需要维护的 docker run 命令会越来越多。
真正的问题并不是命令太长,而是:
应用的部署配置只存在于某次执行过的 Shell 命令中。
Docker Compose 做的事情,就是把这些 container 的运行参数保存成一个可以版本管理的配置文件。
例如:
services:
app:
image: myapp:latest
restart: unless-stopped
ports:
- "8000:8000"
environment:
DATABASE_HOST: db
DATABASE_USER: app
volumes:
- ./config:/app/config
depends_on:
- db
db:
image: postgres
restart: unless-stopped
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: example
POSTGRES_DB: app
volumes:
- database-data:/var/lib/postgresql/data
volumes:
database-data:
通常保存为:
compose.yaml
然后:
docker compose up -d
Compose 会根据配置创建 container、network 和 volume。
flowchart TD
A[compose.yaml]
A --> B[App Container]
A --> C[Database Container]
A --> D[Default Network]
A --> E[database-data Volume]
B --> D
C --> D
C --> E
这里有一个非常有用的默认行为:
同一个 Compose project 中的 service 默认会加入同一个 network,并且可以直接通过 service name 相互访问。
所以上面的:
DATABASE_HOST: db
可以直接工作。
不需要知道 database container 的 IP,也不需要为了 app → db 的内部通信把 PostgreSQL 的 5432 端口暴露给宿主机。Compose 默认会为项目创建一个网络,并为 service name 提供内部 DNS 解析。
因此 database 可以只写:
db:
image: postgres
而不需要:
ports:
- "5432:5432"
除非宿主机或者 Docker network 之外的程序确实需要直接访问 PostgreSQL。
这可以减少没有必要暴露的端口。
5. Compose 中最常用的配置
一个实际的 Compose service 通常就是在描述几个问题:
services:
app:
image: example/app:latest
restart: unless-stopped
ports:
- "8080:8000"
environment:
APP_ENV: production
volumes:
- ./config:/app/config:ro
- app-data:/app/data
networks:
- frontend
可以把这些配置理解为:
image 用什么软件环境
ports 哪些端口暴露给 Host
environment 给应用什么环境变量
volumes 哪些数据放到 Container 外部
networks 能和哪些 Container 通信
restart Container 退出以后怎么办
这几个字段已经覆盖绝大多数 Homelab 和普通服务器部署。
Environment
环境变量可以直接写:
environment:
APP_ENV: production
DATABASE_HOST: db
也可以放进单独的文件:
env_file:
- .env
对于密码、token 等信息,不建议直接提交到公开 Git repository。
Ports
ports:
- "8080:80"
仍然表示:
Host :8080 -> Container :80
如果只允许 Host 本机访问:
ports:
- "127.0.0.1:8080:80"
这非常适合:
Internet
↓
Nginx
↓
127.0.0.1:8080
↓
Application Container
Volumes
Bind mount:
volumes:
- ./config:/app/config
Named volume:
volumes:
- app-data:/app/data
并在最外层声明:
volumes:
app-data:
Docker 官方也区分这两种存储模型:bind mount 使用明确的宿主机路径,而 named volume 的存储位置由 Docker 管理;对于主要由 container 使用的持久数据,volume 通常是更合适的默认选择。
Networks
对于一个简单 Compose project,通常甚至不需要写:
networks:
Compose 会自动创建 default network。
只有需要进行更明确的网络隔离时才需要自己配置。
例如:
services:
nginx:
image: nginx
networks:
- frontend
app:
image: myapp
networks:
- frontend
- backend
db:
image: postgres
networks:
- backend
networks:
frontend:
backend:
结构就变成:
flowchart LR
subgraph frontend
A[Nginx]
B[App]
end
subgraph backend
C[App]
D[Database]
end
A --> B
C --> D
这样 Nginx 可以访问 App,但是不能直接访问 Database。
对于普通服务不需要一开始就设计复杂 network;Compose 默认 network 已经足够。只有明确存在隔离、跨项目通信或者反向代理共享 network 等需求时,再增加自定义 network。
6. 日常使用 Docker Compose
Compose 最大的变化,是让管理对象从“一个个 container”变成“一个应用”。
在包含 compose.yaml 的目录中启动:
docker compose up -d
查看状态:
docker compose ps
查看日志:
docker compose logs
持续查看:
docker compose logs -f
只查看一个 service:
docker compose logs -f app
重新读取配置并更新服务:
docker compose up -d
如果 image 有更新,可以:
docker compose pull
docker compose up -d
停止服务:
docker compose stop
再次启动:
docker compose start
如果希望删除当前 project 创建的 container 和 network:
docker compose down
docker compose down 默认不会删除 compose 文件中声明的 named volume,因此正常重新部署不会把数据库数据一起删除。只有显式加入:
docker compose down -v
才会删除项目声明的 named volume;对于数据库等重要数据,这条命令需要特别谨慎。
日常更新一个 Compose 服务,最常用的流程实际上只有:
docker compose pull
docker compose up -d
docker compose logs -f
如果修改了 compose.yaml,通常也不需要先手工删除原来的 container。再次执行:
docker compose up -d
Compose 会根据配置变化重新创建需要更新的 container。
这也是为什么在长期运行的服务器上,相比保存大量 docker run 命令,我更推荐使用 Compose。
最终需要长期保存的东西可以收敛成:
my-service/
├── compose.yaml
├── .env
├── config/
└── other-bind-mounted-data/
而 container 本身应该随时可以重新创建。
整个思路可以概括成:
flowchart TD
A[compose.yaml]
B[Image]
C[Container]
D[Persistent Data]
E[Docker Network]
A -->|定义运行方式| C
B -->|创建| C
C -->|可以删除和重建| C
C --> D
C --> E
D -->|独立于 Container 生命周期| F[长期保留]
Docker 真正值得掌握的并不是几十条命令,而是这几个对象之间的边界:
Image 是模板,Container 是运行实例;Container 可以随时重建,重要数据放在 Volume 或 Bind Mount 中;Container 通过 Network 通信,通过 Port Mapping 向 Host 暴露服务;Docker Compose 则把这些运行配置保存成可以重复部署和版本管理的文件。
只要这一套模型建立起来,大部分 Docker 配置即使以前没有见过,也可以根据配置文件本身推断出它在做什么。