SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor

Ubuntu 24.04 VPSのDocker Compose入門

Ubuntu 24.04にDocker EngineとCompose pluginを導入し、MinifluxとPostgreSQLの2サービスを起動します。ufwを迂回する公開ポート、ボリュームのバックアップ、削除につながるdocker compose downの注意点も解説します。

構築するもの

Docker Compose は、このサイトのほぼすべての構成を支える基盤です。Nextcloud、Vaultwarden、n8n、Immich、Rocket.Chat の各ガイドは、いずれも「この compose ファイルを作成する」から始まります。このページでは、そのファイルが実際に何を意味するのかを説明します。Ubuntu 24.04 で Docker 独自の apt リポジトリから Docker Engine と Compose plugin をインストールし、実際に2サービスのスタックを起動します。構成は、小規模な RSS リーダーである Miniflux と PostgreSQL です。この組み合わせでは、大規模なアプリケーションで使われるすべてのパターンを確認できます。固定したイメージ、healthcheck を設定したデータベース、名前付きボリューム、.env ファイルに保存する Secret、localhost にだけ公開するポートを扱います。

インストールは5分で完了します。このガイドの残りでは、後から問題になりやすい点を説明します。docker グループが別名の root になり得ること、公開ポートが ufw のルールを通り越して直接到達可能になること、そして docker compose down にある1つのフラグで確認プロンプトなしにデータベースが削除されることです。

前提条件は、新規の Ubuntu 24.04 KVM VPS、sudo を実行できるユーザー、1 GB 以上の RAM です。既存の Docker インストールがあっても問題ありません。最初のセクションで削除方法を説明します。

Docker のリポジトリからインストールする

最初のコマンドを実行する前に、2 つの誤った方法を避けます。Ubuntu 独自の docker.io パッケージは動作しますが、Docker のリリースより遅れており、他の構成が前提とするプラグイン配置にも対応していません。また、ハイフン付きの docker-compose コマンドは、古いチュートリアルが案内する方法です。古いサーバーでは通常、これは Python で実装された Compose v1 です。Docker はすでにサポート終了を宣言しているため、これらのチュートリアルが動作しなくなる原因になります。Docker は現在の Go コードからビルドしたスタンドアロンの docker-compose バイナリも公開しています。ただし、公式のインストールページではこの方法を非推奨とし、後方互換性のためだけにサポートしています。現在の Compose は、スペースで区切る docker compose です。これは CLI プラグインであり、エンジンと同じリポジトリからインストールします。

すでにいずれかがインストールされている場合は、docker-compose-v2 と docker-buildx も含めて最初に削除します。docker-buildx は Ubuntu 独自のプラグインパッケージです。すべてを 1 つのリポジトリから取得できるようにします。

sudo apt remove -y docker.io docker-compose docker-compose-v2 docker-doc docker-buildx podman-docker containerd runc

Package 'docker.io' is not installed, so not removed は、新しい VPS で通常表示される出力です。次に 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 バイナリではなく、プラグインを使用していることを確認できます。プラグインは 2025 年 12 月に v2 から v5 へ直接移行しました。古い Compose ファイル形式のバージョンとの混同を避けるため、3 と 4 は使われていません。そのため、ここで v2.x.x と表示するガイドも同じツールを説明しています。hello-world の実行結果は Hello from Docker! で終わるはずです。パッケージにより、サービスはブート時に有効になります。systemctl is-enabled docker は enabled を表示します。

docker グループは root です。理解したうえで判断してください

現在、すべての docker コマンドで sudo が必要です。デーモンのソケット /var/run/docker.sock は 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

グループメンバーシップはログイン時に適用されるため、現在のシェルではエラーが続きます。このセッションでは newgrp docker を実行するか、ログアウトしてから再度ログインしてください。その後、id でグループ一覧に docker が表示されるはずです。

ここでは、重要な点を明確に述べます。docker グループのメンバーシップは、ホスト上の root 権限に相当します。 「root に近い権限」でも「昇格された権限」でもありません。root です。このグループのメンバーは、パスワードを要求されることなく docker run --rm -it -v /:/host alpine chroot /host を実行し、ファイルシステム全体を完全に制御できます。このグループは隔離のためではなく、利便性のために存在します。

