Docker Compose初心者入門:Node.jsをVPSで公開【2026年版】
「Node.js でアプリを作ったけれど、VPS でどう動かせばいいのか分からない」「Docker Compose 初心者で、Dockerfile と compose.yaml の書き方から知りたい」——そんな人に向けた手順ガイドです。
この記事では、Node.js のアプリ・Dockerfile・compose.yaml・Caddyfile の4つのファイルで、「アプリ + HTTPS のリバースプロキシ」を立てるところまでを一続きで紹介します。ログが VPS のディスクを埋めないようにする設定と、落ちたら自動で起動し直す設定も最初から入れておきます。
載せているコードは、2026年11月13日にテスト用の Linux 環境(Docker)で実際に動かしたものだけです。VPS への公開・DNS・証明書の取得は、Docker と Caddy の公式ドキュメントの手順として説明します。

「とりあえず動く」から一歩進んで、半年放っておいても止まらない・ディスクがあふれない構成を最初から作る——という位置づけの記事です。
この記事はこんな人向けです
- Node.js でアプリを作ったが、VPS でどう動かせばいいか分からない
- Docker Compose を初めて使うので、Dockerfile から公開までを一続きで知りたい
- HTTPS・ログ・落ちたときの自動再起動まで、最初からきちんと設定しておきたい
Docker Composeとは?一言でまとめると
Docker Compose は「複数のコンテナを、1つの設定ファイル(compose.yaml)でまとめて起動・停止する道具」です。
Node.js のアプリだけなら docker run でも動きます。ただ、実際に公開するには「HTTPS を受け持つリバースプロキシ」も必要です。コンテナが2つ以上になると、起動の順番・ネットワーク・ボリューム・再起動の設定を毎回コマンドで打つのは大変です。Compose なら、それを全部 compose.yaml に書いて docker compose up -d の1行で立ち上げられます。

まず、この記事に出てくる言葉を整理しておきます。
| 言葉 | 意味 | この記事では |
|---|---|---|
| イメージ | アプリと動かす環境をまとめた「ひな形」 | Dockerfile から作る(myapp-app) |
| コンテナ | イメージから起動した、実際に動いているもの | app と caddy の2つ |
| Dockerfile | イメージの作り方を書いたファイル | Node.js 26 にアプリを入れる |
| compose.yaml | 複数のコンテナの設計図 | app と caddy・再起動・ログの設定 |
| リバースプロキシ | 外からの通信を受けて、裏のアプリに渡す係 | Caddy(HTTPS の証明書も自動で取る) |
| ボリューム | コンテナを作り直しても消えない保存場所 | Caddy の証明書を保存する |
リバースプロキシには Nginx もよく使われます。この記事では、ドメインを書くだけで HTTPS の証明書を自動で取得・更新してくれる Caddy を選びました。設定ファイルが3行で済むので、初心者がつまずきにくいからです。
公開までの全体の流れと、用意するもの
VPS で公開するまでの流れは、大きく5つです。③と⑤は手元の PC(Docker が入っていれば Windows・Mac でも)で先に試せるので、VPS を借りる前に動きを確かめておくと安心です。

用意するファイルは、次のとおりです。フォルダ1つ(ここでは myapp)に全部入れます。

