安裝好 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 只能讀取檔案,不能修改宿主機上的設定。

下面是一段實際錄製的操作:用 bind mount 啟動一個唯讀掛載靜態檔案的 nginx,直接在宿主機上修改檔案內容,不重新建置、不重啟 container,馬上就能在瀏覽器(這裡用 curl 代替)看到更新;最後刪除 container,確認宿主機上的檔案完全不受影響:

docker run 搭配 bind mount 即時反映宿主機檔案修改的實際操作

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 MountVolume
宿主機路徑自己指定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”變成“一個應用”。下面是一段實際錄製的終端機操作,從啟動、查看狀態、確認服務真的能連上,到查看日誌、關閉服務的完整過程:

docker compose up、ps、logs、down 的實際操作

在包含 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 設定即使以前沒有見過,也可以根據設定檔本身推斷出它在做什麼。