この記事で作る構成と前提条件

この記事では、Ubuntu ServerのVPSにDocker Engine(コンテナを動かす本体)とDocker Compose(複数のコンテナを1つの設定ファイルでまとめて管理するツール)を入れます。そのうえで、compose.ymlにWebアプリ(WordPress)とデータベース(MariaDB)を書いて起動し、サーバーを再起動しても自動で立ち上がる状態にします。WordPressは動作を確かめる例として使っています。自分のアプリに置き換える場合も、手順は同じです。

必要なもの

SSHでログインでき、sudoを使える一般ユーザーが必要です。rootで直接作業すると、後半で扱うdockerグループの確認ができません。OSは、Docker公式のインストール手順が対象にしているUbuntuのバージョンにしてください。執筆時点ではUbuntu 22.04 LTSと24.04 LTSが含まれています。対象のバージョンは変わるため、Docker公式のUbuntu向けインストール手順で確認してください。

メモリはWordPressとMariaDBの構成で2GB以上を目安にしてください。1GBでも起動はします。ただしデータベースとPHPが同時にメモリを使うため、アクセスが増えるとメモリ不足でプロセスが強制終了されることがあります。ディスクは、イメージ(コンテナの元になるファイル群)とデータの置き場所として、20GB以上の空きがあると安心です。

先に知っておく4つのつまずき

この手順で失敗が多いのは次の4点です。1つ目は、Ubuntuの標準リポジトリやSnapで入れたDockerと、公式版のDockerがぶつかること。2つ目は、dockerグループに入れたユーザーが実質的にroot権限を持つこと。3つ目は、Dockerで公開したポートがファイアウォールのufwの設定を通らないこと。4つ目は、ボリュームを設定しないとコンテナを作り直したときにデータが消えることです。どれも該当する手順の中で、確認コマンドと戻し方を説明します。

手順1:競合するDockerパッケージを確認して削除する

最初に、公式版以外のDockerが入っていないかを確認します。Ubuntuには標準リポジトリのdocker.ioパッケージと、Snap版のdockerがあります。どちらかが入ったまま公式版を入れると、どのdockerコマンドが動いているのか分からなくなったり、パッケージ同士がぶつかってインストールが失敗したりします。Ubuntu Serverのインストーラーでは、初期設定の途中でSnap版Dockerを選べるため、選んだ覚えがなくても確認してください。

dpkg -l | grep -E 'docker|containerd|runc'
snap list docker
which -a docker

1行目は、aptで入っているDocker関連のパッケージを表示します。2行目でdockerが表示されたら、Snap版が入っています。3行目で複数のパスが出たら、Dockerが二重に入っています。

見つかった場合は、Docker公式の手順にある次のコマンドで削除します。入っていないパッケージは「インストールされていない」と表示されて飛ばされるので、そのまま実行して問題ありません。Snap版がある場合は、2行目も実行します。

for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done
sudo snap remove docker

apt-get removeでは、/var/lib/dockerにある既存のイメージやコンテナのデータは消えません。一方、Snap版を削除すると、Snap側に保存されていたコンテナのデータは使えなくなります。Snap版で動かしているコンテナがあるなら、削除する前にデータを外へ書き出してください。

戻し方:削除したパッケージが別の用途で必要だった場合は、sudo apt-get install docker.ioのように同じ名前で入れ直せば元に戻ります。ただし、公式版と同時に入れることはできません。どちらか一方に決めてください。

手順2:Docker公式リポジトリからDocker EngineとComposeを入れる

Dockerが配布しているaptリポジトリを登録し、そこからインストールします。最初に、リポジトリの署名を確かめるための鍵を取得します。

sudo apt-get update
sudo apt-get install 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

次に、リポジトリを登録します。コマンドの中で、OSのコードネーム(24.04ならnoble)とCPUのアーキテクチャが自動的に埋め込まれます。

echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo ${UBUNTU_CODENAME:-$VERSION_CODENAME}) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update

最後に、本体とプラグインを入れます。docker-compose-pluginを入れると、docker compose(ハイフンなし)というサブコマンドが使えるようになります。古い記事に出てくるdocker-compose(ハイフンあり)は旧版のツールで、この記事では使いません。

sudo apt-get install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
apt-cache policy docker-ce
sudo docker run hello-world
docker compose version

apt-cache policy docker-ceの出力にある取得元がdownload.docker.comになっていれば、公式リポジトリから入っています。hello-worldが「Hello from Docker!」で始まるメッセージを表示すれば、Docker Engineは正常に動いています。