| ファイル | 役割 | どこで作る? |
|---|---|---|
server.js |
Node.js のアプリ本体 | 自分で書く |
package.json / package-lock.json |
使うパッケージ(Express)とその版 | npm install で作る |
Dockerfile |
アプリのイメージの作り方 | 自分で書く |
.dockerignore |
イメージに入れないファイル | 自分で書く |
compose.yaml |
app と caddy の設計図 | 自分で書く |
conf/Caddyfile |
Caddy の設定(どのドメインをどこへ渡すか) | 自分で書く |
フォルダの形は次のようになります。
フォルダの構成
myapp/
├── server.js
├── package.json
├── package-lock.json
├── Dockerfile
├── .dockerignore
├── compose.yaml
└── conf/
└── Caddyfile
手順①:Node.jsアプリを用意する
例として、「/」で JSON を返し、「/health」で ok を返すだけの小さな Express のアプリを使います。自分のアプリに置き換えるときも、「ポート番号を環境変数 PORT で受け取る」「/health のような生存確認の URL を作る」の2点はそのまま真似するのがおすすめです。
server.js
const express = require("express");
const app = express();
const port = process.env.PORT || 3000;
app.get("/", (req, res) => {
console.log(`${new Date().toISOString()} GET / from ${req.ip}`);
res.json({ message: "Hello from Docker Compose!", node: process.version });
});
app.get("/health", (req, res) => {
res.send("ok");
});
const server = app.listen(port, () => {
console.log(`listening on port ${port}`);
});
// docker compose down / stop のときに受け取る SIGTERM で、きれいに終わる
process.on("SIGTERM", () => {
console.log("SIGTERM received, closing server");
server.close(() => process.exit(0));
});
最後の SIGTERM の部分が、Docker で動かすときの大事なポイントです。Docker はコンテナを止めるとき、まずアプリに SIGTERM という「終わってください」の合図を送り、10秒たっても終わらなければ強制終了します。Node.js の公式イメージのベストプラクティスでも、Node.js はコンテナの最初のプロセス(PID 1)として動くことを想定していないと注意されています。合図を受け取って自分で終わる処理を入れておくと、止めるたびに待たされずに済みます。
package.json は、アプリのフォルダで npm install express を実行すると、package-lock.json と一緒に作られます。
package.json
{
"name": "myapp",
"version": "1.0.0",
"private": true,
"main": "server.js",
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "^5.3.0"
}
}
package-lock.json は必ず一緒に置いてください。次の Dockerfile で使う npm ci は、ロックファイルが無いとエラーになります。
手順②:Dockerfileを書く
Dockerfile は「イメージの作り方」です。Node.js 26(2026年10月28日から LTS=長期サポート版)の軽い Alpine 版をもとに作ります。
Dockerfile
FROM node:26-alpine
WORKDIR /app
# 先に依存関係だけ入れる(コードを直しても、ここはキャッシュが効く)
COPY package.json package-lock.json ./
RUN npm ci --omit=dev
# アプリのコードをコピー
COPY . .
ENV NODE_ENV=production
USER node
EXPOSE 3000
CMD ["node", "server.js"]
1行ずつの意味は次のとおりです。
| 行 | 意味 | なぜこう書く? |
|---|---|---|
FROM node:26-alpine |
Node.js 26 入りの小さな Linux をもとにする | LTS なので 2029年4月末まで更新が続く |
COPY package.json package-lock.json ./ → RUN npm ci --omit=dev |
パッケージだけ先に入れる | コードを直してもこの層はキャッシュが効き、再ビルドが速い |
npm ci |
ロックファイルどおりに入れ直す | npm install と違い、版が勝手に変わらない |
--omit=dev |
開発用のパッケージを入れない | イメージが小さくなる |
ENV NODE_ENV=production |
本番モードで動かす | Express などが本番向けの動きになる |
USER node |
root ではない node ユーザーで動かす |
公式イメージに用意済み(uid 1000)。乗っ取られたときの被害を小さく |
CMD ["node", "server.js"] |
起動コマンド | npm start を挟まず、SIGTERM が Node.js に直接届く |
あわせて .dockerignore も作ります。手元の node_modules や .env がイメージに紛れ込むのを防ぎます。
.dockerignore
node_modules
npm-debug.log
.git
.env
Node.js の版の選び方・サポート期限は、こちらで詳しく整理しています。

