DockerとNginx、SSLでVPSにアプリをデプロイする:本番環境対応ガイド

DevOps tutorial - IT technology blog
DevOps tutorial - IT technology blog

深夜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が行うこと:

  1. HTTPチャレンジによるドメイン所有権の確認
  2. Let’s Encrypt証明書の取得
  3. 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時に良いアイデアに思えたからではなく、具体的な理由があるときだけ追加してください。

Share: