Xserver VPS に Docker Compose で Next.js をデプロイする手順【スタンドアロン出力】

VPSにNext.jsを本番デプロイするとき、多くの記事は next start をそのまま動かす構成を紹介している。しかしその方法では、node_modules を含む全ファイルをコンテナにコピーすることになり、Dockerイメージが1GB近くになることも珍しくない。
Next.jsには output: 'standalone' という設定があり、これを使うと本番実行に必要なファイルだけを含む軽量なビルド出力が得られる。.next/standalone/server.js を node で直接実行するシンプルな構成で、イメージサイズを数百MB単位で削減できる。
この記事では、Xserver VPSにUbuntu 24.04でDocker Composeを使い、Next.jsをスタンドアロン出力で本番デプロイする手順を一から解説する。静的ファイルをNginxで直配信する構成・Xserver VPS特有のパケットフィルター設定・多くの記事が見落としている HOSTNAME 環境変数の設定まで含めて書く。
この記事でわかること
① Xserver VPSの特徴とConoHaとの違い(データ転送量・NVMe SSD・価格)
② Ubuntu 24.04でのサーバー初期設定(SSH・ufw・パケットフィルター)
③ Docker & Docker Compose のインストール手順(2026年版)
④ output: 'standalone' を使ったマルチステージDockerfileの正しい書き方
⑤ Nginxで /_next/static/ を直配信するDocker Compose構成
この記事の対象読者
- Xserver VPSを契約済み、またはこれから契約する予定のエンジニア
- Next.jsアプリを本番VPSで動かしたい
next startではなくコンテナ化した構成で運用したい- 静的ファイルをNginxで直配信してパフォーマンスを上げたい
逆に、大量トラフィックやオートスケールが必要な用途はECS FargateやCloud Runのようなマネージドコンテナサービスの方が適している。VPS固定リソースのシンプルな運用向けの構成だ。
なぜ Xserver VPS を選ぶのか
Xserver VPSを選ぶ理由として最も大きいのは、データ転送量が無制限という点だ。
ConoHa VPSは共有100Mbps回線で「他のユーザーへの影響が出るレベルのトラフィック」が発生すると帯域が500kbps前後まで絞られる仕様がある(実体験はConoHa VPS Docker記事に書いた)。Next.jsアプリは画像・JS・CSSなどの静的アセットを配信するため、アクセスが増えてきたときの帯域制限は痛手になる。Xserver VPSはデータ転送量無制限を明示しており、突然の帯域制限を気にせず運用できる。
また、全プランにNVMe SSDのRAID0構成が採用されており、DockerのビルドやNext.jsのファイルI/O処理が速い。
| Xserver VPS | ConoHa VPS | |
|---|---|---|
| ストレージ | NVMe SSD(RAID0) | SSD |
| データ転送量 | 無制限 | 制限あり(共有100Mbps) |
| CPU | AMD EPYC™ 3世代 | 非公開 |
| 2GB / 36ヶ月 | 約¥990/月 | 約¥1,210/月 |
| 4GB / 36ヶ月 | 約¥1,980/月 | 約¥2,420/月 |
価格もXserver VPSの方が安く、スペック面でも優位性がある。
VPS各社の詳しい比較はVPS比較記事にまとめているので、まだVPSを選定中の場合はそちらも参照してほしい。
前提条件
この記事では以下の環境を前提に書く。
| 項目 | 内容 |
|---|---|
| OS | Ubuntu 24.04 LTS |
| Docker Engine | 26.x(2026年5月時点) |
| Docker Compose | v2.x(docker compose コマンド) |
| Node.js | 20 LTS(Dockerイメージ内) |
| フレームワーク | Next.js 14 / 15(App Router対応) |
| ローカル環境 | macOS または Linux |
Docker Compose v1(docker-compose)は非推奨
docker-compose(ハイフンあり)コマンドはすでに非推奨・EOL。2026年時点ではdocker compose(スペース区切り)が標準だ。古い記事の手順をコピーするときは要注意。
STEP 1:VPS を契約・OS を選択する
プラン選択
Next.jsアプリ単体なら2GBプランから始めれば十分動く。画像処理や複数コンテナを同居させる場合は4GBプランが安心だ。Xserver VPSはコントロールパネルからオンラインでスケールアップできるため、まず2GBで様子を見て必要になったら変更するのが無駄がない。
OS は Ubuntu 24.04 LTS を選ぶ
Xserver VPSのOSテンプレートはUbuntu・AlmaLinuxなど複数から選べる。Dockerの公式サポートが最も手厚く、情報量も多いのはUbuntuだ。24.04 LTSを選べばセキュリティアップデートが2029年まで保証される。
アプリイメージは選ばない
Xserver VPSにはWordPressやLAMPがプリインストールされた「アプリイメージ」がある。Dockerで自前管理したい場合は素のUbuntuを選ぶこと。アプリイメージを選ぶと既存のApache等が起動しており、80/443ポートが競合する。
STEP 2:サーバーの初期設定をする
Xserver VPSでサーバーを作成すると、rootユーザーのパスワードログインが有効な状態で起動する。以下の順で初期設定を行う。
2-1. SSH公開鍵をコントロールパネルに登録する
ローカルマシンで鍵ペアを生成する。
ssh-keygen -t ed25519 -C "your_email@example.com"
# ~/.ssh/id_ed25519 と id_ed25519.pub が生成される
cat ~/.ssh/id_ed25519.pub
# 表示された公開鍵をコピーしてXserverコンパネに登録するXserver VPSのコントロールパネル → 「SSH Key」→「SSH Keyを登録」で公開鍵を登録しておくと、サーバー作成時に自動で設定される。
2-2. root でログインして一般ユーザーを作る
ssh root@<サーバーのIPアドレス>一般ユーザーを作成してsudo権限を付与する。
adduser deploy
usermod -aG sudo deploy
# rootの公開鍵を新しいユーザーにコピー
mkdir -p /home/deploy/.ssh
cp ~/.ssh/authorized_keys /home/deploy/.ssh/
chown -R deploy:deploy /home/deploy/.ssh
chmod 700 /home/deploy/.ssh
chmod 600 /home/deploy/.ssh/authorized_keys2-3. SSH設定を強化する
vim /etc/ssh/sshd_config# rootログインを禁止
PermitRootLogin no
# パスワード認証を無効化(公開鍵のみ許可)
PasswordAuthentication no
# ポートをデフォルトから変更(任意だが推奨)
Port 2222systemctl restart sshd切断前に必ず別ターミナルで接続確認
sshd_configを変更した後、現在の接続を切る前に別のターミナルから新しい設定で接続できることを確認すること。設定ミスのまま切断するとVPSにアクセスできなくなる。
2-4. ufw でファイアウォールを設定する
# SSHポートを先に許可してから有効化する
sudo ufw allow 2222/tcp
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw enable
sudo ufw status2-5. Xserver VPS のパケットフィルターを設定する
Xserver VPSにはコントロールパネル上に独自の「パケットフィルター」機能がある。ufwに加えてコントロールパネル側でもポートを開放しないと外部からアクセスできない。
コントロールパネル → サーバー詳細 → 「パケットフィルター」→「パケットフィルターの設定」から以下を許可する。
| 対象 | プロトコル | ポート |
|---|---|---|
| SSH | TCP | 2222(変更した場合) |
| HTTP | TCP | 80 |
| HTTPS | TCP | 443 |
ufwとパケットフィルターの両方が必要
「ufwで開けたのに接続できない」というトラブルの多くは、コントロールパネルのパケットフィルターが閉じていることが原因だ。うまく接続できない場合は必ず両方を確認してほしい。
STEP 3:Docker & Docker Compose をインストールする
3-1. 旧バージョンを削除する
sudo apt-get remove docker docker-engine docker.io containerd runc3-2. Docker Engine をインストールする
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
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
sudo chmod a+r /etc/apt/keyrings/docker.gpg
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
sudo apt-get update
sudo apt-get install -y \
docker-ce \
docker-ce-cli \
containerd.io \
docker-buildx-plugin \
docker-compose-plugin3-3. ユーザーを docker グループに追加する
sudo usermod -aG docker $USER
newgrp docker3-4. 動作確認
docker --version
# Docker version 26.x.x
docker compose version
# Docker Compose version v2.x.x
docker run hello-worldHello from Docker! と表示されれば成功だ。
STEP 4:Next.js をスタンドアロン出力に設定する
next.config.js(または next.config.ts)に output: 'standalone' を追加する。
/** @type {import('next').NextConfig} */
const nextConfig = {
output: 'standalone',
}
module.exports = nextConfigこの設定でビルドすると、.next/standalone/ に本番実行に必要なファイルだけが出力される。
.next/
├── standalone/
│ ├── server.js ← node で直接実行するエントリーポイント
│ ├── node_modules/ ← 本番に必要な依存のみ(ツリーシェイク済み)
│ └── .next/
│ └── server/ ← サーバーサイドのコード
├── static/ ← クライアント向け静的ファイル(standalone に含まれない)
└── ...
public/ ← 画像・フォントなど重要なのは、static/ ディレクトリは standalone/ の中に含まれないという点だ。Dockerfileで別途コピーする必要がある。これを知らないと静的ファイルがコンテナに入らず、ページのスタイルが崩れる原因になる。
STEP 5:Dockerfile を作る(マルチステージビルド)
マルチステージビルドを使い、本番イメージに含まれるファイルを最小限にする。
# ---- Stage 1: 依存インストール ----
FROM node:20-alpine AS deps
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci
# ---- Stage 2: ビルド ----
FROM node:20-alpine AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npm run build
# ---- Stage 3: 本番ランナー ----
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=3000
# HOSTNAME=0.0.0.0 を明示的に設定する(後述)
ENV HOSTNAME=0.0.0.0
# スタンドアロン出力をコピー
COPY --from=builder /app/.next/standalone ./
# 静的ファイルを正しい位置にコピー
COPY --from=builder /app/.next/static ./.next/static
# public/ をコピー(OGP画像・フォントなど)
COPY --from=builder /app/public ./public
EXPOSE 3000
CMD ["node", "server.js"]HOSTNAME=0.0.0.0 は必ず設定する
HOSTNAME 未設定でNginxからのリクエストが届かなくなる
Next.jsスタンドアロン出力の server.js は、HOSTNAME 環境変数が設定されていない場合に localhost(127.0.0.1)でバインドすることがある。この状態だとコンテナ内では起動しているのにNginxからのリクエストが届かず、502 Bad Gatewayになる。ENV HOSTNAME=0.0.0.0 をDockerfileに書いておけば確実に防げる。
pnpm を使っている場合
FROM node:20-alpine AS deps
RUN npm install -g pnpm
WORKDIR /app
COPY package.json pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile.dockerignore を設定する
node_modules
.next
.git
.env*
*.md.next/ を除外しているのは、ローカルのビルドキャッシュがコンテナ内のビルドに干渉しないようにするためだ。docker compose up --build のたびにコンテナ内でフルビルドを実行する。
STEP 6:Docker Compose 構成を作る
myapp/
├── docker-compose.yml
├── Dockerfile
├── .dockerignore
├── nginx/
│ └── default.conf
└── (Next.jsのソースコード)services:
nextjs:
build: .
container_name: nextjs
restart: always
expose:
- "3000"
volumes:
- nextjs_static:/app/.next/static
environment:
NODE_ENV: production
nginx:
image: nginx:alpine
container_name: nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- nextjs_static:/var/www/html/_next/static:ro
depends_on:
- nextjs
volumes:
nextjs_static:nextjs_static という名前付きボリュームを使い、Next.jsコンテナが持つ .next/static/ をNginxが読めるようにしている。
名前付きボリュームの初期化タイミング
Dockerの名前付きボリュームは、空の状態で初めて作成されるとき、コンテナのディレクトリ内容でInitializeされる。再デプロイ時に古い静的ファイルが残り続ける問題があるため、コードを更新して再デプロイするときは docker compose down -v でボリュームを削除してから起動し直す必要がある(後述)。
STEP 7:Nginx の設定をする
server {
listen 80;
server_name _;
# /_next/static/ はNginxが直接配信(1年キャッシュ)
location /_next/static/ {
alias /var/www/html/_next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
# それ以外はNext.jsコンテナへプロキシ
location / {
proxy_pass http://nextjs: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;
}
}/_next/static/ にはNext.jsがビルド時にコンテンツハッシュ付きのファイル名を付ける(例:_next/static/chunks/main-a1b2c3.js)。ファイルが更新されると必ずファイル名も変わるため、1年間キャッシュしても古いファイルを掴み続ける問題は起きない。
起動・動作確認
cd ~/myapp
docker compose up -d --build
# ログ確認
docker compose logs -f
# 動作確認(IPアドレスで確認)
curl http://localhost/VPSのIPアドレスにブラウザでアクセスしてNext.jsアプリが表示されれば成功だ。
STEP 8:HTTPS(Let's Encrypt)を設定する
本番運用にはHTTPS化が必須だ。ドメインを取得してVPSのIPアドレスに向けておく必要がある。
services:
nextjs:
build: .
container_name: nextjs
restart: always
expose:
- "3000"
volumes:
- nextjs_static:/app/.next/static
environment:
NODE_ENV: production
nginx:
image: nginx:alpine
container_name: nginx
restart: always
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/default.conf:/etc/nginx/conf.d/default.conf
- nextjs_static:/var/www/html/_next/static:ro
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
depends_on:
- nextjs
certbot:
image: certbot/certbot
volumes:
- ./certbot/conf:/etc/letsencrypt
- ./certbot/www:/var/www/certbot
volumes:
nextjs_static:server {
listen 80;
server_name example.com www.example.com;
location /.well-known/acme-challenge/ {
root /var/www/certbot;
}
location / {
return 301 https://$host$request_uri;
}
}
server {
listen 443 ssl;
server_name example.com www.example.com;
ssl_certificate /etc/letsencrypt/live/example.com/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/example.com/privkey.pem;
location /_next/static/ {
alias /var/www/html/_next/static/;
expires 1y;
add_header Cache-Control "public, immutable";
}
location / {
proxy_pass http://nextjs: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;
}
}docker compose run --rm certbot certonly \
--webroot \
--webroot-path=/var/www/certbot \
--email your@email.com \
--agree-tos \
--no-eff-email \
-d example.com \
-d www.example.com
# Nginxを再起動して証明書を読み込む
docker compose restart nginx# crontab -e で以下を追加
0 0 * * 0 cd ~/myapp && docker compose run --rm certbot renew && docker compose restart nginx再デプロイの手順
コードを更新して再デプロイする際の手順をまとめておく。
cd ~/myapp
# 最新コードをpull(GitHubで管理している場合)
git pull origin main
# ボリュームを削除してビルドし直す
docker compose down -v
docker compose up -d --build-v でボリュームを削除しているのは、nextjs_static ボリュームに古いビルドの静的ファイルが残り続けるのを防ぐためだ。これをしないと古いJSファイルがNginxから配信され続け、画面が壊れることがある。
ダウンタイムを最小化したい場合
docker compose down -v の間は一時的にサービスが止まる。個人開発や副業プロジェクトであれば数秒のダウンタイムは許容範囲内だ。無停止デプロイが必要になったときは、Blue-Greenデプロイや docker compose up -d --scale を使う構成を検討すること。
GitHub Actionsで自動化する場合は以下のワークフローが基本形だ。
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy to VPS
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.VPS_HOST }}
username: deploy
key: ${{ secrets.VPS_SSH_KEY }}
port: 2222
script: |
cd ~/myapp
git pull origin main
docker compose down -v
docker compose up -d --buildVPS_HOST と VPS_SSH_KEY はGitHubリポジトリのSecretに登録しておく。
トラブルシューティング
502 Bad Gateway になる
Nginxが起動しているのにNext.jsコンテナに繋がらない場合。
docker compose ps
docker compose logs nextjsNext.jsコンテナが起動していても502になる場合は HOSTNAME 環境変数を確認する。localhost バインドになっているとコンテナ外からアクセスできない。Dockerfileに ENV HOSTNAME=0.0.0.0 が設定されているかを確認する。
静的ファイルが古いまま(デプロイ後も更新されない)
docker compose down -v
docker compose up -d --buildVPSのIPにアクセスできない
docker compose ps
sudo ss -tlnp | grep :80ufwだけでなくXserver VPSコントロールパネルのパケットフィルターも確認すること。両方でポートが開放されていないと外部からアクセスできない。
docker: command not found
newgrp docker
# またはいったんログアウトして再ログインコンテナが起動しない・再起動を繰り返す
docker compose logs --tail=100 nextjsよくある原因:
next.config.jsにoutput: 'standalone'が設定されていない → ビルドは成功するがserver.jsが出力されない.next/static/のCOPY命令が抜けている → スタイルが当たらないかビルドエラー- 環境変数不足(必要な
.envの値がコンテナに渡っていない)
Dockerfileのビルドが遅い
マルチステージビルドとBuildKitのキャッシュを活用すると2回目以降のビルドが速くなる。
DOCKER_BUILDKIT=1 docker compose up -d --buildよくある質問(FAQ)
Q. App Router と Pages Router のどちらでも使える?
どちらでも使える。output: 'standalone' は Next.js のルーターに関係なく機能する。App RouterのServer ActionsやServer Componentsも含めてスタンドアロン出力に含まれる。
Q. APIルートはスタンドアロン出力に含まれるか?
含まれる。/api/*(Pages Router)や Route Handlers(App Router)はすべて server.js に含まれる。外部APIへのプロキシや認証ロジックもそのまま動く。
Q. public/ フォルダのファイルはNginxで配信しなくていいのか?
このDocker Compose構成では public/ の配信はNext.jsコンテナ(server.js)が担当する。ファイル数・サイズが少なければ問題ない。OGP画像など大量の静的アセットがある場合は public/ も別ボリュームでNginxに共有する構成に拡張できる。
Q. Vercelではなくわざわざ VPS を使う利点は?
Vercelの無料プランはAPIルートのタイムアウト(10秒)やサーバーレス関数のコールドスタートがある。バックグラウンドジョブや長時間処理を伴うAPIを動かす場合、VPSの方が柔軟だ。月額固定コストで使い方に制限がなく、個人開発では管理しやすい。
Q. 環境変数はどうやって渡すか?
開発環境は .env.local を使うが、Dockerコンテナには渡さないのが基本だ。VPS上では docker-compose.yml の environment に直接書くか、.env ファイルを env_file で読み込む。
services:
nextjs:
build: .
env_file:
- .env.production
environment:
NODE_ENV: production.env.production はGitにコミットせず .gitignore に追加しておく。
Q. Node.js のバージョンは 20 でなくてもよいか?
Next.js 14/15 は Node.js 18.17 以上が必要だ。LTS版を使うのが安全で、2026年時点では Node.js 20(Active LTS)または Node.js 22(Current)が推奨される。node:20-alpine を node:22-alpine に変えても動く。
まとめ:今日からできるアクション
Xserver VPSへのNext.jsスタンドアロン出力 + Docker Composeデプロイの手順をまとめた。
- VPS契約:2GBプランから始め、
docker statsでメモリ監視しながらスケールアップを判断する - 初期設定:ufw設定に加えて、Xserver VPSコントロールパネルのパケットフィルターも忘れずに開放する
- next.config.js:
output: 'standalone'を設定してスタンドアロンビルドを有効化する - Dockerfile:マルチステージビルド +
ENV HOSTNAME=0.0.0.0が重要。.next/static/のCOPYも忘れずに - Docker Compose:名前付きボリュームで
.next/static/をNginxに共有し、キャッシュ効率を上げる - 再デプロイ:
docker compose down -v && docker compose up -d --buildでボリュームごとリセットする
データ転送量無制限・NVMe SSD・月額¥990〜というXserver VPSのスペックは、Next.jsアプリのホスティングに向いている。個人開発から副業プロジェクトまで、固定コストで安心して動かせる環境を作ってほしい。
NVMe SSD・データ転送量無制限・36ヶ月で月額¥990〜。個人開発のNext.jsアプリをVPSで動かすなら、Xserver VPSは有力な選択肢だ。まず公式サイトでプランを確認してみよう。
Xserver VPS のプランを確認する →※本リンクはアフィリエイトリンクです