手順③:compose.yamlとCaddyfileを書く
ここが Docker Compose の中心です。app(Node.js)と caddy(リバースプロキシ)の2つのサービスを書きます。
compose.yaml
services:
app:
build: .
restart: unless-stopped
environment:
PORT: "3000"
expose:
- "3000"
healthcheck:
test: ["CMD", "wget", "-qO-", "http://127.0.0.1:3000/health"]
interval: 30s
timeout: 5s
retries: 3
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
- "443:443/udp"
volumes:
- ./conf:/etc/caddy
- caddy_data:/data
- caddy_config:/config
depends_on:
- app
logging:
driver: json-file
options:
max-size: "10m"
max-file: "3"
volumes:
caddy_data:
caddy_config:
初心者が特に間違えやすいのは、ports と expose の違いです。
| 書き方 | 外(インターネット)から届く? | 同じ compose のコンテナから届く? | この記事では |
|---|---|---|---|
ports: "80:80" |
届く(VPS のすべてのアドレスで公開) | 届く | caddy の 80・443 だけ |
expose: "3000" |
届かない | 届く(app:3000 で) |
app の 3000 |
ports: "127.0.0.1:3000:3000" |
届かない(VPS の中からだけ) | 届く | 使わない |
app を ports で公開してしまうと、Caddy を通らずに http://VPSのIP:3000 で誰でもアプリに直接つながってしまいます。外に出す入口は caddy だけにするのが基本です。
残りの設定の意味も押さえておきましょう。
restart: unless-stopped: アプリが落ちたら自動で起動し直す。手で止めたときは止めたまま(後の節で詳しく)healthcheck: 30秒ごとに/healthを呼び、3回続けて失敗したら「unhealthy」と表示する。docker compose psで状態がひと目で分かるlogging: ログを 10MB ずつ最大3ファイルまで残し、古い分は消す(後の節で詳しく)caddy_data: Caddy が取った証明書と秘密鍵の保存場所。ここを消すと証明書を取り直すことになるので、docker compose down -v(ボリュームも消す)は本番では使わない./conf:/etc/caddy: Caddyfile はファイルではなくフォルダごと渡す。Caddy の公式イメージの説明で、ファイルを直接渡すと編集が反映されないことがあると注意されているためdepends_on: caddy を app の後に起動する。ただし「app の準備ができるまで待つ」わけではない
Caddyfile(conf/Caddyfile)は、まず手元で試す用の形です。「80番に来たものを app の 3000 番に渡す」だけの3行です。
conf/Caddyfile(手元で試す用)
:80 {
reverse_proxy app:3000
}
app はサービス名です。同じ compose.yaml のコンテナ同士は、サービス名で呼び合えるので、IP アドレスを書く必要はありません。
VPS で公開するときは、:80 の部分を自分のドメインに書き換えます。これだけで Caddy が HTTPS の証明書を自動で取得し、http から https への転送も自動で設定します。
conf/Caddyfile(VPS で公開する用)
example.com {
reverse_proxy app:3000
}
手順④:VPSで起動して確認する
ここからは VPS での作業です。この節の VPS 上の操作は、Docker・Caddy の公式ドキュメントの手順を整理したものです(下の「実際にセットアップしてみた」はテスト用の環境で動かしたもので、VPS・ドメインでの公開はしていません)。
① VPS の初期設定を先に済ませる
Docker を入れる前に、SSH の鍵認証・root ログインの禁止・ファイアウォールなどの初期設定を済ませます。手順は VPS ごとにまとめています。


② Docker を公式の手順で入れる
Ubuntu なら、Docker 公式の apt リポジトリから入れる方法が案内されています(2026年11月時点の対応版は Ubuntu 26.10・26.04 LTS・24.04 LTS・22.04 LTS の64ビット)。docker-compose-plugin も一緒に入るので、Compose を別に入れる必要はありません。Ubuntu・Debian では、Docker は VPS の起動時に自動で立ち上がります。

入れ終わったら、版を確かめます(テスト環境では次のように表示されました)。
ターミナル(VPS)
docker compose version
実行結果
Docker Compose version v5.6.1
注意点が2つあります。
get.docker.comの便利スクリプトは、公式が本番環境には勧めていません。VPS では apt リポジトリの方法を使う- 毎回
sudoを付けないために作業ユーザーをdockerグループに入れる方法もありますが、docker グループは root と同じ権限を持つ点は覚えておく
そしていちばん大事なのが、ufw(Ubuntu のファイアウォール)と Docker の関係です。

