
ついにVercelでバックエンドが動く!任意のDockerfileをそのままデプロイできるようになったので試してみた
こんにちは、豊島です。
先日のAWS Summit Japan 2026のVercelブースで、多くの方々とお話しする機会がありました。そこで何度も伺ったのが、フロントエンドはVercel、バックエンドはAWSをはじめとする大手クラウド、というふうにプラットフォームが分かれている構成の話です。デプロイもドメインもオブザーバビリティも別々になり、つなぎ込みに手間がかかる、という声をよく聞きました。
そんな構成で運用している方に朗報です。2026年6月30日に、Vercelから任意のDockerfileがデプロイできるようになったという発表がありました。
Vercelといえばフロントエンドプラットフォームという印象が強いと思いますが、それを大きく動かすような発表でした。
この記事では何がどう変わるのかを整理しつつ、後半では、FFmpegを使ったサムネイル生成APIを実際に作って動かします。
これまではどうだったのか(上記のBlogから抜粋)
これまでのVercelは、基本的にフレームワーク定義型インフラ(Framework-Defined Infrastructure)の世界でした。Vercelがフレームワークを認識すると、コードを読んで必要なインフラを自動で導出してくれる仕組みで、Next.jsを筆頭に、対応しているフレームワークなら最高の体験でデプロイできます。
裏を返すと、この土俵に乗らないものは扱いづらい面がありました。具体的には次のようなケースです。
- RailsやSpring Boot、Laravelなど、任意のバックエンド言語・構成
- FFmpegやChromiumのようなシステムライブラリに依存するサービス
- 今そのまま動いているアプリを、作り直さずに持ち込みたいケース
結果として、こうしたバックエンドはAWSのECS/EKSなどVercelの外に置き、フロントだけVercel、という分割構成になりがちでした。フロントとバックでプラットフォームが分かれ、デプロイもオブザーバビリティもドメインも別々という状態です。
何ができるようになったのか
Dockerfile.vercelというファイルをプロジェクトに置くだけで、HTTPで通信するものなら何でもVercelネイティブで動かせるようになりました。
ポイントを整理すると次の4つです。
1. 言語・スタックの制約が事実上消えた
Rails、Spring Boot、Express、Laravel、ASP.NET、FastAPI、nginx配下のWebサーバー...などなど、同じやり方でデプロイできます。
2. 既存アプリを作り直さず持ち込める
Dockerfileを利用することで、既存の構成をVercelに持ち込めるようになりました。
3. フロントと同じ基盤・同じ運用に統合される
ここが一番大きい変化です...!
コンテナがフロントエンドと同じ基盤・同じコンピュート上で動くようになりました。
フロントエンドとバックエンドがVercelの同じプロジェクト、ネットワーク経由でプライベートに繋がります。
4. コンテナなのに使った分だけ課金になる
従来コンテナといえば常時起動、サイズ分課金が当たり前でしたが、VercelのFluid computeのActive CPU課金により、コードが実際に実行されている時間だけ課金されます。アイドルなサーバーはCPUを消費しません。
仕組み
基本は2つのファイルで動きます。
まずアプリ本体。ここではNode(TypeScript)の最小HTTPサーバーを例にします。$PORTを読んでリッスンするのがポイントです。
import { createServer } from "node:http";
const port = Number(process.env.PORT ?? 80);
createServer((_req, res) => {
res.end("Hello from a container on Vercel");
}).listen(port);
次にDockerfile.vercelを置きます。ファイルをコピーして起動するだけです。Node 24はTypeScriptをそのまま実行できるので、ビルド手順も要りません。
FROM node:24-alpine
WORKDIR /app
COPY server.ts .
CMD ["node", "server.ts"]
あとはデプロイするだけです。
vercel deploy
このコマンド一発で、イメージのビルド、レジストリへの保存、Fluid computeへのデプロイ、本番URLの発行までが完了します。git pushのたびにイメージが再ビルドされ、新しいプレビューURLが発行されます。
イメージの保存先はVercel Container Registry(VCR)です。プロジェクト単位のOCIイメージ用レジストリで、ホストはvcr.vercel.com。docker loginすれば手元のDocker互換ツールからpush/pullもできます。ECRのVercel版、と捉えると分かりやすいです。
参考: Vercel Container Registry(公式ドキュメント)
参考: コンテナイメージのデプロイに関する公式ドキュメント
どんなシーンで使えるのか
具体的なユースケースをいくつか挙げます。
既存バックエンドAPIの統合
フロントはNext.jsでVercel、APIはRailsでAWS ECS、という典型構成のAPIを持ち込むパターンです。CORSやクロスアカウントのネットワーク設定が消えるのが嬉しいところです。
FFmpegを使う動画・画像処理
サムネイル生成やフォーマット変換など、従来はサーバーレス関数で扱いにくかった代表例で、Dockerfileの出番としてBlogでも取り上げられています。処理が散発的なほど、Active CPU課金のコスト効率が効いてくると思います。
このFFmpegのケースは、後半で実際にサムネイル生成APIを作って動かしてみます。
Chromiumを使うPDF・スクリーンショット生成
帳票PDFの動的生成、OGP画像生成、ページのスクリーンショット取得など、ヘッドレスブラウザを使う処理です。Chromium入りのイメージをそのまま持ち込めるため、リクエスト単位で完結するステートレスな処理と相性がとても良いです。
レガシーアプリのリフト&シフト
nginx + PHP-FPMのような、今動いているそのままの形で持ってきたい既存アプリです。まずDockerfileでそのまま動かし、徐々にVercelの仕組みに寄せていく、という段階移行が取れます。
実際にFFmpegでサムネイル生成APIを作ってみる
題材としては、動画を投げると指定秒のフレームをサムネイルとして返すAPIです。TypeScript(Node)で実装し、FFmpegで1フレームを切り出します。依存パッケージは使わず、Node組み込みのhttpだけで書きます。
ディレクトリ構成
ts-thumbnail/
├── server.ts
└── Dockerfile.vercel
package.jsonは無く、サーバー1ファイルとDockerfileだけで試してみました。
サーバー実装
トップページはブラウザから試せる簡易フォームを作ってみました。
/thumbnailに動画をPOSTすると、FFmpegで指定秒のフレームを1枚JPEGで書き出して返します。
おまけとして記載
import { createServer } from "node:http";
import { spawnSync } from "node:child_process";
import { writeFileSync, readFileSync, unlinkSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const port = Number(process.env.PORT ?? 80);
// ブラウザから試せる簡易フォーム
const FORM = `<!doctype html>
<html lang="ja">
<head><meta charset="utf-8"><title>FFmpeg Thumbnail (TS)</title>
<style>body{font-family:system-ui;margin:40px;max-width:640px;color:#111}button{margin-left:8px}</style></head>
<body>
<h1>FFmpeg サムネイル生成(TypeScript)</h1>
<p>動画を選んで秒数を指定すると、その時点のフレームをJPEGで返します。</p>
<form id="f">
<input type="file" name="file" accept="video/*" required>
<label>秒: <input type="number" name="second" value="3" step="0.5" style="width:5em"></label>
<button>サムネイル生成</button>
</form>
<img id="out" alt="" style="margin-top:24px;max-width:100%">
<script>
const f = document.getElementById("f");
f.addEventListener("submit", async (e) => {
e.preventDefault();
const res = await fetch("/thumbnail?second=" + f.second.value, { method: "POST", body: f.file.files[0] });
document.getElementById("out").src = URL.createObjectURL(await res.blob());
});
</script>
</body></html>`;
const server = createServer((req, res) => {
const url = new URL(req.url ?? "/", "http://localhost");
// トップは簡易フォーム
if (req.method === "GET" && url.pathname === "/") {
res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
res.end(FORM);
return;
}
// ヘルスチェック
if (req.method === "GET" && url.pathname === "/health") {
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
// 動画を受け取り、指定秒のフレームをJPEGで返す
if (req.method === "POST" && url.pathname === "/thumbnail") {
const second = url.searchParams.get("second") ?? "1";
const chunks: Buffer[] = [];
req.on("data", (c) => chunks.push(c as Buffer));
req.on("end", () => {
const srcPath = join(tmpdir(), `src-${Date.now()}.mp4`);
const outPath = `${srcPath}.jpg`;
writeFileSync(srcPath, Buffer.concat(chunks));
const result = spawnSync("ffmpeg", [
"-ss", second,
"-i", srcPath,
"-frames:v", "1",
"-q:v", "2",
"-y",
outPath,
]);
if (result.status !== 0) {
res.writeHead(500, { "content-type": "application/json" });
res.end(JSON.stringify({ error: result.stderr.toString().slice(-500) }));
unlinkSync(srcPath);
return;
}
const data = readFileSync(outPath);
unlinkSync(srcPath);
unlinkSync(outPath);
res.writeHead(200, { "content-type": "image/jpeg" });
res.end(data);
});
return;
}
res.writeHead(404);
res.end();
});
// $PORTでリッスンする。デフォルトは80
server.listen(port, () => {
console.log(`listening on :${port}`);
});
エンドポイントは3つです。
GET /… ブラウザから試すための簡易フォーム(HTMLを返すだけ)GET /health… ヘルスチェックPOST /thumbnail?second=N… 受け取った動画のN秒地点を1枚JPEGで返す
動画の受け渡しは、一般的なファイルアップロード(multipart/form-data)にもできますが、ここでは依存を増やしたくないので、リクエストボディに動画のバイト列をそのまま載せる形にしました。
Dockerfile.vercel
Dockerfileはこれだけです。apk add ffmpegの1行でFFmpegが本番のイメージに入ります。
FROM node:24-alpine
# FFmpegをインストール(ここがDockerfileを使う最大の理由)
RUN apk add --no-cache ffmpeg
WORKDIR /app
COPY server.ts .
# Node 24はTypeScriptを直接実行できる。$PORTでリッスン(デフォルト80)
CMD ["node", "server.ts"]
まずローカルで動作確認
デプロイ前にテスト用の動画をFFmpegで生成し、サーバーを起動してPOSTしてみます。
# テスト用の動画を生成(5秒のカラーバー)
ffmpeg -f lavfi -i testsrc=duration=5:size=640x360:rate=30 \
-pix_fmt yuv420p test.mp4 -y
# サーバー起動(ビルド不要、tsを直接実行)
PORT=8090 node server.ts
# 別ターミナルから3秒地点のサムネイルを取得
curl -X POST "http://localhost:8090/thumbnail?second=3" \
--data-binary "@test.mp4" -o out.jpg
GET /healthにアクセスすると{"status":"ok"}が返り、サーバーが起動していることを確認できました。
トップページ(GET /)をブラウザで開くと、動画を選んでサムネイルを生成できる簡易フォームが表示されます。テスト動画をアップロードして生成すると、その場でサムネイルが表示されました。

コマンドラインからPOST /thumbnailを叩いても同じ結果です。fileコマンドで確認すると、入力と同じ640x360のJPEGになっています。
out.jpg: JPEG image data, JFIF standard 1.02, aspect ratio, density 1x1, segment length 16, comment: "Lavc62.28.102", baseline, precision 8, 640x360, components 3
処理時間もcurl -w "%{time_total}"で計測しました。起動直後の初回(コールドスタート)は0.1〜0.2秒で値も揺れますが、数回叩いて温めると約0.03秒に収束します。大半はFFmpegの実行時間です。
このコールドとウォームの差は、Fluid computeの設計と相性がよく、ウォームなうちは初回のオーバーヘッドを引きずらず、アイドル中は課金されません。
Vercelへデプロイ
ローカルで動いたので、CLIからデプロイします。
vercel deploy
最初はVercel CLIが古く、Dockerfile.vercelが使われずNode関数としてビルドされてしまいました(コンテナ扱いにならず、ffmpegも入らない状態です)。本機能はリリース当日のものだったので、CLIを最新版に上げて、新しいプロジェクトとしてデプロイし直したら回避できました。
pnpm add -g vercel@latest
今度はDetected Containerと表示され、Dockerfile.vercelどおりにイメージがビルドされます。ビルドログがこちらです(apkのインストール一覧は中略)。
Detected Container (Output Directory: None)
→ Authenticating to vcr.vercel.com as team_xxxxxxxx
✓ authenticated
→ Ensuring registry repository "dockerfile"
✓ created repository "dockerfile"
▲ container Building image vcr.vercel.com/xxxxxxxxxx/ts-thumb-fresh/dockerfile (buildah)
STEP 1/5: FROM node:24-alpine
STEP 2/5: RUN apk add --no-cache ffmpeg
...
(105/105) Installing ffmpeg (8.1.2-r0)
STEP 3/5: WORKDIR /app
STEP 4/5: COPY server.ts .
STEP 5/5: CMD ["node", "server.ts"]
✓ built in 5.6s
→ Pushing vcr.vercel.com/.../dockerfile with zstd compression
✓ pushed in 4.4s
Build Completed in /vercel/output [11s]
Dockerfile.vercelがそのまま使われ、node:24-alpineにapk add ffmpegでFFmpeg 8.1.2が入り、ビルドしたイメージがVercel Container Registry(vcr.vercel.com)にpushされて、デプロイまで通りました。
FFmpegというサーバーレス関数では一手間かかっていた依存が、Dockerfileに1行書くだけで済みます。
(過去にこういうブログも書いていました)
なお、発行された本番URLにアクセスすると、Vercel認証(Deployment Protection)にリダイレクトされました。
$ curl -s -o /dev/null -w "%{http_code}\n" https://ts-thumb-fresh-xxxxxxxxxxxxxxxxx.vercel.app/
302
今回検証したTeamではデプロイにデフォルトで認証がかけているため、公開URLをそのままcurlやブラウザで叩くと認証画面に飛ばされます。
今回は本番の/thumbnailを直接叩くところまでは確認できていませんが、上のビルドログのとおりffmpeg入りのコンテナがビルド・pushされたことは確認できました。
既存ワークフローへの組み込み方
ここが一番イメージしづらいところだと思うので、1つのシナリオで通します。
次のような会社を想定します。
- フロント: Next.js(すでにVercel)
- バックエンド: Rails製API(AWS ECS)
- CI/CD: GitHub ActionsでRailsをビルドしてECRにpush、ECSにデプロイ
この構成にDockerfile対応を組み込むと、Rails側のデプロイ先をECSからVercelに付け替えるだけで済みます。
手順は以下の通りです
- すでにECSで使っている
Dockerfileを、ほぼそのままDockerfile.vercelという名前で置く($PORT対応だけ確認) - GitHub Actionsの「build → ECRへpush → ECS更新」のワークフローを削除する
vercel deployまたはgit pushに任せる
これにより、今まで自分でGitHub Actionsに書いていた一連のデプロイ処理が、まるごとVercel側の標準動作に置き換わります。
コードを直してpushすると、フロントと同じようにコミット専用のプレビューURLが自動で出ます。レビュアーはそのURLで挙動を確認 -> マージして本番に反映できます。
ログもメトリクスもフロントと同じダッシュボードに並び、スケーリングは自動なので、例えばECSのタスク数やオートスケーリングの設定作業はやらなくて済むようになります。
注意点 ステートレス前提ということ
便利な一方で、押さえておくべき制約があります。Vercel上のコンテナは、従来のサーバーとは動き方が違います。
各コンテナはステートレスなプロセスで、リクエストを受けてレスポンスを返し、その間に何もデータを保持しません。
トラフィックが来ればインスタンスが増え、止まれば縮退します。ドキュメントによると、無通信が本番環境で5分・プレビュー環境で30秒続くとスケールインし、縮退時はコンテナにSIGTERMが送られて30秒の猶予のあとに停止します。そのため永続的な状態は、アタッチするバッキングサービス(データベースやキャッシュ)側に持たせる前提です。
つまりアプリのロジックはVercelに乗るが、データの保管場所は外部(RDS、Supabaseなど)、という分担になります。セッションをインメモリに持っているようなアプリは要改修です。なお、コンテナにアタッチする永続ストレージも2026年6月時点では近日提供予定とされているので、この制約は将来緩和される見込みです。
セキュリティの観点
バックエンドをVercelに乗せるなら、必ず詰めておきたい論点です。
デプロイするとバックエンドに本番URLが付きます。今回はDeployment Protection(Vercel認証)がデフォルトで効いていて、URLを直接叩くと認証画面にリダイレクトされました(先ほどの302)。逆に保護を外せば、URLはそのままインターネットに公開されます。
その上でインバウンドとアウトバウンドの観点で整理してみました。
インバウンド
「限られたフロントエンドからしか叩けないようにする」を実現する手段は主に次の方法が考えられます。
- 同一プロジェクト内のプライベート通信
- フロントエンドとバックエンドを同じプロジェクトに置けば、バックエンドを公開エンドポイントとしてさらさず、Vercelネットワーク経由の内部通信だけで繋げます
- これが目的に最も近い構成だと思います
- Deployment ProtectionとWAF
- 公開せざるを得ない場合は、プレビューURLに認証をかけたり、EnterpriseのIPブロックやDDoS緩和を被せたりして守ります
- アプリ層の認証
- JWTやOAuthでの認可は、コンテナでも結局アプリ側で実装する領域です
- Vercelに乗せたから自動で守れるわけではありません
アウトバウンド
社内DB側でVercelからのトラフィックだけ許可したい、という出口側の制御は、通常のVercel FunctionsではSecure ComputeやStatic IPs(専用の静的IPを払い出し、その範囲だけ許可する仕組み)で対応します。
ただし、ここがコンテナ特有の注意点です。公式ドキュメントには、コンテナイメージについて「Secure ComputeとStatic IPsはまだ未対応」と明記されています。固定IPでの絞り込みやVPC peeringが要件になるバックエンドをコンテナで動かす場合、2026年6月時点ではこの制約に当たる可能性があります。要件に挙がっているなら、最初に確認しておきたいポイントです。
参考: コンテナイメージの公式ドキュメント(制限と料金の節にSecure Compute / Static IPs未対応の記載があります)
なお認証情報の扱いについては、環境変数をダッシュボードで管理できるほか、OIDC Federationを使えば長期間有効な認証情報を埋め込まずに、短命トークンをVercel側で発行する方式も取れます。
Dockerイメージのスキャンについて
VCR自体にスキャン機能はありません。ドキュメントを見ても、VCRはイメージの保存とpush/pull、容量制限が中心で、脆弱性スキャンや署名の記載はありませんでした(2026年7月時点)。そのため、スキャンは別途自分で組み込む前提になります。
一般的には、CIでイメージをスキャンしてHIGHやCRITICALの脆弱性で落とす、というチェックを入れると思います。(Snyk ContainerやTrivyが有名かと思います)
Vercelの場合、vercel deployがイメージをサーバー側でビルドするので、ビルドの途中にスキャンを差し込む口は今のところありません。
GitHub Actions側で同じDockerfileをビルドしてTrivyなどでスキャンしてからデプロイする、あるいはVCRにpushされたイメージをdocker pullして定期的にスキャンする(あんまりないと思いますが)、という形が現実的かと思います。
今回のようにパッケージを毎回入れる構成だと、ビルドのたびに比較的新しいパッケージが入るので、ベースイメージとパッケージを定期的に作り直すだけでもリスクは下げられます。それでも依存の脆弱性は出るので、スキャンは別で回しておくのが安全です。
まとめ
Vercelはフロントエンド専用だからバックエンドは大手クラウドで、という前提が崩れてフルスタック構成をVercelに寄せることができるようになりました。
Vercelは2026年6月のShip Londonで、エージェントフレームワークeveも発表しました。
Next.jsがWebアプリのフレームワークだったように、eveはエージェントのためのフレームワーク、という位置づけです。フロントエンドの基盤から、エージェントを動かすインフラ(Agentic Infrastructure)へと軸足を移そうとしているように見えます。任意のバックエンドをコンテナごと引き受ける今回の変更も、その土台の一部と感じています。
一方で、ステートレス前提やセキュリティの線引きなど、実運用に乗せる前に押さえるべき点もあります。まずは画像・動画処理や帳票生成、社内向けAPIといったステートレスなワークロードから試していった方がいいと感じました。
今後のアップデートにも期待です。