戻し方:apt-get updateがNO_PUBKEYや「Release ファイルがありません」というエラーで止まった場合、鍵の取得かリポジトリの行が間違っています。sudo rm /etc/apt/sources.list.d/docker.list /etc/apt/keyrings/docker.ascで登録を消し、この手順をやり直してください。Docker自体を完全に消す場合は、sudo apt-get purge docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-pluginを実行します。そのあとsudo rm -rf /var/lib/docker /var/lib/containerdを実行すると、すべてのイメージ、コンテナ、ボリュームが消えます。データを残したい場合は、この2つ目のコマンドを実行しないでください。

手順3:dockerグループの権限を理解してから設定する

この時点では、dockerコマンドを実行するたびにsudoが必要です。ユーザーをdockerグループに追加すると、sudoなしで実行できるようになります。

sudo usermod -aG docker $USER

グループの変更は、ログインし直すまで反映されません。SSHを一度切って入り直し、次のコマンドで確認します。

groups
docker ps

groupsの出力にdockerがあり、docker psが空の一覧を表示すれば設定は完了です。permission denied while trying to connect to the Docker daemon socketと表示される場合は、まだ再ログインが反映されていません。

注意点があります。dockerグループに入ったユーザーは、ホストのルートディレクトリをコンテナにマウントするなどの方法で、パスワードなしでroot権限を得られます。つまり、dockerグループへの追加はsudoをパスワードなしで許可するのとほぼ同じ意味を持ちます。共有サーバーや、複数の人がログインするサーバーでは、ユーザーを追加せずに毎回sudo dockerを使うほうが安全です。

戻し方:sudo gpasswd -d $USER dockerでグループから外し、再ログインします。

手順4:compose.ymlとパスワード用の.envファイルを作る

アプリ用のディレクトリを作り、パスワードを書いた.envファイルを用意します。Composeは同じディレクトリにある.envを自動で読み込み、compose.yml内の${...}をその値に置き換えます。こうすると、compose.ymlにパスワードを直接書かずに済みます。

mkdir -p ~/apps/wordpress
cd ~/apps/wordpress
printf 'DB_PASSWORD=%s\nDB_ROOT_PASSWORD=%s\n' "$(openssl rand -hex 16)" "$(openssl rand -hex 16)" > .env
chmod 600 .env

続いて、同じディレクトリにcompose.ymlを作り、次の内容を書きます。

services:
  db:
    image: mariadb:11.4
    restart: unless-stopped
    environment:
      MARIADB_DATABASE: wordpress
      MARIADB_USER: wordpress
      MARIADB_PASSWORD: ${DB_PASSWORD}
      MARIADB_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
    volumes:
      - db_data:/var/lib/mysql

  web:
    image: wordpress:6
    restart: unless-stopped
    depends_on:
      - db
    environment:
      WORDPRESS_DB_HOST: db
      WORDPRESS_DB_NAME: wordpress
      WORDPRESS_DB_USER: wordpress
      WORDPRESS_DB_PASSWORD: ${DB_PASSWORD}
    ports:
      - "127.0.0.1:8080:80"
    volumes:
      - wp_data:/var/www/html

volumes:
  db_data:
  wp_data:

この設定のうち、後の手順に関わる箇所は3つあります。restart: unless-stoppedは、Dockerが起動したときに、このコンテナも自動で起動させる設定です。portsの先頭にある127.0.0.1は、ポートをサーバー内部からだけ使えるようにする指定で、理由はufwの節で説明します。volumesは、データをコンテナの外に保存する設定で、理由はボリュームの節で説明します。WORDPRESS_DB_HOST: dbのdbはサービス名です。Composeが作るネットワークの中では、サービス名がそのままホスト名として使えます。そのため、データベースのポートを外部に公開する必要はありません。

手順5:コンテナを起動して状態を確かめる

docker compose config --quiet
docker compose up -d
docker compose ps
docker compose logs -f web

1行目は、compose.ymlの書式と.envの読み込みを検証するコマンドで、問題がなければ何も表示されません。up -dを実行すると、イメージをダウンロードしてからバックグラウンドで起動します。psで2つのサービスの状態がrunningまたはUpになっていれば起動しています。ログの表示はCtrl+Cで抜けられ、抜けてもコンテナは止まりません。

ポートは127.0.0.1だけに公開しているため、手元のブラウザから直接は開けません。動作を確かめるには、手元のPCからSSHのポート転送を使い、ssh -L 8080:127.0.0.1:8080 ユーザー名@サーバーのIPで接続します。その状態で、手元のブラウザからhttp://localhost:8080を開くと、WordPressの初期設定画面が表示されます。

Dockerのポート公開がufwの設定を通らない問題を防ぐ

