提供を受けた検証について:本記事の手順の動作確認には、エックスサーバー株式会社から無償提供を受けた「XServer VPS クラウド」の利用環境(VPS〈Linux〉・メモリ4GB/vCPU 4コア・ディスク50GB/1ヶ月)を使っています。記事の内容・評価は当サイトが独自に行っており、提供元の確認・承認を受けていません。
VPSでWebアプリを本番公開するなら、Docker Composeで「nginx+certbot+アプリ」の3サービス構成が定番です。Let’s Encryptで無料のSSL証明書を取得し、自動更新まで設定します。最大の難所は「鶏と卵問題」(証明書の取得にHTTPが必要)で、初回だけ仮の証明書でnginxを起動してから本物に差し替えると解けます。当サイトの検証環境(XServer VPS クラウド/Ubuntu 24.04)で1回目に通したときの主な工程の所要は、1つずつ測って足すと約1分でした(内訳はStep1とStep3に記載。DNSの反映や防火壁の設定を待つ時間、ファイルの作成は含みません)。
「ローカルでは動くアプリを、独自ドメイン+HTTPSで公開したい」——VPSとDockerを使えば、再現性が高く壊れにくい本番環境を自分で組めます。本記事ではインフラエンジニアの視点で、構成・手順・つまずきどころを解説します。手順は2026年9月に実機で確かめたものです(Step1は、OSを再インストールしたあと、Dockerを入れていない状態から実行。Step2〜4は、一度通したあと、新しい作業ディレクトリで本記事のとおりに連続して実行し直しました)。
動作確認した環境(2026年9月12日)
- VPS:XServer VPS クラウド 4GBプラン(vCPU 4コア・メモリ4GB・NVMe SSD 50GB)※無償提供
- OS:Ubuntu 24.04 LTS
- Docker 29.1.3(Ubuntuの
docker.io)・Docker Compose 2.40.3(docker-compose-v2) - コンテナ:nginx 1.31.5(
nginx:alpine)・certbot 5.8.0(certbot/certbot)・アプリ(動作確認用の最小構成・Python 3.12)
この記事で作る構成
作るのは、1台のVPS上にDockerで3つのコンテナを立てる構成です。Nginxがリバースプロキシとして全リクエストを受け、HTTPSを終端してアプリへ転送。certbotがSSL証明書を取得・更新します。
ポイントはNginxを「玄関」にすること。外部からの通信はすべてNginxが受け、内部のアプリ(8000番など)はexposeのみで外に晒しません(検証環境では、ホストが8000番を待ち受けておらず、docker compose port app 8000 にも公開の割り当てが無いことを確認しました)。これでアプリのフレームワーク(FastAPI・Django・Node.jsなど)を問わず、同じ型で公開できます。
Step1:VPSの初期設定
まずはVPSにDockerを入れ、ファイアウォールで必要なポートだけを開けます。公開に使うのは22(SSH)・80(HTTP)・443(HTTPS)の3つです。
# ファイアウォールで必要なポートを開放(有効化の確認には y で答える)
sudo ufw allow 22,80,443/tcp
sudo ufw enable
# Docker と Compose を導入(Ubuntu 24.04 の標準のパッケージ名)
sudo apt update && sudo apt install -y docker.io docker-compose-v2
sudo systemctl enable --now docker
# sudo なしで docker を使えるようにする(実行後、一度ログアウトしてログインし直す)
sudo usermod -aG docker $USER
所要時間(検証環境):ファイアウォールの設定 0.4秒・Docker と Compose の導入(apt install)14.5秒。
⚠️ 検証で実際につまずいた2点
docker-compose-pluginは Ubuntu 24.04 の標準のリポジトリにありません。指定するとE: Unable to locate package docker-compose-pluginで止まります(当サイトの検証で確認)。標準のリポジトリでの名前はdocker-compose-v2です。docker-compose-pluginは、Docker公式のリポジトリを追加したときのパッケージ名です。- docker グループに入れないと、sudo なしの
docker composeは権限エラーになります。usermodのあと、ログインし直すと反映されます(検証では再ログイン後に sudo なしで起動できました)。
続いてドメインのDNS Aレコードを、VPSのIPアドレスに向けておきます(反映はdig ドメイン名やnslookup ドメイン名で確認)。VPS事業者の管理画面にも防火壁(パケットフィルター)がある場合は、そちらでも22・80・443を許可します。検証環境では、サーバーのufwで許可しても、管理画面の防火壁で許可するまで80・443が外から届きませんでした。
本番公開にはVPSが1台必要なので、まだ用意していなければ初期費用0円(ConoHa公式の料金ページ/2026年8月時点)で試せるConoHa VPSあたりが始めやすいです。
Step2:Docker Composeで3サービスを定義
nginx・certbot・appの3サービスを1つのdocker-compose.ymlにまとめます。証明書のボリュームをnginxとcertbotで共有するのがコツです。作業用のディレクトリ(例:~/app)に、次の4つのファイルを置きます。
① docker-compose.yml
services:
nginx:
image: nginx:alpine
ports: ["80:80", "443:443"]
volumes:
- ./data/nginx:/etc/nginx/conf.d
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
certbot:
image: certbot/certbot
volumes:
- ./data/certbot/conf:/etc/letsencrypt
- ./data/certbot/www:/var/www/certbot
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
app:
build: .
expose: ["8000"]
certbotのentrypointは「12時間ごとに証明書の更新を試みる」ループです。Let’s Encryptの証明書は90日で期限切れになるため、この自動更新を入れておきます(nginxに読み込み直させる点はStep3の最後で補います)。
② nginx の設定(data/nginx/app.conf)=example.com を自分のドメインに置き換えます。
server {
listen 80;
server_name example.com;
location /.well-known/acme-challenge/ { root /var/www/certbot; }
location / { return 301 https://$host$request_uri; }
}
server {
listen 443 ssl;
http2 on;
server_name example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location / {
proxy_pass http://app:8000;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-Proto https;
}
}
80番は「ACMEチャレンジの置き場所」と「HTTPSへの転送」だけを受け持ち、443番でHTTPSを終端してアプリへ転送します。
③ アプリの Dockerfile=自分のアプリのものに置き換えます。以下は動作確認用の最小例です(同じ場所に index.html を置く)。
FROM python:3.12-alpine
WORKDIR /srv
COPY index.html .
EXPOSE 8000
CMD ["python", "-m", "http.server", "8000"]
④ .dockerignore=data の1行だけを書きます。composeのbuild: .は作業用ディレクトリ全体をビルドに渡すため、証明書などのデータをアプリのビルドに含めないようにします。
💡 検証用のサブドメインで試すなら
443番の server に add_header X-Robots-Tag "noindex, nofollow" always; を足しておくと、検索エンジンに登録しないよう指示できます(当サイトの検証ではこの形で公開し、応答ヘッダーに出ていることを確認しました)。
Step3:SSL証明書を取得する(鶏と卵問題)
最大の難所がここです。HTTPSを有効にするには証明書が必要ですが、Let’s Encryptは証明書を発行する前にHTTPでドメイン所有を確認します。「証明書がないとNginxが起動しない、でも起動しないと証明書が取れない」という循環が起きます。
解決策は、初回だけ仮の証明書でNginxを起動→本物の証明書を取得→差し替えという順に進めることです。検証で実際に通した手順は次のとおりです(作業用ディレクトリで実行・DOMAIN は自分のドメインに置き換える)。
DOMAIN=example.com
# 1) 仮の証明書(自己署名・有効1日)を置く
sudo mkdir -p data/certbot/conf/live/$DOMAIN
docker compose run --rm --entrypoint "openssl req -x509 -nodes -newkey rsa:2048 -days 1 -keyout /etc/letsencrypt/live/$DOMAIN/privkey.pem -out /etc/letsencrypt/live/$DOMAIN/fullchain.pem -subj /CN=localhost" certbot
# 2) nginx とアプリを起動(80番で ACME チャレンジに答えられる状態にする)
docker compose up -d nginx app
# 3) 仮の証明書を消し、まず練習用(staging)で取得できるか試す
sudo rm -rf data/certbot/conf/live/$DOMAIN data/certbot/conf/archive/$DOMAIN data/certbot/conf/renewal/$DOMAIN.conf
docker compose run --rm --entrypoint "certbot certonly --webroot -w /var/www/certbot -d $DOMAIN --register-unsafely-without-email --agree-tos --non-interactive --staging" certbot
# 4) 練習が通ったら、練習の証明書を消して本番を取得
sudo rm -rf data/certbot/conf/live/$DOMAIN data/certbot/conf/archive/$DOMAIN data/certbot/conf/renewal/$DOMAIN.conf
docker compose run --rm --entrypoint "certbot certonly --webroot -w /var/www/certbot -d $DOMAIN --register-unsafely-without-email --agree-tos --non-interactive" certbot
# 5) nginx に本物の証明書を読み込ませ、更新ループの certbot を起動
docker compose exec nginx nginx -s reload
docker compose up -d certbot
- 練習用(staging)を先に通すのは、本番の発行には回数の制限があるためです(Let’s Encryptの発行制限)。設定の誤りで失敗を重ねると、しばらく発行できなくなります。
- 検証では、メールアドレスを登録せずに取得しました(
--register-unsafely-without-email)。登録する場合は--email 自分のアドレスに置き換えます。--agree-tosは Let’s Encrypt の利用規約への同意です。 - 所要時間(検証環境):仮の証明書 12.9秒(certbot のイメージ取得を含む)・起動 8.6秒(アプリのビルドを含む)・練習用 11.0秒・本番 16.1秒。
ACMEチャレンジの仕組み
Let’s Encryptは、指定パス(/.well-known/acme-challenge/)に置いたファイルにHTTPでアクセスして「このドメインを本当に管理しているか」を確認します(HTTP-01チャレンジ)。だからNginxは先に80番で起動している必要があるのです。
⚠️ 更新した証明書は、nginx が読み込み直すまで使われません
certbot コンテナは期限が近づくと証明書を更新しますが、nginx は起動時と読み込み直し時にしか証明書を読みません。定期的に nginx へ読み込み直させる仕組みを入れておきます。
# 例:毎日3時に nginx へ読み込み直させる(crontab -e で追加・~/app は作業用ディレクトリ)
0 3 * * * cd ~/app && docker compose exec -T nginx nginx -s reload
※ 当サイトの検証で確かめたのは次の2点です。① certbot renew --force-renewal で更新を強制すると、nginx は更新前の証明書を出し続け(シリアル番号で確認)、nginx -s reload のあとに新しい証明書へ切り替わりました。② 上の cron の行を、実行時刻だけ直近に変えて登録すると、人の手を介さずに nginx が読み込み直しました(nginx のログに signal 1 (SIGHUP) received と記録)。期限が近づいたときの certbot の自動更新は、検証期間内には起きないため確かめていません。
Step4:本番起動と動作確認
ブラウザで鍵マークが表示され、HTTPでアクセスしてもHTTPSにリダイレクトされれば成功です。2回目以降の起動は docker compose up -d だけです。
# コンテナを起動(2回目以降)
docker compose up -d
# 動作確認(HTTPS応答とリダイレクト)
curl -I https://あなたのドメイン
curl -I http://あなたのドメイン
# アプリの応答内容(本文)も確認
curl https://あなたのドメイン/
docker compose logs -f nginx
検証環境では、Step2〜4を新しい作業ディレクトリで本記事のとおりに連続して実行し直し(Step3〜4の所要は、練習用と本番の発行を含めて26.2秒)、外部から次の5点を確かめました。
- HTTPSで
200、証明書の検証も成功 - HTTPで
301、転送先はhttps://ドメイン/ - アプリの応答内容が返る(
curl https://ドメイン/で本文を取得し、置いた index.html の内容と一致) - 証明書のホスト名が自分のドメイン、発行者が Let’s Encrypt、有効期限が発行から90日
- 3つのコンテナがすべて稼働中(
docker compose ps)
よくあるトラブルと対処
証明書の取得で失敗したら、まずDNSの反映と、80番が外から届くか(サーバーのufwと、事業者の管理画面の防火壁の両方)を確かめます。
| 症状 | 主な原因と対処 |
|---|---|
Unable to locate package docker-compose-plugin |
Ubuntuの標準のリポジトリにないパッケージ名。docker-compose-v2 を入れる(Step1) |
sudoなしのdockerが権限エラー |
docker グループに入っていない。sudo usermod -aG docker $USER のあと、ログインし直す |
| 証明書取得に失敗 | DNS Aレコードが未反映、または80番が塞がっている。digで反映を確認し、ufwと管理画面の防火壁を確認 |
| 502 Bad Gateway | Nginxのproxy_passの向き先(アプリのコンテナ名・ポート)が誤り |
| 更新されず期限切れ | certbotコンテナが停止している(docker compose psで確認)。または、nginxが読み込み直していない |
| スクリプトの途中から先が実行されない | 手順を標準入力で流し込む(bash -s など)と、docker compose run が残りの行を読み込んでしまう。docker compose run の行の末尾に </dev/null を付けて標準入力を切る(検証では、これで最後まで実行されました。-T を付けても止まりませんでした) |
本番公開まで自動化したいなら、コードのpushで自動デプロイするGitHub Actionsでの自動デプロイに発展させるのもおすすめです。
まとめ:型を覚えれば公開は怖くない
VPS+Docker+Nginx+Let’s Encryptの構成は、一度型を覚えればどんなWebアプリでも同じ手順で本番公開できます。鍵は「Nginxを玄関にする」「鶏と卵問題は仮の証明書で先に起動して解く」「証明書は自動更新し、nginxに読み込み直させる」の3点。VPS選びで迷うならVPS3社比較も参考にしてください。Docker自体をしっかり学びたいなら、ハンズオン講座が近道です。
よくある質問(FAQ)
SSL証明書は無料で使えますか?
どんなWebアプリでも公開できますか?
「鶏と卵問題」とは何ですか?
どのくらいのスペックのVPSが必要ですか?
docker stats --no-stream で2回測ったところ、nginx・certbot・動作確認用の最小アプリの3つのコンテナの使用メモリは合計19.7〜21.2MiB(nginx 5.0〜6.5・certbot 2.2・アプリ 12.4〜12.5MiB)でした。必要なスペックはアプリの規模で大きく変わるため、まずは小さめのプランで公開し、使用状況を見て増強するのが安全です。

コメント