Docker の rootless モードは、実際の代替手段です。デーモン自体が、権限を持たないユーザーとして実行されます。ただし、いくつかの制約があります。1024 未満のポートには追加設定が必要です。ネットワークはユーザー空間の shim を介して動作するため、測定可能なオーバーヘッドが発生します。また、実際の root 権限がないと正常に動作しないイメージもあります。単一管理者の VPS で、ログインできるユーザーがすでに sudo を実行できる場合、実際の運用上、このグループによる違いはありません。このチュートリアルではすべてこの構成を前提としています。ただし、sudo より弱い権限であるかのように、安易にメンバーを追加しないでください。

Compose ファイルの構成

各スタックに専用ディレクトリを割り当てます。ディレクトリ名がプロジェクト名になり、コンテナ、ネットワーク、ボリュームの名前に接頭辞として付加されます。

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:

上の各行には、それぞれ判断が含まれています。1 つずつ確認してください。

イメージのバージョンを固定する。:latest と pull の組み合わせは無人アップグレードになる

postgres:16-alpineを使用し、postgres:latestは使用しません。タグは固定されません。:latestは pull のたびに、メンテナーが直近で push した内容へ再解決されます。これに、これから説明する通常のアップグレード手順であるdocker compose pull && docker compose up -dを組み合わせると、:latestによって、メジャーバージョンの更新が自分で選んだタイミングではなく、上流プロジェクトがリリースした時点で適用されます。PostgreSQL では、これは仮定の話ではありません。16 から 17 への予期しない更新により、互換性のないデータディレクトリを使ってコンテナがクラッシュループすることがあります。Postgres のメジャーアップグレードには再起動ではなく、dump と restore が必要だからです。

少なくともメジャーバージョンは固定します。postgres:16-alpineなら 16.x のパッチリリースに追従します。アプリケーションはminiflux/miniflux:2.2.9のように正確なリリースへ固定し、プロジェクトの releases ページを確認して、ファイルを書く時点での最新バージョンを使用します。これでアップグレードは、意図して行う 1 行の編集になります。変更内容はgit diffで確認できます。

Docker は ufw を迂回するため、127.0.0.1 に公開する

"127.0.0.1:8080:8080"は、ホストアドレス、ホストポート、コンテナポートの順です。多くのチュートリアルでは"8080:8080"と記述します。これは0.0.0.0:8080:8080の省略形です。つまり、パブリックインターフェースを含むすべてのインターフェースで待ち受けます。

ここに落とし穴があります。ほとんどの人が一度はこの問題に遭遇します。Docker は、パケットをフィルタリングする前に、宛先をコンテナの内部 IP に書き換える DNAT ルールを作成してポートを公開します。そのため、パケットはFORWARDの経路を通り、ufw のルールが適用されるINPUTには到達しません。sudo ufw deny 8080は成功を報告し、ufw statusはポートを拒否したと表示する一方で、サービスはインターネット全体に応答し続けます。ファイアウォールが壊れているのではありません。設計上、迂回されているだけです。Docker が ufw を迂回する理由と、コンテナのトラフィックを実際にフィルタリングする方法では、この仕組みと、公開したままにする必要があるポートに対するDOCKER-USERの対策を説明します。

この問題をなくす習慣は、特別な理由がない限り、公開ポートを127.0.0.1にバインドすることです。外部公開するサービスの前段にはリバースプロキシを置きます。次の手順として、このページの後にTraefik リバースプロキシガイドでまさにその構成を作成します。1 つのコンテナがポート 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 mount

db-data:/var/lib/postgresql/dataは名前付きボリュームです。Docker が/var/lib/docker/volumes/配下にディレクトリを作成して管理し、コンテナへマウントします。もう 1 つの方法は bind mount である./data:/var/lib/postgresql/dataです。これは、ホスト上で指定したパスをマッピングします。

