本文へ移動
購読
GUIDE#devtools更新 2026.09.24

Docker Compose入門:自宅サーバーでWeb・DB・リバースプロキシをまとめて動かす

Docker Engineの導入からCaddy・WordPress・MariaDBの3層構成をcompose.yamlで動かすまでの手順。.envの優先順位、healthcheckでの起動順制御、更新とバックアップも。

level
初級
time
約25分
stack
Docker Engine / Docker Compose v2 / Ubuntu 24.04
author
@ryoma_t
// 前提条件
Linuxの基本操作(SSH接続、ファイル編集)ができること / Ubuntu 24.04のサーバーまたはVMが用意できること
docker compose up -dでCaddy・WordPress・MariaDBの3層構成を起動し、psとcurlで動作を確認したターミナル
画像:nagoya.dev(実際の画面・実行結果をもとに作成)

Docker Composeとは、複数のコンテナの構成を1つのYAMLファイル(compose.yaml)に書き、docker compose upの1コマンドでまとめて起動・停止・更新できるDocker公式のツールです。docker runの長い引数をファイルに移すため、別のマシンでも同じ構成を再現でき、変更履歴もGitで追えます。

対象環境はUbuntu 24.04のサーバー1台で、実例にはCaddy(リバースプロキシ)+ WordPress + MariaDBの3層構成を使います。

この記事でわかること

  • Compose v2(docker compose)と旧v1の違い、公式リポジトリからの導入手順
  • services / volumes / networksの役割と、名前付きボリュームとバインドマウントの使い分け基準
  • .envによる変数展開の優先順位と、environment / env_fileの関係
  • depends_onのcondition: service_healthyで起動順を制御する仕組み
  • 3層構成の実例と、pullでの更新、mariadb-dumpとtarによるバックアップ

Docker Composeとは:Dockerとの違いとCompose v2の位置づけ

Docker Engineはコンテナを1つずつ実行する基盤で、Composeはcompose.yamlを読んでEngineのAPIを呼び、コンテナ・ネットワーク・ボリュームを作る「単一ホスト向けの複数コンテナ管理ツール」です。独自の実行方式は持たないので、Composeで作ったコンテナもdocker psやdocker logsでそのまま扱えます。

執筆時点のComposeは「v2」で、Docker CLIのプラグイン(docker-compose-plugin)としてdocker composeサブコマンドで動きます。Pythonで書かれた旧docker-compose(ハイフン付き、v1)は2023年にサポートが終了しました。ファイル名はcompose.yamlが推奨で、先頭のversion:キーは不要です(書くと警告が出て無視されます)。

Docker Engineをインストールする

Ubuntu標準のdocker.ioはComposeが別パッケージ(docker-compose-v2)で更新も遅れがちなので、Docker公式のaptリポジトリからdocker-ce一式を入れます。執筆時点の公式手順は次のとおりです。

terminal
sudo apt-get update
sudo apt-get 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-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker "$USER"
newgrp docker
docker compose version

usermod -aG dockerは、/var/run/docker.sockの書き込み権限を持つdockerグループに自分を加え、sudoなしで操作するための設定です。グループ変更はログインし直すまで反映されないため、その場ではnewgrp dockerで新しいシェルを開きます。

dockerグループのユーザーは-v /:/hostのようにホストのルートをマウントしたコンテナを起動できるため、実質root権限と同じです。付与する相手を絞り、SSH鍵認証とufwを先に済ませてください。手順はLinuxサーバーの基本ハードニングにまとめています。

compose.yamlの書き方:services・volumes・networks

トップレベルはservices(コンテナ定義)、volumes(名前付きボリュームの宣言)、networks(ネットワークの宣言)の3つです。各サービスの必須級のキーはimage、ports、volumes、environment、restartで、portsは「ホスト側:コンテナ側」の順に書き、8080:80がYAMLで60進数と解釈されないよう引用符で囲みます。restart: unless-stoppedはホスト再起動後に自動で立ち上がりつつ、自分でstopしたコンテナは起こさない設定です。

volumesには./conf:/etc/appのようなバインドマウントと、db_data:/var/lib/mysqlのような名前付きボリューム(トップレベルのvolumes:で宣言)の2種類があり、基準は「人が編集するファイルはバインドマウント、プロセスが書き込むデータは名前付きボリューム」です。

観点バインドマウント(./path:/container)名前付きボリューム(name:/container)
実体の場所指定したホストのディレクトリ/var/lib/docker/volumes/<project>_<name>/_data
所有者・権限ホスト側のUID/GIDがそのまま見え、不一致でPermission deniedになりやすい初回起動時にイメージ側の所有者で初期化される
バックアップホスト側で普通にコピー一時コンテナからtar、またはアプリのダンプ機能

例外はNASに置きたいデータで、バインドマウントかdriver_optsでNFSを指定したボリュームを使います。書き終えたらdocker compose configで検証します。インデント誤りや存在しないキーはAdditional property ... is not allowedと指摘され、変数展開後の最終形も確認できます。

環境変数と.envファイルの優先順位