ufwで受け付ける通信を絞っていても、Dockerで公開したポートには外部から接続できます。Dockerはポートを公開するときに、iptables(Linuxのパケットフィルタ)に独自のルールを追加します。コンテナ宛ての通信はこのルールで転送されるため、ufwの許可・拒否のルールを通りません。そのため、ports: - "8080:80"と書くと、ufw deny 8080を設定してもインターネットから接続できてしまいます。Docker公式のドキュメントにも、ufwとは併用できないと書かれています。

確認するには、サーバー上で次のコマンドを実行します。

docker ps --format '{{.Names}}  {{.Ports}}'
sudo ss -tlnp

0.0.0.0:8080->80/tcpや[::]:8080と表示されたポートは、すべてのネットワークに公開されています。127.0.0.1:8080->80/tcpと表示されていれば、サーバー内部からしか接続できません。念のため、サーバーの外にある手元のPCからcurl -I http://サーバーのIP:8080を実行し、接続できないことも確かめてください。

インターネットに公開する場合は、コンテナのポートは127.0.0.1に限定したままにします。そのうえで、ホストにnginxやCaddyなどのリバースプロキシ(外部からのリクエストを受けて内部のアプリに中継するサーバー)を入れ、ufwでは80番と443番だけを許可する構成にします。この構成なら、ufwの設定どおりに通信を制御できます。ufwを有効にする前に、sudo ufw allow OpenSSHでSSHを許可してください。許可しないまま有効にすると、SSHで接続できなくなります。

Dockerの設定ファイル/etc/docker/daemon.jsonで"iptables": falseを指定する方法も、検索するとよく見つかります。これを設定すると、コンテナから外部への通信など、Dockerのネットワーク機能そのものが動かなくなることがあるため、初心者向けの対策としては勧めません。

戻し方:すでに0.0.0.0で公開してしまったポートは、compose.ymlのportsを127.0.0.1:から始まる形に書き換えます。そのあとdocker compose up -dを実行すれば、新しい設定でコンテナが作り直され、外部からの接続はすぐに遮断されます。

ボリュームを使ってデータが消えないようにする

コンテナの中に書き込まれたデータは、そのコンテナを削除すると一緒に消えます。イメージを新しいバージョンに更新すると、コンテナは削除されて作り直されます。そのため、ボリュームを設定していないと、更新のたびにデータベースの中身が消えます。この記事のcompose.ymlでは、db_dataとwp_dataという名前付きボリューム(Dockerが管理する保存領域)を使っているため、コンテナを作り直してもデータは残ります。

docker volume ls
docker inspect -f '{{json .Mounts}}' $(docker compose ps -q db)

1行目の結果に、wordpress_db_dataのように、ディレクトリ名から始まるボリュームが表示されれば作成されています。2行目の結果で、Destinationが/var/lib/mysql、Typeがvolumeになっていれば、データベースのデータはボリュームに保存されています。

削除系のコマンドにも注意が必要です。docker compose downはコンテナだけを削除し、ボリュームは残します。docker compose down -vはボリュームも削除するため、データがすべて消えます。-vは、データを捨ててもよいときだけ付けてください。

戻し方:ボリュームを設定せずに運用を始めてしまった場合は、コンテナを削除する前にデータを書き出します。次の1行目でデータベースをbackup.sqlに書き出します。compose.ymlにボリュームを追加してdocker compose up -dを実行したら、2行目で書き戻します。一度削除したコンテナのデータは取り戻せないため、書き出しは必ず先に行ってください。

docker compose exec -T db sh -c 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" --all-databases' > backup.sql
docker compose exec -T db sh -c 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD"' < backup.sql

再起動後に自動で起動することを確かめる

コンテナが自動で起動するには、Docker本体がOSの起動時に立ち上がり、そのあと各コンテナのrestart設定が効く、という2つの条件が必要です。Ubuntuでは、インストールした時点でDockerの自動起動が有効になっています。念のため確認し、無効になっていれば有効にします。

systemctl is-enabled docker containerd
sudo systemctl enable docker.service containerd.service
sudo reboot

再起動したらSSHで入り直し、cd ~/apps/wordpress && docker compose psを実行します。2つのサービスが起動していれば、作業は完了です。unless-stoppedには「手動で止めたコンテナは再起動後も止めたままにする」という性質があります。再起動の前にdocker compose stopを実行していた場合は、起動していなくても正常な動作です。docker compose up -dで起動し直してください。

公開前に確認する3つのポイント

この構成をインターネットに公開する前に、次の3点を確認してください。外部の端末からcurlを実行し、コンテナのポートに直接接続できないこと。docker volume lsの結果に、データベース用のボリュームがあること。再起動したあと、操作しなくてもdocker compose psで全サービスが起動していること。3つとも確認できたら、次はリバースプロキシでHTTPSを設定し、ボリュームの定期バックアップを用意してください。バックアップには、上のmariadb-dumpのコマンドをcronで毎日実行する方法が使えます。