Ubuntu 24.04 Docker Compose 安裝與設定指南
在 Ubuntu 24.04 上安裝 Docker Engine 與 Compose v2。學習部署雙服務架構、避開 ufw 連接埠繞過陷阱,並掌握正確的資料庫儲存卷備份技巧。
您將建置的內容
Docker Compose 是本站幾乎所有專案的基礎。無論是 Nextcloud、Vaultwarden、n8n、Immich 還是 Rocket.Chat,每個指南的開頭都是「編寫此 compose 檔案」,而本頁將解釋該檔案的實際含義。您將在 Ubuntu 24.04 上從 Docker 官方 apt 套件庫安裝 Docker Engine 與 Compose v2 外掛程式,接著部署一個真實的雙服務堆疊:輕量級 RSS 閱讀器 Miniflux 及其搭配的 PostgreSQL。這組範例涵蓋了大型應用程式所使用的所有模式:固定映像檔版本、具備健康檢查的資料庫、命名儲存卷 (named volume)、存放在 .env 檔案中的機密資訊,以及僅發布至 localhost 的連接埠。
安裝過程只需五分鐘。本指南其餘部分將涵蓋後續可能引發問題的關鍵點:docker 群組本質上等同於 root 權限、發布的連接埠會直接繞過您的 ufw 規則,以及 docker compose down 上一個會直接刪除資料庫且不經確認的旗標。
先決條件:一台全新的 Ubuntu 24.04 KVM VPS、一個具備 sudo 權限的使用者,以及至少 1GB 的記憶體。若已安裝 Docker 亦可,第一節將說明如何移除舊有版本。
從 Docker 官方儲存庫安裝,而非 Ubuntu 儲存庫
在執行第一個指令前,請先避開兩個常見錯誤。Ubuntu 內建的 docker.io 套件雖然可用,但版本更新落後於 Docker 官方,且缺乏其他工具預設依賴的插件架構。此外,名稱帶有連字號的獨立二進位檔 docker-compose 是 Compose v1:它基於 Python,已於 2023 年終止支援,這也是舊版教學失效的主因。現代的 Compose 是以空格分隔的 docker compose,屬於 CLI 插件,應與 Docker Engine 從同一個儲存庫安裝。
若系統中已存在上述任何套件,請先將其移除,包含 Ubuntu 官方封裝的插件 docker-compose-v2,以確保所有元件皆來自同一個儲存庫:
sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc podman-docker containerd runc全新 VPS 的預期輸出應為 Package 'docker.io' is not installed, so not removed。接著加入 Docker 儲存庫並進行安裝:
sudo apt update
sudo apt install -y 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
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "$VERSION_CODENAME") stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin驗證以下三個層級:
docker --version
docker compose version
sudo docker run --rm hello-world前兩項指令會輸出版本字串,Docker Compose version v2.x.x 則確認您安裝的是插件,而非已淘汰的 v1 二進位檔。hello-world 執行後應以 Hello from Docker! 結尾。該套件會自動啟用開機啟動服務;systemctl is-enabled docker 應輸出 enabled。
docker 群組等同於 root,請審慎評估風險
目前執行每個 docker 指令都需要 sudo,這是因為位於 /var/run/docker.sock 的 daemon socket 擁有者為 root 與 docker 群組。若未加入該群組,您將會遇到 Docker 最常見的錯誤訊息:
permission denied while trying to connect to the Docker daemon socket at
unix:///var/run/docker.sock標準解決方案如下:
sudo usermod -aG docker $USER群組成員資格會在登入時生效,因此目前的 shell 仍會出現錯誤。請執行 newgrp docker 以在當前 session 套用變更,或登出後重新登入;接著執行 id 應能列出 docker 群組。
現在必須誠實說明:加入 docker 群組等同於在主機上擁有 root 權限。 這不是「類 root」或「提升權限」,而是真正的 root。群組內的任何使用者皆可執行 docker run --rm -it -v /:/host alpine chroot /host 並取得整個檔案系統的控制權,且無需輸入密碼。此群組的存在是為了方便,而非為了隔離。
Docker 的 rootless 模式才是真正的替代方案,此模式下 daemon 會以您的非特權使用者身分執行。但這會帶來代價:1024 以下的埠號需要額外設定,網路傳輸需透過 userspace shim 處理,會產生顯著的效能損耗,且部分映像檔若無真正的 root 權限將無法正常運作。在僅有一位管理員且該帳號已具備 sudo 權限的 VPS 上,加入此群組在實務上並無差別,本指南的所有教學皆預設此配置,但請務必認知其權限等同於 sudo。
Compose 檔案剖析
請為每個堆疊(stack)建立專屬目錄,目錄名稱即為專案名稱,此名稱會作為容器、網路與儲存卷的前綴:
sudo mkdir -p /opt/miniflux && sudo chown $USER /opt/miniflux && cd /opt/miniflux建立 compose.yml(這是現代名稱;docker-compose.yml 仍可使用)。請略過舊版的 version: 鍵值,它已過時,若 Compose 偵測到會發出警告。
services:
miniflux:
image: miniflux/miniflux:2.2.9
restart: unless-stopped
ports:
- "127.0.0.1:8080:8080"
environment:
- DATABASE_URL=postgres://miniflux:${POSTGRES_PASSWORD}@db/miniflux?sslmode=disable
- RUN_MIGRATIONS=1
- CREATE_ADMIN=1
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=${ADMIN_PASSWORD}
depends_on:
db:
condition: service_healthy
db:
image: postgres:16-alpine
restart: unless-stopped
environment:
- POSTGRES_USER=miniflux
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=miniflux
volumes:
- db-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD", "pg_isready", "-U", "miniflux", "-d", "miniflux"]
interval: 10s
timeout: 5s
retries: 5
volumes:
db-data:上述每一行都是一項決策,請逐一審視。
鎖定映像檔版本,:latest 搭配 pull 等同於無人值守升級
請使用 postgres:16-alpine,而非 postgres:latest。標籤並非固定不變::latest 每次執行 pull 時,都會重新解析為維護者最近推送的任何版本。若結合您即將學到的例行升級習慣 docker compose pull && docker compose up -d,:latest 意味著當上游發布新版本時,您的服務就會自動跳轉至主要版本,而非由您決定。以 PostgreSQL 為例,這並非假設性問題:若意外從 16 跳轉至 17,容器會因資料目錄不相容而陷入崩潰迴圈,因為 Postgres 的主要版本升級需要執行 dump 與 restore,而非僅僅重啟。
至少鎖定主要版本(postgres:16-alpine 會跟隨 16.x 的修補程式更新),並將應用程式鎖定在特定發布版本,例如 miniflux/miniflux:2.2.9。請查看專案的發布頁面,並在撰寫檔案時使用當前版本。如此一來,升級就成為您刻意進行的一行編輯,且在 git diff 中清晰可見。
發布至 127.0.0.1,因為 Docker 會繞過 ufw
"127.0.0.1:8080:8080" 的格式為:主機位址、主機埠、容器埠。大多數教學使用 "8080:8080",這是 0.0.0.0:8080:8080 的縮寫:監聽所有介面,包含公開介面。
這是陷阱,幾乎每個人都會踩過一次。Docker 透過寫入 DNAT 規則來發布埠,該規則會在過濾前將封包目的地改寫為容器的內部 IP,因此封包會走 FORWARD 路徑,完全不會觸及 INPUT(您的 ufw 規則所在處)。sudo ufw deny 8080 會回報成功,ufw status 顯示該埠已拒絕,但服務仍會回應整個網際網路。您的防火牆並未故障,而是被設計機制繞過了。為何 Docker 會繞過 ufw,以及如何真正過濾容器流量 一文詳細說明了此機制,以及針對必須保持公開之埠的 DOCKER-USER 修復方式。
讓此問題消失的習慣是:除非有特殊理由,否則請將發布的埠綁定至 127.0.0.1,並在前方放置反向代理來處理任何需要對外開放的服務。這正是 Traefik 反向代理指南 在本頁之後所要建構的內容:一個擁有 80 與 443 埠的容器,透過主機名稱路由至其他服務,並提供 TLS。(若您來自舊版 Traefik v2 設定?請參考 Traefik v2 遷移至 v3 指南,其中涵蓋了名稱變更與規則調整。)
啟動堆疊後請驗證綁定:sudo ss -tlnp | grep 8080 應顯示 127.0.0.1:8080,而非 0.0.0.0:8080 或 *:8080。
具名儲存卷與綁定掛載
db-data:/var/lib/postgresql/data 是具名儲存卷(named volume):Docker 會在 /var/lib/docker/volumes/ 下建立並管理目錄,並將其掛載至容器內。另一種選擇是綁定掛載(bind mount),即 ./data:/var/lib/postgresql/data,它會對應至您在主機上選擇的路徑。
實務上的區分原則為:具名儲存卷適用於僅由容器存取的資料,特別是資料庫,因為 Docker 會以映像檔預期的擁有權初始化儲存卷,檔案權限能直接運作。綁定掛載適用於您會從主機存取的檔案,例如您用文字編輯器修改的設定檔、您透過 rsync 同步的媒體庫,或任何您希望路徑明確的檔案。綁定掛載最常見的失敗原因是擁有權問題:容器以 UID 999 執行,而您的主機目錄擁有者為 UID 1000,導致應用程式在啟動時因 permission denied 而崩潰。具名儲存卷能消除這類錯誤,代價是資料存放在 Docker 管理的路徑下,詳見下文。
environment 與 .env,請勿將機密放入 git
${POSTGRES_PASSWORD} 不會從您的 shell 讀取;Compose 會從 compose.yml 旁邊名為 .env 的檔案中進行插值(interpolation)。請建立它:
cat > .env <<'EOF'
POSTGRES_PASSWORD=change-me-to-something-long
ADMIN_PASSWORD=change-me-too
EOF
chmod 600 .env
echo ".env" >> .gitignore請使用 openssl rand -hex 24 產生真實數值。刻意使用十六進位而非 base64:因為此密碼會進入 DATABASE_URL 連接字串中,而 base64 產生的 /、+ 與 = 字元會破壞 URL 解析,這種失敗會表現為驗證錯誤而非語法錯誤,且會浪費您一整個晚上。.gitignore 行應在首次 commit 前加入:compose 檔案可以安全地發布與版本控制,但 .env 檔案絕對不行;若機密已進入 git 歷史紀錄,請視為該機密已洩漏並進行輪替。若啟動堆疊時缺少變數,Compose 會發出警告並以空字串繼續執行,對於 Postgres 密碼而言,這意味著部署失敗:
WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.docker compose config 會列印出完整插值後的檔案,這是檢查容器實際接收內容最快的方式;請記住其輸出包含您的機密。
depends_on 不會等待任何東西,除非您加入健康檢查
單純的 depends_on: [db] 僅控制啟動「順序」:Compose 先啟動 Postgres,隨後啟動應用程式,此時 Postgres 距離接受連線可能還有幾秒鐘。應用程式連接資料庫失敗,隨後根據其撰寫品質選擇崩潰或重試。
可靠的版本即為上述檔案所使用的:db 服務定義了 healthcheck(Postgres 本身提供 pg_isready 以達成此目的),而應用程式宣告 depends_on 並搭配 condition: service_healthy。Compose 會啟動資料庫,每 10 秒輪詢一次檢查,直到檢查通過才啟動 Miniflux。若資料庫始終無法健康運作(例如密碼錯誤、儲存卷損毀),應用程式將不會啟動,且 Compose 會告知您哪個相依項目失敗:
dependency failed to start: container miniflux-db-1 is unhealthy該訊息會指向 docker compose logs db,那才是真正錯誤所在之處。
restart: unless-stopped
兩項服務皆設定 restart: unless-stopped 意味著容器在崩潰後或 VPS 重啟後會自動恢復,但若您刻意執行 docker compose stop,它們將保持停止狀態。另一種選擇 always 即使在手動停止後也會復活容器,這通常不是您想要的。若沒有重啟策略,凌晨 4 點的核心更新重啟會讓您的服務靜默關閉,直到您發現為止。
日常操作指令
日常維運僅需五個指令,請在專案目錄下執行。
docker compose up -d # create and start; idempotent, recreates only what changed
docker compose ps # status, ports, and health of this project's containers
docker compose logs -f miniflux # follow one service's logs; --tail 100 for recent history
docker compose pull && docker compose up -d # upgrade to the pinned tags
docker compose down # stop and remove containers and the network執行 up -d 是安全的,可重複執行。它會比對檔案與實際狀態的差異,僅針對設定檔或映像檔已變更的服務進行更新。升級指令對會抓取您鎖定標籤(pinned tags)所指向的最新版本:postgres:16-alpine 會更新修補版本(patch releases),若使用精確鎖定則不會變更,這正是設計目的。升級後會殘留舊映像檔,請使用 docker image prune -f 回收磁碟空間。
接著是具破壞性的指令,請務必留意:docker compose down 是安全的,容器與網路皆可拋棄,資料則存於 volume 中。docker compose down -v 會一併刪除具名 volume。這代表您的資料庫將會瞬間消失,且無確認提示,亦無法復原。 -v 旗標僅用於拆除實驗環境;若堆疊中含有實際資料,請將其視同 rm -rf 處理。在 /var/lib/docker/volumes/ 下沒有資源回收桶。
若需進入執行中的容器執行一次性指令:docker compose exec db psql -U miniflux 可讓您進入資料庫,docker compose exec miniflux sh 則可取得應用程式的 shell。
資料實際存放的位置
Named volumes 會加上專案前綴,因此 db-data 在名為 miniflux 的目錄中會變成 miniflux_db-data:
docker volume ls
docker volume inspect miniflux_db-datainspect 的輸出內容包含關鍵的一行:
"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"該目錄即為資料庫,位於宿主機檔案系統中並由 root 擁有,即使執行 down、升級或重建容器,資料依然會保留。這也是備份時必須包含的確切路徑。
備份具名儲存卷 (Named Volume)
標準做法是建立一個拋棄式容器,將儲存卷以唯讀模式掛載至主機目錄,並使用 tar 進行封存:
docker run --rm \
-v miniflux_db-data:/data:ro \
-v "$PWD":/backup \
alpine:3.22 tar czf /backup/miniflux-db-$(date +%F).tar.gz -C /data .此方法無需安裝額外軟體,執行後不會留下任何背景程序。還原時則是鏡像操作,使用 tar xzf 將資料寫入至掛載方式相反的全新空儲存卷中。
針對資料庫需注意:直接對「執行中」的 Postgres 資料目錄進行 tar 備份,可能會擷取到寫入過程中的不一致狀態,導致還原後無法正常啟動。建議在執行 tar 的數秒內使用 docker compose stop 暫停寫入,或者採取更好的做法:執行邏輯傾印 (logical dump),其架構本身即具備一致性:
docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz其中的 -T 參數會停用 Compose 預設分配的虛擬終端 (pseudo-terminal),因為透過 TTY 傳輸傾印輸出可能會導致資料損毀。請將上述指令加入 cron 並將結果複製到 VPS 外部;若備份檔與原始資料存放在同一顆磁碟上,那僅是複本而非備份。Nextcloud 指南 即是圍繞這兩種模式建立完整的排程備份流程。
故障模式與常見錯誤訊息
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sock,表示您尚未加入 docker 群組,或是加入後尚未重新登入以更新權限。執行 id 可查看目前的有效群組;newgrp docker 可修正當前 shell 的權限,登出並重新登入則可套用至所有環境。
Cannot connect to the Docker daemon at unix:///var/run/docker.sock. Is the docker daemon running?,這是不同的問題:Docker daemon 本身已停止。執行 sudo systemctl status docker 與 sudo journalctl -u docker -n 50 可查詢原因。在 VPS 上,常見原因是磁碟空間已滿,請先執行 df -h /var/lib/docker 確認。
Bind for 127.0.0.1:8080 failed: port is already allocated,表示已有其他容器佔用了該主機連接埠。執行 docker ps 可查看佔用者;通常是 docker run 週前實驗性執行後未清理的舊容器所致。若 docker ps 顯示無異常,則可能是非 Docker 程序佔用,執行 sudo ss -tlnp | grep 8080 可找出該程序。
yaml: line 14: did not find expected key,表示指定行數或其上方出現縮排錯誤。Compose 檔案採用 YAML 格式:必須使用兩個空格進行縮排,且嚴禁使用 tab 字元。執行 docker compose config 可在不啟動服務的情況下驗證檔案語法,養成每次編輯後執行此指令的習慣可避免錯誤。
ufw 的隱憂在於它不會顯示任何錯誤訊息,這使其極具風險:部署過程看似成功,ufw status 顯示的狀態也正確,但從外部進行連接埠掃描時,您的資料庫仍可能暴露在外。請重新閱讀上述連接埠章節,檢查每個 ports: 項目是否遺漏 127.0.0.1: 前綴,並從「另一台機器」執行 curl http://your-vps-ip:8080 進行確認,若收到 connection refused 才代表設定正確。
接下來,Traefik 指南 將引導您將單一堆疊擴展為多個應用程式,並共用同一個 HTTPS 入口;2026 年值得自架的服務清單 則提供了可供部署的應用建議。當您部署多個服務且每個服務都有各自的登入介面時,如 Authentik 等自架 SSO 伺服器 可將這些服務整合至同一個帳號,並統一由反向代理進行管理。
遊戲伺服器是適合練習的第一個 Compose 專案,例如 VPS 上的 Minecraft 伺服器。如果你想用每天都會開啟的服務來學習,openGym,自架的健身追蹤器 是一個鎖定 git tag 而非 image tag 的小型堆疊;在註冊第一個 passkey 前,必須先在前端配置 TLS。照片通常是人們最想從他人雲端取回的資料,而 比較 PhotoPrism 與 Immich 可在你決定將 volume 掛載到其中任何一個服務前,先確認所需的最低 RAM 與備份流程。當兩個服務已經不敷使用時,建置 AFFiNE,打造類似 Notion 的工作區 會將相同模式延伸至 4 個容器,也能測試你是否已養成固定使用上述 pinned tag、healthcheck 與 named volume 的習慣。
FAQ
為什麼會出現 "permission denied while trying to connect to the Docker daemon socket"?
您的使用者帳號不在 docker 群組中,或是加入群組後尚未重新登入,群組權限僅在登入時生效。請執行 sudo usermod -aG docker $USER,接著執行 newgrp docker 或登出後重新登入,並以 id 確認。此群組擁有與 root 等同的權限,請僅將其授予您信任且具備 sudo 權限的使用者。
執行 docker compose down 會刪除我的資料嗎?
一般的 docker compose down 不會刪除資料,它僅移除容器與專案網路;具名儲存卷 (named volumes) 會保留,下次執行 up -d 時會自動重新掛載。docker compose down -v 是破壞性指令:它會直接刪除具名儲存卷(包含您的資料庫),且不會有確認提示或復原機制。除非您已持有經過驗證的備份,否則請勿在存有實際資料的堆疊上執行 -v。
docker-compose 與 docker compose 有什麼不同?
docker-compose(帶連字號)是 Compose v1,這是一個獨立的 Python 二進位檔,已於 2023 年終止支援,不應安裝在新的伺服器上。docker compose(帶空格)是 Compose v2,這是 Docker CLI 的 Go 語言外掛程式,透過 Docker 的 apt 套件庫安裝為 docker-compose-plugin。指令與 YAML 格式幾乎完全相容,因此當舊教學提到 docker-compose up 時,請輸入 docker compose up。
為什麼即使 ufw 封鎖了連接埠,我仍能從網際網路存取 Docker 容器?
因為 Docker 透過 iptables 的 PREROUTING 鏈使用 DNAT 規則發布連接埠,重寫後的封包會經由 Docker 自身的鏈路徑 FORWARD 傳輸,因此封包不會經過 ufw 規則所在的 INPUT 鏈。因此,ufw deny 8080 對於已發布的容器連接埠無效。請從源頭解決:將連接埠發布至 127.0.0.1:,並透過反向代理伺服器公開服務。
我應該使用具名儲存卷還是綁定掛載 (bind mount)?
若資料僅由容器存取(特別是資料庫),請使用具名儲存卷,因為 Docker 會自動設定映像檔所需的擁有權,權限問題通常能自動解決。若檔案也需要從主機端處理(例如您要編輯的設定檔、上傳的媒體檔案,或任何您希望路徑明確的檔案),請使用綁定掛載。若容器在啟動時因 permission denied 而失敗,請優先檢查主機與容器之間的 UID 是否不匹配。