Docker で ports に書いたポートは、ufw のルールを通らずに外へ公開されます。「ufw で 3000 番を閉じたから大丈夫」は通用しません。だからこそ、外に出すポートは compose.yaml の ports に書いたものだけになるよう、app は expose にしておくのが安全です。ufw では SSH と 80・443 を許可しておけば十分です。
③ DNS を VPS に向ける
ドメインの管理画面で、A レコード(IPv6 なら AAAA レコード)を VPS の IP アドレスに向けます。Caddy の公式ドキュメントでも、証明書を取るために DNS を先に設定してから Caddy を起動するよう案内されています。
④ 起動して確認する
myapp フォルダを VPS にコピーし(git で clone してもかまいません)、フォルダの中で次を実行します。
ターミナル(VPS)
docker compose up -d --build
docker compose ps
curl -s -i http://localhost/
docker compose ps で app が 「Up … (healthy)」、caddy が 「Up」になっていれば成功です。手元のテスト環境で実際に動かしたときの表示は、下の「実際にセットアップしてみた」に載せています。ドメインを設定した場合は、ブラウザで https://example.com を開いて鍵マークが付くかを確かめます。
アプリを直したときは、同じ docker compose up -d --build をもう一度実行するだけです。変わった app だけが作り直され、caddy は動いたままになります。Caddyfile だけを直したときは、次の1行で再読み込みできます。
ターミナル(VPS)
docker compose exec -w /etc/caddy caddy caddy reload
ログの見方とローテーション設定
アプリが console.log で出した内容は、Docker がまとめて保存しています。見るときは docker compose logs を使います。
ターミナル(VPS)
docker compose logs app --tail 10
docker compose logs caddy
よく使うオプションは次のとおりです。
| オプション | 意味 | 使う場面 |
|---|---|---|
-f(--follow) |
新しいログを流し続ける | アクセスしながら様子を見る |
-n / --tail 10 |
最後の10行だけ | 長いログの最新だけ見たい |
--since 42m |
42分前から | 「さっき落ちた」の前後を見る |
-t(--timestamps) |
時刻を付ける | アプリが時刻を出していないとき |
サービス名(app・caddy) |
そのサービスだけ | 片方のログに絞る |
ここで見落としがちなのが、ログの上限です。Docker の既定の記録方式(json-file)は、何も設定しないとログを無制限にため続け、ローテーション(古いログの削除)をしません。アクセスの多いアプリを半年動かすと、小さな VPS のディスクを圧迫しかねません。
そこで compose.yaml に max-size: "10m" と max-file: "3" を書いています。1つのコンテナあたり最大 10MB × 3ファイル=約30MBで頭打ちになります。
| 設定 | ログの上限 | 向いている使い方 |
|---|---|---|
| 何も書かない(既定) | 無制限 | おすすめしない |
max-size: "10m" だけ |
10MB(1ファイル) | 古いログはほぼ残らない |
max-size: "10m" + max-file: "3" |
約30MB | 小さな VPS の標準 |
Docker 全体の既定を /etc/docker/daemon.json で変える方法もありますが、その変更は新しく作るコンテナにしか効きません。compose.yaml にサービスごとに書いておけば、どの VPS に持っていっても同じ設定になります。
再起動設定:restartの4つの違い
restart には4つの値があります。違いは「どんなときに起動し直すか」です。


Web アプリの公開には、unless-stopped がいちばん扱いやすいと考えています。理由は次の2つです。
- アプリが落ちても、VPS を再起動しても、自動で戻ってくる
- メンテナンスで手で止めたときは、止めたままになる(
alwaysだと Docker の再起動で勝手に起動してしまう)
注意点として、起動してから10秒以上動いていないコンテナは、再起動の対象になりません。起動直後に落ちるアプリ(設定ミスなど)が延々と再起動を繰り返すのを防ぐためです。「restart を書いたのに戻ってこない」ときは、まず docker compose logs app で起動時のエラーを見てください。
うまく動かないときのチェック
つながらないときは、外側から順に原因を絞り込みます。

