🐳

VercelでDockerfileが使えるようになったので試してみた

VercelでDockerfileが使えるようになりました。これまでVercelは基本的にNode.jsやNext.jsなどのランタイムを前提にした仕組みでしたが、今回のアップデートで任意のコンテナイメージをそのままVercel Functionsとして動かせるようになっています。

「Dockerで動くものなら何でもVercelに乗る」というのが面白そうだったので、実際にサンプルを作ってデプロイするところまで試してみました。

実際に動いているサンプルはこちらです。

リポジトリはこちらに置いてあります。

何ができるようになったのか

ざっくり言うと、Dockerfileをリポジトリに置くだけで、そのコンテナがVercel上でHTTPサーバーとして動くようになりました。

ポイントは以下です。

  • プロジェクトルートにDockerfile.vercel(またはContainerfile.vercel)を置くと、Vercelが自動検出してビルドする
  • ビルドしたイメージはVercel Container Registry(VCR)にpushされる
  • 全トラフィックをそのコンテナへ流すrewriteルールが自動で追加される
  • コンテナはVercel Functionsとして動き、Active CPU課金(実際にCPUを使った分だけ課金)になる
  • 唯一のルールは「サーバーが$PORT(デフォルト80)でリッスンすること」。HTTPさえ話せれば何でもデプロイできる

Go、Rails、Spring Boot、Express、Laravel、FastAPI、nginxなど、コンテナ化できるアプリならフレームワーク問わず動くのが売りのようです。

サンプルを作る

今回は依存ライブラリなしのNode.js製HTTPサーバーを、コンテナとして動かしてみます。ファイル構成はこんな感じです。

.
├── Dockerfile.vercel   # Vercelが自動検出するコンテナ定義
├── server.js           # $PORTでリッスンする最小HTTPサーバー
├── package.json
├── public/
│   └── index.html      # トップページ
└── .dockerignore

Dockerfile.vercel

DockerfileではなくDockerfile.vercelという名前にするのがポイントです。この名前だとVercelが自動でコンテナ用のビルドだと認識してくれます。

FROM node:26-alpine

WORKDIR /app

# 依存関係(今回は標準モジュールのみなのでpackage.jsonだけコピー)
COPY package.json .

# アプリ本体
COPY server.js .
COPY public ./public

# ドキュメントに合わせて80を公開。
ENV PORT=80
EXPOSE 80

CMD ["node", "server.js"]

中身は普通のDockerfileです。特別なベースイメージやVercel固有の記述は不要で、「$PORTでHTTPサーバーを立ち上げる」ことだけ守れば大丈夫です。

server.js

外部ライブラリを使わず、Node標準のhttpモジュールだけでサーバーを書きます。唯一のルールである「$PORT(デフォルト80)でリッスンする」を満たしています。

import { createServer } from "node:http";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";

const __dirname = dirname(fileURLToPath(import.meta.url));
const PORT = process.env.PORT || 80;

const server = createServer(async (req, res) => {
  // ヘルスチェック用エンドポイント
  if (req.url === "/api/health") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(JSON.stringify({ status: "ok", uptime: process.uptime() }));
    return;
  }

  // コンテナ内部の情報を返すAPI(コンテナで動いていることの確認用)
  if (req.url === "/api/info") {
    res.writeHead(200, { "Content-Type": "application/json" });
    res.end(
      JSON.stringify(
        {
          message: "Hello from a Dockerfile running on Vercel!",
          node: process.version,
          platform: process.platform,
          arch: process.arch,
          hostname: process.env.HOSTNAME ?? null,
          port: Number(PORT),
          clientIp: req.headers["x-forwarded-for"] ?? null,
          time: new Date().toISOString(),
        },
        null,
        2,
      ),
    );
    return;
  }

  // それ以外はトップページ(静的HTML)を返す
  try {
    const html = await readFile(join(__dirname, "public", "index.html"));
    res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
    res.end(html);
  } catch {
    res.writeHead(404, { "Content-Type": "text/plain" });
    res.end("Not Found");
  }
});

// SIGTERMでのgraceful shutdown
process.on("SIGTERM", () => {
  console.log("SIGTERM received, shutting down gracefully...");
  server.close(() => process.exit(0));
});

server.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});

SIGTERMのハンドリングを入れているのは、コンテナがスケールインするときにVercelからSIGTERMが送られてくるためです(猶予30秒)。無トラフィックが続くと自動でスケールダウンする挙動があるので、後片付けを書いておくと行儀がよくなります。

ローカルで動作確認する

