深夜2時、アプリが落ちている
新バージョンをプッシュしたばかりです。チームメンバーがコンテナなしで直接VPSにデプロイしました。tmuxセッション上でベタな node server.js を走らせただけです。今やプロセスは落ちていて、ポート80は別の何かに占有され、SSL証明書は3日前に誰も気づかないまま期限切れになっています。心当たりはありませんか?
私も何度も経験しました。4つの本番サービスでDocker + Nginx + Certbotに切り替えてから、そんな深夜の緊急対応はなくなりました。どのようにセットアップしたかを詳しく紹介します。
根本原因:ベアメタルデプロイが失敗する理由
解決策に入る前に、「とりあえず動かす」アプローチでどこが問題になるかを理解しておく価値があります:
- プロセス管理が手動 — プロセスがクラッシュしても、自動で再起動するものが何もない。
- ポートの競合 — 隔離なしで2つのサービスがポート80や3000を奪い合う。
- 依存関係の地獄 — サーバーはNode 16、コードはNode 20。問題が静かに発生する。
- SSLは後回し — クライアントの前でブラウザ警告が出て痛い目を見るまで、HTTPSを追加しないチームがほとんど。
Dockerが隔離を担い、NginxがルーティングとSSL終端を処理し、Certbotが証明書の更新を自動化します。各ツールには1つの役割があります。組み合わせることで、最悪のタイミングで発生する障害をカバーします。
選択肢を比較する
よくある3つのデプロイ戦略を、正直に評価します:
オプション1:ベアメタル(コンテナなし)
- 始めるのは速いが、メンテナンスが苦痛
- ロールバックの手段がなく、依存関係の競合が時間とともに蓄積する
- 趣味のプロジェクトなら問題ないが、実際のサービスには危険
オプション2:Dockerのみ(Nginxなし)
- 隔離は解決するが、ポート80/443を直接公開する必要がある
- SSL管理が一元化されず、1つのVPSで複数のアプリをホストするのが難しい
オプション3:Docker + Nginx + Certbot(このガイド)
- Nginxがリバースプロキシとして機能 — アプリは内部ポートのみでリッスンする
- CertbotがLet’s EncryptのSSLを自動更新付きで管理する
- クリーンな分離:新しいNginx設定ブロックを追加するだけで新しいアプリを追加でき、インフラ変更は不要
- 14か月以上、5つのサービスでこの構成を運用していますが、証明書の更新を意識したことはなく、アプリ間のポート競合もゼロです
私たちが構築するのはオプション3です。
前提条件
- Ubuntu 22.04を動かしているVPS(Debianベースのディストリビューションならどれでも可)
- VPSのIPを指しているドメイン名(
Aレコードの設定が必要) - sudo権限付きのSSHアクセス
- アプリに
Dockerfileがあること(なければ追加します)
ステップ1:VPSにDockerをインストールする
# 依存パッケージの更新とインストール
sudo apt update && sudo apt install -y ca-certificates curl gnupg
# Docker公式GPGキーの追加
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
# Dockerリポジトリの追加
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Dockerのインストール
sudo apt update && sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
# sudoなしでDockerを実行できるようユーザーを追加する
sudo usermod -aG docker $USER
newgrp docker
ステップ2:アプリをコンテナ化する
Dockerfileがまだない? Node.js用の最小限のものを紹介します。Python、Goなど、使用している言語に合わせて修正してください。
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]
VPSに触れる前に、ローカルでビルドとテストを行います:
docker build -t myapp .
docker run -p 3000:3000 myapp
http://localhost:3000にアクセスして動作を確認してください。この手順を省かないでください — ローカルのコンテキストなしにVPSでデバッグするのは本当に辛いです。
ステップ3:Docker Composeのセットアップ
VPS上にプロジェクトディレクトリを作成してdocker-compose.ymlを追加します。これでコンテナ設定の再現性が確保され、バージョン管理が可能になります。
mkdir -p /opt/myapp && cd /opt/myapp
# docker-compose.yml
services:
app:
image: myapp:latest
restart: always
ports:
- "127.0.0.1:3000:3000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgres://user:pass@db:5432/mydb
networks:
default:
name: web
portsのバインディングは重要です:127.0.0.1:3000:3000はコンテナをローカルホストのみに公開します。Nginx(システムサービスとして動作)はそこ経由でアクセスします。ポート3000は直接パブリックインターネットに晒されません。
restart: alwaysが深夜のクラッシュ問題を解決します。コンテナが落ちてもDockerが自動的に再起動してくれます — tmuxセッションは不要です。
ステップ4:Nginxのインストールと設定
sudo apt install -y nginx
sudo systemctl enable nginx
ドメイン用のサイト設定ファイルを作成します:
sudo nano /etc/nginx/sites-available/myapp
server {
listen 80;
server_name yourdomain.com www.yourdomain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_cache_bypass $http_upgrade;
}
}
# サイトを有効化する
sudo ln -s /etc/nginx/sites-available/myapp /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginx
アプリがHTTPで接続できるようになっているはずです。SSLに進む前にcurl http://yourdomain.comで確認してください。この確認を省かないでください — HTTP設定が壊れた状態でSSLのトラブルシューティングをするのは最悪の体験です。
ステップ5:CertbotでSSLを無料で追加する
sudo apt install -y certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com -d www.yourdomain.com
Certbotが行うこと:
- HTTPチャレンジによるドメイン所有権の確認
- Let’s Encrypt証明書の取得
- NginxのconfigにHTTPSを追加し、HTTP → HTTPSリダイレクトを自動設定
自動更新タイマーをテストします — このステップだけで証明書期限切れの問題を防げます:
sudo certbot renew --dry-run
Certbotは1日2回の更新チェックを実行するsystemdタイマーをインストールします。証明書は有効期限の30日前に自動的に更新されます。もう手動で操作する必要はありません。
ステップ6:アプリコンテナのデプロイ
イメージをVPSに転送します。レジストリなしで最も手っ取り早い方法:
# ローカルマシン上で
docker save myapp:latest | gzip > myapp.tar.gz
scp myapp.tar.gz user@your-vps-ip:/opt/myapp/
# VPS上で
cd /opt/myapp
docker load < myapp.tar.gz
docker compose up -d
CI/CDパイプラインの場合は、Docker Hubやプライベートレジストリにプッシュして、VPS上でプルする方が良いでしょう — 繰り返しのデプロイにはよりクリーンで高速です:
docker pull youruser/myapp:latest
docker compose up -d --pull always
ステップ7:すべての動作確認
# コンテナの状態を確認する
docker compose ps
# リアルタイムログを追う
docker compose logs -f app
# Nginxの状態を確認する
sudo systemctl status nginx
# SSLが機能しているか確認する
curl -I https://yourdomain.com
HTTP/2 200と有効なSSLヘッダーが表示されるはずです。リダイレクトループが発生していますか?NginxがX-Forwarded-Protoを正しく渡しているか、アプリ側で二重リダイレクトが起きていないか確認してください。
同じVPSに2つ目のアプリを追加する
このインフラ構成がその真価を発揮するのがここです。同じサーバーで別のサービスが必要?別のポートを指す新しいNginx設定、もう一つのDocker Composeスタック、新しいドメイン用のCertbot。ポートの競合なし、依存関係の共有なし、問題なし。
sudo nano /etc/nginx/sites-available/anotherapp
# proxy_passをhttp://localhost:4000に向ける
sudo certbot --nginx -d anotherapp.com
docker compose -f /opt/anotherapp/docker-compose.yml up -d
本当に頼れるスタック
Dockerが隔離を担い、Nginxがルーティングを処理し、CertbotがSSLを管理します。この組み合わせが、人々を夜も眠れなくさせる問題の90%に対処します。各コンポーネントは1つの役割を持ち、それをしっかりこなします。
何か問題が起きたときは、ログは2つの明確な場所にあります:docker compose logsと/var/log/nginx/。ロールバックは以前のイメージタグでdocker compose down && docker compose up -dを実行するだけです。このスタックから始め、シンプルに保ち、複雑さはリリース前夜の11時に良いアイデアに思えたからではなく、具体的な理由があるときだけ追加してください。

