安裝好 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,確認宿主機上的檔案完全不受影響:
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 設定即使以前沒有見過,也可以根據設定檔本身推斷出它在做什麼。