デプロイする前に、手元のDockerで動くか確認します。

docker build -f Dockerfile.vercel -t docker-on-vercel-sample .
docker run --rm -p 8080:80 docker-on-vercel-sample
# http://localhost:8080 を開く

/api/infoを叩くと、コンテナ内部の情報がJSONで返ってきます。

curl -s localhost:8080/api/info
{
  "message": "Hello from a Dockerfile running on Vercel!",
  "node": "v26.4.0",
  "platform": "linux",
  "arch": "arm64",
  "hostname": "3fd6a73c0740",
  "port": 80,
  "clientIp": null,
  "time": "2026-07-01T00:00:00.000Z"
}

ちゃんとLinuxコンテナの中でNodeが動いていることが確認できます。

Vercelにデプロイする

ローカルで動いたので、Vercelにデプロイします。まずVercel CLIを入れます。

npm i -g vercel
vercel --version
# Vercel CLI 54.18.6

あとはログインしてリンク、デプロイの流れです。

# 1. ログイン(ブラウザ認証が走る)
vercel login

# 2. プロジェクトをリンク(対話で新規作成 or 既存選択)
vercel link

# 3. デプロイ(ビルド時にイメージがビルドされVCRにpushされる)
vercel deploy

vercel linkでは対話でプロジェクトを設定します。ここでDetected Containerと表示され、Vercelがコンテナ構成を認識してくれているのがわかります。

vercel link実行コンソール画面

vercel deployを実行すると、ビルドステップでDockerイメージがビルドされ、Vercel Container Registryへpushされます。Gitリポジトリに連携しておけば、push毎に自動でビルドしてプレビューURLを発行してくれるのも通常のVercelと同じです。

vercel deploy実行コンソール画面

デプロイが完了すると、ProductionのURLが払い出されます。ちゃんとコンテナ経由でページが配信されています。

ページ上のボタンから/api/infoを叩くと、コンテナ内部の情報が返ってくる様子はこんな感じです。

API受信テスト

ブラウザのDevToolsで/api/infoのレスポンスを見ると、arch: "x64"のLinuxコンテナ上でNodeが動いていることが確認できます。

APIレスポンスプレビュー

レスポンスヘッダを見るとServer: Vercelとなっており、Vercel経由でコンテナが配信されているのがわかります。

APIレスポンスヘッダ

覚えておきたい挙動

試していて気づいた・ドキュメントに書かれていた挙動をまとめておきます。

  • ポートのデフォルトは80。変えたい場合はVercelプロジェクト設定の環境変数PORTで上書きできる
  • 無トラフィックが続くとスケールダウンする。猶予は本番で5分、プレビューで30秒
  • スケールイン時にコンテナへSIGTERMが送られ、30秒の猶予後に強制終了される
  • コンテナのstdout/stderrはVercelのランタイムログに流れる。ただしリクエストに紐づかないログは、そのインスタンスの全リクエストへブロードキャストされる点に注意
  • 課金は通常のVercel Functionsと同じActive CPU課金で、I/O待ちやスリープ中は課金されない
  • 現時点でSecure ComputeやStatic IPsはまだ非対応

複数アプリを1プロジェクトに載せる(services)

今回は単一コンテナ構成にしましたが、vercel.jsonservicesを使うと、フロントとバックエンドなど複数アプリを同じプロジェクトに同居させることもできます。

{
  "services": {
    "frontend": {
      "root": "frontend/",
      "entrypoint": "Dockerfile.vercel"
    },
    "backend": {
      "root": "backend/",
      "entrypoint": "Dockerfile.vercel"
    }
  },
  "rewrites": [
    { "source": "/api/(.*)", "destination": { "service": "backend" } },
    { "source": "/(.*)", "destination": { "service": "frontend" } }
  ]
}

各サービスのroot配下にDockerfile.vercelを置き、rewritesでパスごとにルーティングする形です。マイクロサービス的な構成もVercel内で完結できるのは便利そうです。

まとめ

Dockerfile.vercelを置くだけで、コンテナがそのままVercel Functionsとして動くのは想像以上に手軽でした。$PORTでHTTPを話すだけという最小ルールなので、既存のDockerアプリをほぼそのまま持っていけるのが強みだと思います。

Node以外のランタイム(Go、Rails、FastAPIなど)を動かしたいときや、Vercelでは扱いづらかった依存を持つアプリをデプロイしたいときに効いてきそうです。

参考文献

新着記事

関連ネットワーク(beta)

ドラッグで移動 / Ctrl+ホイールでズーム