実運用で有効な使い分けは明確です。コンテナだけが扱うデータには名前付きボリュームを使用します。特にデータベースでは、Docker がイメージの想定する所有権でボリュームを初期化するため、ファイル権限がそのまま機能します。ホストから操作するファイルには bind mount を使用します。テキストエディターで編集する設定ファイル、rsync で配置するメディアライブラリなど、パスを明確にしておきたいものが対象です。bind mount でよくある失敗は所有権です。イメージが固定 UID でプロセスを実行し、Miniflux イメージは UID 65534 で実行し、postgres:16-alpineは UID 70 に変更します。一方、ホストのディレクトリが UID 1000 の所有だと、アプリケーションは起動時に停止し、ログにpermission deniedが出力されます。名前付きボリュームを使えば、この種の問題の多くを解消できます。その代わり、データは Docker が管理するパスに保存されます。これについては後述します。

environment と .env、秘密情報を git に含めない

Compose はcompose.ymlの隣にある.envというファイルから${POSTGRES_PASSWORD}を読み込みます。ただし、シェルでその変数がすでに定義されていない場合に限ります。シェル環境に export された値は、.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 ではなく hex を使用します。このパスワードはDATABASE_URL接続文字列に含まれます。base64 が生成する/、+、=の文字は URL の解析を壊します。この問題は構文エラーではなく認証エラーとして現れるため、原因の特定に時間がかかります。.gitignoreの行は最初の commit より前に追加します。compose ファイルは公開してバージョン管理できますが、.envファイルは公開してはいけません。git の履歴に記録された Secret は、ローテーションが必要な Secret です。変数が未設定のままスタックを起動すると、Compose は警告を出して空文字列のまま処理を続けます。Postgres のパスワードでは、これによりデプロイが壊れます。

WARN[0000] The "POSTGRES_PASSWORD" variable is not set. Defaulting to a blank string.

docker compose configは、完全に補間されたファイルを出力します。コンテナが実際に受け取る値を確認する最も簡単な方法です。ただし、出力には Secret も含まれることに注意してください。

depends_on は healthcheck を追加しない限り何も待たない

単純な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は、手動で停止した後もコンテナを復活させます。これは意図した動作でないことがほとんどです。restart ポリシーを設定しないと、午前 4 時のカーネル更新による再起動でサービスが停止し、気付くまで復旧しません。

日常的に使うコマンド

日常の操作は、プロジェクトディレクトリから実行する5つのコマンドに集約できます。

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は繰り返し安全に実行できます。現在の状態とファイルを比較し、設定またはイメージが変更されたサービスだけに変更を適用します。アップグレード用の2つのコマンドは、固定したタグが現在指すイメージを取得します。postgres:16-alpineではパッチリリースが取得されます。完全固定したタグでは、タグを編集するまで何も取得されません。これが完全固定の目的です。アップグレード後は古いイメージが蓄積するため、docker image prune -fでディスク領域を回収します。

次は破壊的なコマンドです。重要なので明記します。docker compose downは安全です。コンテナとネットワークは破棄して再作成でき、データは volume に保存されているためです。docker compose down -vは名前付き volume も削除します。これはデータベースそのものを即座に削除する操作で、確認プロンプトも取り消し手段もありません。 -vフラグは実験環境の解体に使用します。実データを保持する stack では、rm -rfと同じ扱いにしてください。/var/lib/docker/volumes/の下にゴミ箱はありません。

実行中のコンテナ内で一時的に shell を開く場合は、docker compose exec db psql -U minifluxでデータベースに入り、docker compose exec miniflux shで app の shell を開きます。

データが実際に保存される場所

名前付きボリュームにはプロジェクトのプレフィックスが付くため、miniflux というディレクトリ内の db-data は miniflux_db-data になります。

docker volume ls
docker volume inspect miniflux_db-data

inspect の出力には、重要な行が含まれています。

"Mountpoint": "/var/lib/docker/volumes/miniflux_db-data/_data"

このディレクトリがデータベースです。ホストのファイルシステム上で root が所有し、down、アップグレード、コンテナの再ビルド後も保持されます。バックアップでは、このディレクトリを必ず取得してください。

名前付きボリュームをバックアップする

