SSD Nodes Learn Hosting plans →
指南 Matt Connor作者: Matt Connor

Ubuntu 24.04 安裝 Docker Compose 完整教學

在 Ubuntu 24.04 上安裝 Docker Engine 與 Compose 外掛。學習編寫包含 Miniflux 與 PostgreSQL 的 compose.yml,並解決 ufw 連接埠繞過與資料庫備份等常見陷阱。

您即將建構的內容

Docker Compose 是本站幾乎所有專案的基礎。無論是 Nextcloud、Vaultwarden、n8n、Immich 還是 Rocket.Chat,每份指南的開頭都是「撰寫此 compose 檔案」,而本頁將解釋該檔案的實際含義。您將在 Ubuntu 24.04 上從 Docker 官方 apt 套件庫安裝 Docker Engine 與 Compose 外掛,接著部署一個包含兩個服務的實際堆疊:輕量級 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 命令則是舊版教學常引導使用的方式。在較舊的伺服器上,該命令通常對應以 Python 撰寫的 Compose v1。Docker 已宣布它終止生命週期,也是這些教學失效的原因。Docker 目前仍發布以 Go 程式碼建置的獨立 docker-compose 二進位檔,但其安裝頁面稱此方式不建議使用,僅為向後相容而支援。現在的 Compose 是帶空格的 docker compose,屬於 CLI 外掛,並且從與引擎相同的套件庫安裝。

如果主機上已存在其中任何元件,請先全部移除,包括 docker-compose-v2 和 docker-buildx。這些是 Ubuntu 自己提供的外掛套件,應確保所有元件都來自同一個套件庫:

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc docker-buildx 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

驗證這 3 層:

docker --version
docker compose version
sudo docker run --rm hello-world

前 2 個命令會輸出版本字串,Docker Compose version v5.x.x 則確認安裝的是外掛,而不是已終止維護的 v1 二進位檔。外掛在 December 2025 從 v2 直接跳到 v5,略過 3 和 4,以避免與舊版 compose 檔案格式版本混淆。因此,教學中在此處顯示 v2.x.x 時,指的是相同的工具。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 搭配自動更新等於無人看管的升級

請使用 postgres:16-alpine,而非 postgres:latest。標籤(tag)並非固定不變::latest 每次執行 pull 時,都會重新解析為維護者最近推送的版本。若再加上您即將學到的例行升級習慣 docker compose pull && docker compose up -d,:latest 意味著只要上游發布新版,您的服務就會自動跳升至大版本。以 PostgreSQL 為例,這並非假設性問題:若意外從 16 跳升至 17,容器會因資料目錄不相容而陷入崩潰迴圈(crash-loop),因為 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。

具名磁碟區與綁定掛載(bind mounts)

db-data:/var/lib/postgresql/data 是具名磁碟區:Docker 會在 /var/lib/docker/volumes/ 下建立並管理目錄,並將其掛載至容器內。另一種選擇是綁定掛載 ./data:/var/lib/postgresql/data,它會對應至您在主機上選擇的路徑。

實務上的區分原則為:具名磁碟區僅用於容器存取的資料(特別是資料庫),因為 Docker 會以映像檔預期的權限初始化磁碟區,檔案權限能直接運作。綁定掛載用於您會從主機存取的檔案,例如您用文字編輯器修改的設定檔、您透過 rsync 同步的媒體庫,或任何您希望路徑明確的檔案。綁定掛載最常見的失敗原因是權限:映像檔以固定 UID 執行程序(Miniflux 映像檔以 UID 65534 執行,postgres:16-alpine 則降權至 UID 70),而您的主機目錄擁有者為 UID 1000,導致應用程式啟動時在日誌中出現 permission denied 錯誤。具名磁碟區能消除這類錯誤,代價是資料存放在 Docker 管理的路徑下,詳見下文。

environment 與 .env,請勿將機密放入 git

Compose 會從 compose.yml 旁的 .env 檔案填入 ${POSTGRES_PASSWORD},但前提是您的 shell 尚未定義該變數:shell 環境中匯出的值優先於 .env 檔案。因此,先前實驗遺留的 export POSTGRES_PASSWORD=... 會靜默覆蓋檔案設定,而 docker compose config(見下文)可顯示最終生效的值。請建立該檔案:

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-data

inspect 的輸出內容包含關鍵的一行:

"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 伺服器 可將其整合為單一帳號,並統一由反向代理進行管理。

遊戲伺服器(例如 在 VPS 上架設 Minecraft 伺服器)是練習 Docker Compose 的絕佳入門專案。若您想從日常使用的工具開始,openGym 自架健身追蹤器 是一個小型堆疊,它使用 git tag 而非 image tag,且在註冊首個 passkey 前需先配置 TLS。照片備份通常是使用者脫離公有雲的首要需求,比較 PhotoPrism 與 Immich 可協助您評估記憶體需求與備份流程,再決定掛載儲存空間。當您不再滿足於僅運行兩個服務時,架設 Notion 風格的 AFFiNE 工作區 是個不錯的挑戰,它包含四個容器,能有效檢驗您是否已養成使用固定 tag、健康檢查 (healthchecks) 與命名磁碟區 (named volumes) 的習慣。

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 不會。它只會移除容器和專案網路;具名 volume 會保留,下一次執行 up -d 時會重新附加。docker compose down -v 是具破壞性的形式:它會刪除具名 volume,也就是您的資料庫,而且不會要求確認,也無法復原。除非已有經驗證的備份,否則絕對不要對包含實際資料的 stack 執行 -v。

docker-compose 和 docker compose 有什麼差異?

docker-compose(連字號)是舊版呼叫 Compose 的方式。在較舊的伺服器上,它通常是 Compose v1,也就是 Docker 已宣布終止生命週期的 Python 原始版本。Docker 目前仍發布以 Go 程式碼建置的獨立 docker-compose 二進位檔,但標示該安裝方式不建議使用,僅為向後相容而支援,因此新伺服器應跳過它。docker compose(空格)是目前的 Compose,自 v2 起使用,並自 December 2025 起編號為 v5;它是 Docker CLI 的 Go 外掛,從 Docker 的 apt 套件庫以 docker-compose-plugin 安裝。命令和 YAML 幾乎完全相容,因此舊版教學寫著 docker-compose up 時,請輸入 docker compose up。

為什麼 ufw 阻擋連接埠後,仍可從網際網路連到我的 Docker 容器?

因為 Docker 會在 iptables 的 PREROUTING chain 中使用 DNAT 規則發布連接埠,而重新寫入的封包會沿著 Docker 自有 chain 的 FORWARD 路徑傳送,因此不會經過套用 ufw 規則的 INPUT chain。ufw deny 8080 因此無法阻擋已發布的容器連接埠。請從來源修正:發布到 127.0.0.1:,再透過反向代理公開服務。

應使用具名 volume 還是 bind mount?

如果資料只由容器存取,請使用具名 volume,尤其是資料庫。Docker 會設定映像檔預期的擁有權,通常可直接使用正確權限。如果也需要從主機處理檔案,請使用 bind mount,例如您會編輯的設定檔、上傳的媒體,以及希望路徑清楚可見的任何資料。如果容器在 bind mount 上以 permission denied 啟動失敗,首先應檢查主機與容器之間的 UID 不一致問題。