環境変数には2つの経路があります。compose.yaml内の${DB_PASSWORD}を置き換える「変数展開」(同じディレクトリの.envが自動で読まれる)と、コンテナのプロセスに渡すenvironment: / env_file:です。.envの中身はそれだけではコンテナに渡らない点を混同しやすいので注意してください。.envはDB_NAME=wordpressのように1行1変数で書き、引用符は不要です。

展開の優先順位は、シェルの環境変数 > .env > compose.yaml内のデフォルト値(${DB_PORT:-3306}の書き方)です。同名の変数をexportしていると.envが無視されるので、「変えたのに反映されない」ときはenv | grep DB_を確認します。コンテナ側ではenvironment:がenv_file:より優先されます。

.envはパスワードを平文で持つのでchmod 600にし、.gitignoreへ追加してください。docker compose configの出力にも展開後のパスワードが含まれるため、掲示板などに貼る前にマスクが必要です。

基本コマンド:up / down / ps / logs / exec

コマンド動作ポイント
docker compose up -d定義どおり作成・起動2回目以降は差分のあるサービスだけ再作成
docker compose ps状態一覧STATUS列の(healthy)でhealthcheck結果が見える
docker compose logs -f --tail=100 appログ追跡サービス名を省くと全サービスが混ざる
docker compose exec db sh稼働中コンテナでコマンド実行リダイレクトするときは-TでTTYを外す
docker compose downコンテナとネットワークを削除名前付きボリュームは残る。-vで削除
docker compose pullイメージ更新続けてup -dで差し替え

downとstopの違いはコンテナを削除するかどうかで、downしてもデータは名前付きボリュームに残ります。down -vは宣言したボリュームごと消すので、DBを初期化し直すとき以外は打たない習慣にしてください。

コンテナ間通信:サービス名で名前解決する

Composeはup時にプロジェクト名(既定はディレクトリ名)を冠したネットワーク<project>_defaultを作り、全サービスを接続します。このネットワークにはDocker内蔵のDNS(127.0.0.11)があり、サービス名がそのままホスト名になります。コンテナのlocalhostは自分自身のネットワーク名前空間なので、アプリからDBへはlocalhost:3306ではなくdb:3306で接続します。

コンテナ同士の通信にportsは不要です。portsはホスト(とLAN)へ公開する設定なので、DBのような内部サービスには書きません。networksをfrontendとbackendに分け、プロキシは前者、DBは後者、アプリは両方に所属させると、プロキシからDBへは直接届かなくなります。

portsで公開したポートは、DockerがiptablesのDOCKERチェーンに直接ルールを書くため、ufwでdenyしていても外から到達できます。LAN外に出さないサービスは、ufwに頼らず127.0.0.1:を付けるかports自体を書かないでください。

実例:リバースプロキシ + アプリ + DBの3層構成

~/stacks/wp/のように用途ごとのディレクトリを作り、compose.yaml・Caddyfile・.env(DB_NAME / DB_USER / DB_PASSWORD / DB_ROOT_PASSWORDの4行)を置きます。ディレクトリ名がプロジェクト名になるので、短く一意な名前にします。

compose.yaml
services:
  proxy:
    image: caddy:2
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile:ro
      - caddy_data:/data
    networks: [frontend]
    depends_on: [app]
    restart: unless-stopped

  app:
    image: wordpress:6
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: ${DB_NAME}
      WORDPRESS_DB_USER: ${DB_USER}
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
    volumes:
      - wp_data:/var/www/html
    networks: [frontend, backend]
    depends_on:
      db:
        condition: service_healthy
    restart: unless-stopped

  db:
    image: mariadb:11
    environment:
      MARIADB_DATABASE: ${DB_NAME}
      MARIADB_USER: ${DB_USER}
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql
    networks: [backend]
    healthcheck:
      test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
      interval: 10s
      timeout: 5s
      retries: 10
    restart: unless-stopped

volumes:
  caddy_data:
  wp_data:
  db_data:

networks:
  frontend:
  backend:
Caddyfile
# LAN内でHTTPのみ。公開ドメインがあれば「:80」を「wp.example.com」に変えると
# 証明書を自動取得してHTTPS化される(80/443番が外から届くことが条件)
:80 {
    reverse_proxy app:80
}

要点はdepends_onのcondition: service_healthyです。depends_onだけでは「dbコンテナが起動した」直後にappが動き出し、MariaDBの初期化が終わる前に接続して失敗します。MariaDB公式イメージ付属のhealthcheck.sh --connect --innodb_initializedをhealthcheckにすると、実際に接続できてInnoDBの初期化が終わった時点で初めてappが起動します。interval: 10s×retries: 10で初回の初期化を最大100秒ほど待つ計算です。

Caddyにしたのは、ドメイン名を書くだけで証明書の取得と更新が自動化され、設定が数行で済むからです。nginxの設定資産がある場合はnginxで問題なく、proxy_pass http://app:80;とサービス名で指定する点は同じです。chmod 600 .envのあとdocker compose config --quiet && docker compose up -dで起動します。

動作確認

dbのSTATUSに(healthy)が付き、getent hosts dbでIPが返り、WordPressの初期設定画面へ302で誘導されれば3層とも繋がっています。

