安装好 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 配置即使以前没有见过,也可以根据配置文件本身推断出它在做什么。