SSD Nodes Learn Hosting plans →
ガイド Matt Connor著者 Matt Connor ・更新日 2026-08-27

Ubuntu 24.04 VPSでDocker Composeを始める方法

Ubuntu 24.04にDocker EngineとCompose v2を導入し、MinifluxとPostgreSQLの2サービスを構築します。ufwの公開ポート問題やボリュームのバックアップ方法も確認できます。

構築するもの

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

インストールは 5 分で完了します。このガイドの残りでは、後で問題になりやすい部分を扱います。別名の root にすぎない docker グループ、ufw のルールをそのまま通過する公開ポート、そして確認プロンプトなしでデータベースを削除する docker compose down の 1 つの flag です。

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

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

最初のコマンドを実行する前に、2 つの誤った方法を避けます。Ubuntu 独自の docker.io パッケージは動作しますが、Docker のリリースより遅れており、他の設定が前提とするプラグイン配置も含まれていません。もう 1 つは、ハイフン付きの単独の docker-compose バイナリです。これは Compose v1 であり、Python 製で 2023 年からサポートが終了しています。古いチュートリアルが動作しない原因にもなります。現在の Compose はスペースを含む docker compose であり、エンジンと同じリポジトリからインストールする CLI プラグインです。

これらのいずれかがすでにサーバーに存在する場合は、最初に削除します。Ubuntu 独自のプラグインパッケージである docker-compose-v2 も含め、すべてを 1 つのリポジトリから導入できる状態にします。

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

3 つの層をすべて確認します。

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

最初の 2 つはバージョン文字列を表示します。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 は 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 つの判断です。1 つずつ確認してください。

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

postgres:16-alpineを指定し、postgres:latestは使いません。タグは固定されません。:latestは pull するたびに、メンテナーが直近で push したバージョンへ再解決されます。これに、これから説明する定期的なアップグレード方法docker compose pull && docker compose up -dを組み合わせると、:latestによって、メジャーバージョンの更新が自分で選んだタイミングではなく、upstream がリリースした時点で適用されます。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 999 で実行され、ホストのディレクトリが UID 1000 の所有になると、アプリケーションは起動時に停止し、ログにpermission deniedが出力されます。名前付きボリュームを使うと、この種類の問題の大半を解消できます。ただし、データが Docker 管理下のパスに保存される点については、後述します。

environment と .env。Secret を git の外に置く

${POSTGRES_PASSWORD}はシェルから読み込まれません。Compose は、compose.ymlと同じディレクトリにある.envという名前のファイルから値を補間します。作成します。

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 は、ローテーションする必要があります。変数が未設定のままスタックを起動すると、Compose は警告を表示して処理を続行し、空の文字列を使用します。Postgres のパスワードでは、これによりデプロイが壊れます。

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

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

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

単純なdepends_on: [db]は、起動順序だけを制御します。Compose は最初に Postgres を起動し、その少し後にアプリケーションを起動しますが、Postgres はまだ接続を受け付けるまで数秒かかります。アプリケーションがデータベースへ接続すると失敗し、実装によってクラッシュするか、再試行します。

信頼できる方法は、上記のファイルで使用している構成です。dbサービスでhealthcheckを定義します(Postgres にはこの目的のpg_isreadyが用意されています)。アプリケーションではdepends_oncondition: 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を手動で実行して意図的に停止した場合は停止したままです。もう 1 つの選択肢であるalwaysは、手動で停止した後もコンテナを復帰させます。通常、これは意図した動作ではありません。restart policy を設定しない場合、午前 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-dataminiflux_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? は別の問題です。daemon 自体が停止しています。sudo systemctl status dockersudo journalctl -u docker -n 50 で原因を確認できます。VPS ではディスク容量の枯渇が典型的な原因なので、まず df -h /var/lib/docker を実行します。

**Bind for 127.0.0.1:8080 failed: port is already allocated は、別の container がその host port をすでに公開していることを示します。docker ps で使用中の container を確認できます。数週間前に試した docker run の古い container が、通常の原因です。docker ps で問題が見つからない場合は、Docker 以外のプロセスがその port を使用しています。sudo ss -tlnp | grep 8080 でプロセスを特定できます。

**yaml: line 14: did not find expected key は、指定された行またはその直前にあるインデントエラーを示します。Compose ファイルは YAML 形式です。インデントは 2 つのスペースで指定し、スペースだけを使用します。tab 文字が 1 つでもあると失敗します。docker compose config で起動せずにファイルを検証できます。編集するたびに実行する習慣を付けると、低コストで問題を防げます。

ufw の意外な落とし穴では、エラーがまったく表示されません。これが危険な理由です。デプロイは成功し、ufw status も正しく見えますが、外部から port scan を実行するとデータベースが見つかることがあります。上記の ports セクションを読み直し、すべての ports: エントリに 127.0.0.1: prefix があるか確認してください。さらに、別の machine から curl http://your-vps-ip:8080 を実行して確認します。期待する結果は connection refused です。

ここからは、Traefik ガイドでこの単一の stack を、1 つの HTTPS entry point の背後にある複数のアプリケーションへ拡張できます。2026 年に self-hosting する価値があるものは、その entry point 経由で運用する候補の一覧です。複数の stack を稼働させ、それぞれが独自の login form を持つようになったら、Authentik のような self-hosted SSO serverで、同じ proxy の背後にある 1 つの account に統合できます。

VPS 上の Minecraft サーバー のようなゲームサーバーは、練習用の最初の Compose プロジェクトとして取り組みやすい題材です。毎日開くものを使って学びたい場合は、自己ホスト型のワークアウトトラッカー openGym が適しています。これはイメージタグではなく git tag に固定した小規模なスタックで、最初の passkey を登録する前に前段へ TLS を配置する必要があります。写真は、他者のクラウドから取り戻したいと最初に考える人が多いデータです。PhotoPrism と Immich の比較 では、どちらかに volume を割り当てる前に、必要な RAM の下限とバックアップ手順を決められます。2 つのサービスでは足りないと感じるようになったら、Notion 風のワークスペースとして AFFiNE を構築する ことで、同じパターンを 4 つのコンテナで実践できます。上記のタグ固定、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 では削除されません。コンテナとプロジェクトネットワークが削除されるだけで、名前付きボリュームは残ります。次回の 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 がイメージの想定する所有者を設定するため、権限がそのまま機能します。ホストからも扱うファイルには bind mount を使用してください。編集する設定ファイル、アップロードするメディアなど、パスを明確にしたいデータが該当します。bind mount 上でコンテナが permission denied により起動に失敗する場合は、まずホストとコンテナの UID 不一致を確認してください。