terminal
$ docker compose ps
NAME         IMAGE         COMMAND                  SERVICE   CREATED          STATUS                    PORTS
wp-app-1     wordpress:6   "docker-entrypoint.s…"   app       27 seconds ago   Up 16 seconds             80/tcp
wp-db-1      mariadb:11    "docker-entrypoint.s…"   db        27 seconds ago   Up 27 seconds (healthy)   3306/tcp
wp-proxy-1   caddy:2       "caddy run --config …"   proxy     27 seconds ago   Up 16 seconds             0.0.0.0:80->80/tcp, [::]:80->80/tcp, 0.0.0.0:443->443/tcp, [::]:443->443/tcp, 443/udp, 2019/tcp

$ docker compose exec app getent hosts db
172.19.0.2      db

$ curl -sI http://localhost/ | grep -iE '^(HTTP|location)'
HTTP/1.1 302 Found
Location: http://localhost/wp-admin/install.php

つまずきポイント

症状・エラーメッセージ原因対処
permission denied while trying to connect to the Docker daemon socket at unix:///var/run/docker.sockdockerグループへの追加が現在のシェルに未反映ログインし直すかnewgrp docker
Bind for 0.0.0.0:80 failed: port is already allocatedホストのApache/nginxや別プロジェクトが80番を使用中sudo ss -ltnp | grep ':80 'で占有プロセスを特定し、止めるかportsを変える
dependency failed to start: container wp-db-1 is unhealthyhealthcheckがretries回失敗。.envの値不足か、db_dataに前回の初期化データが残っているdocker compose logs dbで原因を確認。作り直すならdown -v(データは消える)
画面に「Error establishing a database connection」WORDPRESS_DB_HOSTがlocalhost、または資格情報がMARIADB_*側と不一致ホスト名はサービス名dbにする。.env修正後にup -dで再作成

更新(pull)とバックアップ(volume)

更新はpullで新イメージを取得し、up -dで差し替え、旧イメージをimage pruneで消します。タグはlatestではなくmariadb:11のようにメジャーで固定し、意図しないメジャーアップグレードでデータ形式が変わる事故を避けます。DBだけは更新前にダンプを取ると戻せます。

terminal
docker compose pull && docker compose up -d && docker image prune -f

# DBの論理バックアップ(コンテナ内の mariadb-dump を使う)
docker compose exec -T db sh -c 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --all-databases --single-transaction' \
  > backup/db-$(date +%F).sql

# 名前付きボリュームをtarで固める(実名は docker volume ls で確認)
docker run --rm -v wp_wp_data:/data -v "$PWD/backup":/backup alpine \
  tar czf /backup/wp_data-$(date +%F).tgz -C /data .

/var/lib/mysqlを稼働中にそのままtarすると、書き込みと重なって整合性の取れないファイルになることがあります。DBはmariadb-dumpを基本にし、ファイル単位のコピーはdocker compose stop dbしてから行ってください。取ったバックアップは同じサーバーに置かず、NASやオブジェクトストレージへ複製します。

ベストプラクティスと注意点

  • 1ディレクトリ1プロジェクト。同名のディレクトリを別の場所に作るとネットワーク名・ボリューム名が衝突する
  • 内部サービスにportsを書かない。ホスト専用なら127.0.0.1:を付ける
  • 既定のjson-fileログは上限なしなので、各サービスにlogging.options.max-size: "10m"とmax-file: "3"を付ける
  • deploy.resources.limits.memoryで上限を決め、1コンテナの暴走でホストごと落ちるのを防ぐ(swarmでなくても有効)

よくある質問

Docker Composeで何ができますか?

複数コンテナで構成するアプリを1つのcompose.yamlで定義し、起動・停止・更新をまとめて行えます。ネットワークとボリュームの作成、healthcheckによる起動順制御、環境変数の注入、再起動ポリシーまで含まれるため、自宅サーバーで常時動かすWebアプリやDBの管理に向いています。

DockerとDocker Composeの違いは?

Docker(Docker Engine)はコンテナを実行する基盤で、Composeはその上で複数コンテナの構成をファイルから一括操作するツールです。Composeはcompose.yamlの内容をEngineのAPI呼び出しに変換しているだけで、執筆時点ではDocker CLIのプラグインとしてdocker composeコマンドで使います。

コンテナ同士の通信はどうやりますか?

同じcompose.yamlのサービスは自動で同じネットワークに入り、サービス名をホスト名として名前解決できます。接続先はdb:3306のようにサービス名とコンテナ側ポートで指定してください。portsはホストへの公開用で、コンテナ間の通信には不要です。

まとめ

Compose v2はdocker composeサブコマンドで、compose.yamlにversion:は書きません。人が編集するファイルはバインドマウント、プロセスが書くデータは名前付きボリュームに置き、DBにはportsを書かずサービス名で繋ぎます。起動順はcondition: service_healthyとhealthcheckで制御し、更新はpull→up -d、バックアップはダンプとtarを別の場所へ。この型にしておけば、自宅サーバー1台に複数のサービスを安全に同居させられます。

次に読む

$ subscribe --weekly
毎週月曜、企業ITとホームラボの要点を5分で。