| 症状 | よくある原因 | 確認・対処 |
|---|---|---|
| 502 Bad Gateway | app が止まっている・起動に失敗している | docker compose ps、docker compose logs app |
| ブラウザでつながらない | VPS のファイアウォールで 80・443 が閉じている、DNS が未反映 | VPS の管理画面・ufw の設定、A レコード |
| https にならない | Caddyfile が :80 のまま、DNS が VPS を向いていない |
Caddyfile にドメインを書く、docker compose logs caddy |
| caddy が起動しない | 80・443 を別のソフト(Apache・Nginx など)が使っている | そのソフトを止めるか、ポートを分ける |
npm ci でエラー |
package-lock.json が無い・package.json と合っていない |
手元で npm install し直してロックファイルも置く |
| 止めるのに10秒かかる | SIGTERM を受けて終わる処理が無い | server.js の SIGTERM の処理を入れる |
実際にセットアップしてみた
2026年11月13日に、テスト用の Linux 環境(Docker)で次の手順を実際に動かしました。使ったのは Docker 29.9.2・Docker Compose v5.6.1 で、ファイルはこの記事の server.js・package.json・Dockerfile・.dockerignore・compose.yaml・Caddyfile(:80 の形)そのままです。
① イメージを作って起動する
myapp フォルダの中で、イメージの作成と起動をまとめて行います。
ターミナル
docker compose up -d --build
実行結果(npm ci の部分)
added 65 packages, and audited 66 packages in 1s
found 0 vulnerabilities
Dockerfile の npm ci --omit=dev で 65個のパッケージが入り、ネットワーク・ボリューム2つ・コンテナ2つ(app と caddy)が作られて起動しました。
② 状態を見て、アクセスしてみる
ターミナル
docker compose ps
curl -s -i http://localhost/
curl -s http://localhost/health
curl -s -o /dev/null -w '%{http_code}\n' --max-time 3 http://localhost:3000/

約35秒後の docker compose ps で app が 「Up 35 seconds (healthy)」 になり、http://localhost/ は 200 OK と JSON を返しました。ヘッダーの 「Via: 1.1 Caddy」 が、Caddy を通って届いた印です。app の 3000 番に直接つなぐと 000(つながらない) になり、expose だけで外に公開されていないことが分かります。
③ 落ちたら戻ってくるか試す
restart: unless-stopped の動きを、アプリの異常終了と、手で止めた場合の2通りで試しました。最初の kill -9 は、ホストから app の Node.js のプロセスを強制終了するものです(PID は環境ごとに違います)。
ターミナル
kill -9 25018
docker compose ps app
docker inspect -f 'RestartCount={{.RestartCount}}' myapp-app-1
curl -s http://localhost/
docker kill myapp-app-1
docker compose ps -a app
curl -s -o /dev/null -w '%{http_code}\n' http://localhost/

強制終了すると約1秒後に自動で起動し直し、RestartCount が 1 になってすぐに JSON が返りました。docker kill で手で止めた場合は再起動されず、5秒後も「Exited (137)」のままで、http://localhost/ は 502 になりました(Caddy は動いているがアプリがいない状態)。止めたアプリは次の1行で戻せます。
ターミナル
docker compose start app
起動し直したあとの curl では、ふたたび 200 の JSON が返りました。
④ ログとログの設定を確かめる
ターミナル
docker compose logs app --tail 10
docker compose logs caddy | grep -E 'auto_https|502|no such host' | tail -3
docker inspect -f '{{json .HostConfig.LogConfig}}' myapp-app-1