標準的な方法は、ボリュームを読み取り専用でマウントした一時コンテナをホスト上のディレクトリと並べて起動し、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 してください。より確実なのは、構造上整合性が保たれる論理バックアップを取得する方法です。

docker compose exec -T db pg_dump -U miniflux miniflux | gzip > miniflux-$(date +%F).sql.gz

-T は、Compose がデフォルトで割り当てる疑似端末を無効にします。ダンプの出力を TTY 経由でパイプすると、データが破損する可能性があります。これらのいずれかを cron に登録し、結果を VPS の外部へコピーしてください。保護対象のデータと同じディスクに保存されたバックアップは、バックアップではなく単なるコピーです。Nextcloud ガイドでは、この2つの方法を中心に、完全な定期バックアップの手順を構成しています。

発生するエラーと表示される文字列

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 デーモン自体が停止しています。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 形式であり、インデントはスペース 2 つを使用し、タブ文字が含まれているとエラーになります。docker compose config で起動せずにファイルの構文を検証できるため、編集のたびに実行する習慣を推奨します。

ufw の落とし穴は、エラーが表示されないため危険です。デプロイは成功し、ufw status の表示も正常に見えますが、外部からのポートスキャンではデータベースが露出したままになります。前述のポートに関するセクションを再確認し、すべての ports: エントリに 127.0.0.1: プレフィックスが不足していないか確認してください。また、別のマシンから curl http://your-vps-ip:8080 を実行し、connection refused が返されることを確認してください。

ここから先は、Traefik ガイド を参考に、単一のスタックを 1 つの HTTPS エントリポイント配下で複数のアプリを運用する構成へ発展させます。また、2026 年にセルフホストすべきもの は、その環境で動かすためのリストです。複数のスタックが稼働し、それぞれが独自のログインフォームを持つようになったら、Authentik のようなセルフホスト型 SSO サーバー を導入することで、同じプロキシ配下でアカウントを統合できます。

VPS 上の Minecraft サーバー のようなゲームサーバーは、Compose を練習する最初のプロジェクトとして最適です。毎日使うもので学習したい場合は、openGym (セルフホスト型ワークアウトトラッカー) が適しています。これはイメージタグではなく git タグで固定された小規模なスタックであり、最初のパスキーを登録する前に TLS による保護が必要です。写真は多くの人がクラウドから移行したいと考える最初のデータであり、PhotoPrism と Immich の比較 を読むことで、ボリュームを割り当てる前に必要なメモリ量とバックアップ手順を把握できます。2 つのサービスでは物足りなくなったら、Notion 風ワークスペースである AFFiNE の構築 に挑戦してください。これは 4 つのコンテナを使用する同じパターンであり、固定タグ、ヘルスチェック、名前付きボリュームの運用が習慣化しているかを試す良い機会になります。

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 はデータを削除しません。コンテナとプロジェクトネットワークを削除しますが、名前付きボリュームは残ります。次回の up -d で再接続されます。docker compose down -v は破壊的な形式です。名前付きボリューム、つまりデータベースを、確認なしで削除します。元に戻すこともできません。検証済みのバックアップがない限り、実データを含むスタックで -v を実行しないでください。

docker-compose と docker compose の違いは何ですか?

docker-compose(ハイフン)は、Compose を呼び出す古い方法です。古いサーバーでは通常、Docker がサポート終了を宣言した Python 製の初期実装、Compose v1 です。Docker は現在の Go コードからビルドしたスタンドアロンの docker-compose バイナリも引き続き公開しています。ただし、インストールは非推奨で、後方互換性のためだけにサポートされています。そのため、新しいサーバーでは使用しないでください。docker compose(スペース)は現在の Compose です。v2 以降の形式で、2025 年 12 月からは v5 として番号付けされています。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 がイメージの想定する所有者を設定するため、権限がそのまま機能します。ホストからも扱うファイルには bind mount を使用します。編集する設定ファイル、アップロードするメディア、パスを明確にしたいデータなどが該当します。bind mount でコンテナが permission denied により起動に失敗した場合は、最初にホストとコンテナ間の UID 不一致を確認してください。