app のログには、起動のたびの「listening on port 3000」(最初の起動・自動の再起動・手での start の3回)とアクセスの記録が残っていました。caddy のログには、app が止まっていた間の 「no such host」と status 502、:80 の設定では自動 HTTPS が働かないという注意が出ています。ログの設定は max-size 10m・max-file 3 が効いていました。
⑤ コードを直して作り直す・Caddyfile を読み直す
server.js を1行変えて docker compose up -d --build をもう一度実行すると、npm ci の層はキャッシュ(CACHED)が使われ、app だけが作り直され(Recreate)、caddy は動いたまま(Running)でした。curl では変更後の文言が返りました。
Caddyfile の読み直しは次の1行です。
ターミナル
docker compose exec -w /etc/caddy caddy caddy reload
「using adjacent Caddyfile」「adapted config to JSON」と表示され、エラーなく読み直されました。また docker compose stop app では、app のログに「SIGTERM received, closing server」が出て、待たされずに止まりました(server.js の SIGTERM の処理が効いている)。
つまずいた所: 最初は Caddyfile をファイル1つだけ /etc/caddy/Caddyfile に渡していました。Caddy の公式イメージの説明では、ファイルを直接渡すと編集が反映されないことがあるとされているため、./conf:/etc/caddy とフォルダごと渡す形に直して、もう一度最初から動かしています。この記事の compose.yaml は、直したあとの形です。
⑥ 公開用の Caddyfile を確かめて、片付ける
ドメインを書いた Caddyfile(example.com { … })は、caddy validate で文法をチェックすると、「enabling automatic HTTP->HTTPS redirects」「Valid configuration」と表示され、http から https への転送も有効になる設定です(証明書は取得していません)。
最後に、試したコンテナ・ボリューム・ネットワークを削除しました。
ターミナル
docker compose down -v
-v を付けると Caddy の証明書の保存場所(caddy_data)も消えるので、本番の VPS では使わないでください。
この先は公式の手順
VPS の初期設定・Docker のインストール・DNS の設定・証明書の取得と外部からのアクセスは、今回は行っていません。上の「手順④:VPSで起動して確認する」で、Docker と Caddy の公式ドキュメントの手順として説明しています。
よくある質問
Q. VPS のメモリはどれくらい必要ですか? A. この記事の構成(Node.js のアプリ1つ+Caddy)は軽いものですが、必要な量はアプリしだいです。アプリの中身とアクセス数で決まるので、まずは小さめのプランで動かし、実際の使用量を見てから上げるのが無理のない進め方です。VPS の選び方はこちらで比べています。


Q. Nginx ではだめですか? A. Nginx でも同じ構成は作れます。ただし証明書の取得・更新(Let’s Encrypt の certbot など)を自分で組み込む必要があります。初めてなら、ドメインを書くだけで HTTPS になる Caddy の方が手順が少なく済みます。
Q. docker-compose(ハイフン付き)と docker compose の違いは?
A. 今の Docker では、Compose は docker compose(スペース区切り)のプラグインとして入ります。公式の apt リポジトリから入れれば docker-compose-plugin が一緒に入るので、この記事のコマンドはすべてスペース区切りで書いています。
Q. compose.yaml の先頭に version: "3" は要りますか?
A. この記事の compose.yaml には書いていませんが、そのまま動きました。ファイル名は、Docker の公式ドキュメントで compose.yaml が推奨されています。
Q. 環境変数(API キーなど)はどこに書けばいいですか?
A. 本番の秘密の値は compose.yaml に直接書かず、.env などのファイルに分けて git に入れないようにします。この記事の .dockerignore で .env をイメージに入れないようにしているのも、そのためです。
まとめ
Docker Compose で Node.js のアプリを VPS に公開する手順のポイントをまとめます。
- 用意するもの:server.js・Dockerfile・compose.yaml・conf/Caddyfile(+package-lock.json)
- Dockerfile:node:26-alpine、
npm ci --omit=dev、USER node、CMD ["node", "server.js"] - 外に出すポート:caddy の 80・443 だけ(app は
expose。ufw は Docker の公開ポートを止められない) - HTTPS:Caddyfile にドメインを書くだけ(DNS を先に向ける・
caddy_dataを消さない) - ログ:max-size 10m・max-file 3 で約30MB まで(既定は無制限)
- 再起動:restart: unless-stopped(落ちたら戻る・手で止めたら止まったまま)
- 困ったら:502 は app、つながらないのはファイアウォールと DNS から確認
まずは手元の PC で :80 の形のまま動かしてみて、docker compose ps が healthy になったら、VPS の初期設定とあわせて公開に進んでみてください。
