Dockerfile初心者が0から軽量イメージを作って、動作確認までやってみた
マルチステージビルドで軽量化&コンテナ内部からの動作確認で理解が深まったので共有します
はじめに
フロントエンドエンジニアのゆず(@yuzunosk55)です。
プライベートでバックエンドやインフラ領域の理解を深めています。
この記事では、Honoを使って構築したアプリケーションのDockerfileを作る方法とDockerコンテナで動作させる方法を解説しています。
コンテナ起動後は、実際にリクエストなどを送り動作確認まで行ったのでその方法についても学んだ事を書いています。
初学者の参考になれば幸いです。
※ 未熟な身ですので誤りもあると思います。良かったらコメントなどでご指摘いただけるとありがたいです。
1. この記事について
こんな方におすすめです
- Dockerfileの書き方を知りたい方
- CLIを使った古典的な動作確認方法が知りたい方
- Dockerを勉強している初学者
前提知識
- Node.js/TypeScriptの基本的な知識
- Linuxコマンド
- Dockerの基本的なコマンド操作
- REST APIの基本概念
この記事では扱わない内容
- 言語の知識
- アプリケーションの作成方法
- Dockerコマンドの詳細な説明
- Linuxコマンドの詳細な説明
- 出てくる技術の詳細な解説
学習の動機
Dockerfileを作成する時に、自力で書けるようになりたいと思った事、今後kubernetesなどの技術をキャッチアップをしたいと思ったことが動機です。
2. APIサーバーの作成
まずはTypeScript、Honoを使って簡易なAPIサーバーのコードを書いていきます。
Dockerの動作確認に必要な最小限のAPIを用意します。
(※ 本筋ではないため、サーバー起動に必要な部分だけ記述したという流れで進めていきます。)
作成するAPIの概要
-
/api/health- サーバーの生存確認用(ヘルスチェック) -
/api/details- コンテナのホスト名と現在時刻を返す(動作環境確認用)
これらのAPIは後で、動作確認に使用します。
import { serve } from '@hono/node-server'
import { Hono } from 'hono'
import os from 'os'
// Appオブジェクトを作成
function createApp() {
const app = new Hono()
// 動作確認用API
app.get('/api/health', (c) => {
return c.json({ status: 'ok' })
})
app.get('/api/details', (c) => {
const now = new Date()
return c.json({
host: os.hostname(), // コンテナのホスト名が取得できる
time: now.toString(),
})
})
return app
}
// appを作成し、サーバーを起動する
function main() {
const app = createApp()
const port = 3000
serve(
{
fetch: app.fetch,
port: Number(port),
},
(info) => {
console.log(`Listening on http://localhost:${info.port}`)
},
)
}
main()
package.jsonはこのようになっています。
今回はBunを使っていますが、他のパッケージ管理ツールなどで問題ありません。
npm scriptsのstartコマンドは、あえてnodeで実行しています。これは、本番環境での安定性を考慮したかったからです。
packageのインストール
bun add -D @types/bun @types/node typescript
bun add hono @hono/node-server
{
"name": "backend",
"scripts": {
"dev": "bun run --hot src/main.ts",
"build": "tsc",
"start": "node dist/main.js"
},
"dependencies": {
"@hono/node-server": "^1.14.2",
"hono": "^4.7.10"
},
"devDependencies": {
"@types/bun": "latest",
"@types/node": "^22.15.29",
"typescript": "^5.8.3"
}
}
ここまで書けたら、ローカル環境でサーバーが起動するか確認します。
/api/healthにリクエストを送って、JSONが返ってくればOKです!
$ bun run dev
$ bun run --hot src/main.ts
Listening on http://localhost:3000
curlコマンドで確認
curl localhost:3000/api/health
{"status":"ok"}
3. 軽量なDockerfileの作成
Dockerコンテナで行いたいのは、アプリケーションの動作環境構築です。
TypeScriptとHonoで構築している事を考えると、TypeScriptで書いたコードをコンパイルし、node.js(実行環境)で動く状態にすれば良さそうです。
アプリケーションを動作させるために必要なものと、コンパイルするのに必要なもの、それぞれ言語化しました。
動作するのに必要なもの
- コンパイルされたプログラム
- 動作に必要なパッケージ
- パッケージを管理するツール
プログラムをコンパイルするのに必要なもの
- TypeScript
- 開発環境に必要なパッケージ
- パッケージ管理ツール
これらを踏まえて、Dockerfileを作成していきます。
DockerHubからimageをpullしてくる
必要なimageをDockerHubからダウンロードしてくる必要があります。
最初はNode.jsのimageを取ってこようかと考えたのですが、ビルドをなるべく速くしたいと考えBunのイメージを取ってくることにしました。
GoogleChromeで「docker bun image」と検索すればでてくるので、自分の使う言語で検索してみると良いと思います。
Variantsとは?
Variants(バリアント)とは、同じDockerイメージでも 異なるベースOSや構成で提供されているバージョンのことのようです。
Bunの場合、以下のようなVariantsが提供されています。
Variantsには、それぞれ下記の特徴があるようです。
この辺りは詳しくないため詳しく語ることはできませんが、記事もありますので興味のある方は深堀りしていただけるとよさそうです。
今回はalpineを使っています。
| Variant | サイズ | 用途 | 特徴 |
|---|---|---|---|
| latest | 大 | 開発環境 | フル機能、デバッグツール豊富 |
| alpine | 小 | 本番環境 | 軽量、セキュリティ良好 |
| slim | 中 | バランス重視 | 必要最小限のパッケージ |
| distroless | 最小 | 本番環境 | シェルなし、超セキュア |
OverviewからTagsというページに切り替えて、alpineを探します。
ページの下の方にありました。

docker pull oven/bun:alpine
Dockerfileをルートディレクトリに作成し、使用するimageを記述しておきます。
FROM oven/bun:alpine AS builder
Dockerfile:ビルドステージを書く
ここではコンパイルする部分の処理を、Dockerfileに記述していきます。
まず、さきほど取得してきたimageを確認します。
docker image listを実行するとimage一覧確認できます。
一応似たようなnode.jsのimageも取得して比較してみました。
bunの方がすこし小さいみたいです。
docker image list
REPOSITORY TAG IMAGE ID CREATED SIZE
node 22.16.0-alpine 41e4389f3d98 4 days ago 226MB
oven/bun alpine 37b37b8cefbf 6 days ago 150MB
このステップでやりたい事は、ソースコードのコンパイルです。
それを元にやりたいことを記述していきます。
実際に手を動かして作成していきましたが、パス間違いやnpm scriptsの間違い、パッケージの不足などこの時点でいくつか問題が発生しました。
それらのミスを解消することで理解が深まるかと思いますので、ぜひAIで作らず手を動かしてみてください。
段階的に、やりたい処理を記述していきます。
FROM oven/bun:alpine AS builder
RUN bun run build ← ①最終的にビルドを実行したい
ビルドはtscを使うので、tsconfig.jsonとソースコードが必要です。
FROM oven/bun:alpine AS builder
WORKDIR /app ← ②慣習的なディレクトリを作成し`/app`で作業開始
COPY tsconfig.json ./ ← ②必要な設定ファイルをコピー
COPY ./src/ ./src/ ← ②ソースコードをコピー
RUN bun run build
あとは、開発用のパッケージも必要になるので、package.jsonとbun.lockもコピーします
FROM oven/bun:alpine AS builder
WORKDIR /app
COPY package.json bun.lock ./ ← ③パッケージ管理ツールの設定ファイルをコピー
COPY tsconfig.json ./
COPY ./src/ ./src/
RUN bun run build
Dockerfile:本番ステージを書く
一旦ここまでで、動作確認してみましょう!
docker build -t hono-node-app:v1 . --no-cache
-t を付ける事でビルドするイメージに名前を付けられます。
正確には、名前とタグを付けられます!タグを使うとイメージのバージョン管理ができます。
--no-cacheを付けているのは、変更時に正しく最初から作り直したいからです。
Dockerfileを更新しても、キャッシュを使ってビルドが走り反映されないことがあるため、更新後はつけておいた方が良い気がします。
以下のようなメッセージが表示されて、エラーでてなさそうであればOKです!
[+] Building 28.7s (13/13) FINISHED
ビルドはうまく出来ているので、それを本番用に使います。
本番用のimageに必要なのは以下です。
- コンパイルされたソース
- 動作に必要なパッケージ
- パッケージを管理ツールの設定ファイル
試行錯誤し、完成したDockerfile
# === ビルドステージ ===
FROM oven/bun:alpine AS builder
WORKDIR /app
COPY package.json bun.lock ./
RUN bun install
COPY tsconfig.json ./
COPY ./src/ ./src/
RUN bun run build
# === 本番ステージ ===
FROM oven/bun:alpine AS production
WORKDIR /app
# 同じようにパッケージ管理ツールの設定ファイルをコピー
COPY package.json bun.lock ./
# 本番環境用のパッケージインストール
RUN bun install --frozen-lockfile --production --verbose
# ビルドステージで作ったコンパイル済みのファイルをコピー
COPY --from=builder /app/dist ./dist
EXPOSE 3000
CMD ["bun", "start"]
本番環境用のパッケージインストールでは、オプションで以下を指定しています。
- lockファイルを使ったバージョンを固定したインストール
- dependenciesのパッケージのみを指定
- 詳細ログを出力
先ほどと同じようにdocker buildコマンドでimageを作ります。
image一覧に追加されていればOKです!
REPOSITORY TAG IMAGE ID CREATED SIZE
hono-node-app v1 fc9c1da849d8 34 seconds ago 156MB
node 22.16.0-alpine 41e4389f3d98 4 days ago 226MB
oven/bun alpine 37b37b8cefbf 6 days ago 150MB
コンテナの起動
imageが完成したら、実際にコンテナを起動してみましょう。
docker run -p 3000:3000 hono-node-app:v1
オプションの説明:
-
-p 3000:3000: ホストの3000番ポートをコンテナの3000番ポートにマッピング
起動状態の確認
コンテナが正常に起動しているか確認します。
docker psを実行して、以下のような表示が出ればOKです!
docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
facde6f0036b hono-node-app:v1 "/usr/local/bin/dock…" 40 seconds ago Up 39 seconds 0.0.0.0:3000->3000/tcp peaceful_spence
ホストからの動作確認
ホスト側からAPIが叩けるか確認します。
curl localhost:3000/api/health
{"status":"ok"}
curl localhost:3000/api/details
{"host":"facde6f0036b","time":"Wed Jun 04 2025 22:02:08 GMT+0900 (Japan Standard Time)"}
4. コンテナ内外からの動作確認
今度は、仮に通信がうまく出来なかったとして、コンテナの内部から通信状態を確認をしてみます。
この方法を覚えておくと、トラブルシューティングの時に役立ちます。
コンテナへの接続
docker execコマンドを使ってコンテナ内のシェルに接続します。
接続には、コンテナ名やコンテナIDが必要となります。
それぞれdocker psで確認することができます。
docker exec -it {コンテナ名 or コンテナID} sh
オプションの説明:
-
-i: 対話的モード(interactive) -
-t: 疑似TTYを割り当て -
sh: Alpine Linuxで使用可能なシェル
成功すると、プロンプトが変わります。
/app #
この状態で、コンテナの中に入っています!
ネットワーク状態の確認
次のコマンドで、コンテナ内のネットワーク設定が確認できます。
ip a
コマンドを実行すると後述するような出力が表示されます。
よくわからない文字が並びますが、state UNKNOWNやstate DOWNのところは一旦考えず
state UPになっているところのinetをみます。
すると、inet 172.17.0.2/16 ~ 172.17.255.255の範囲がサブネットで使えることが書かれています。
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue **state UNKNOWN**
inet 127.0.0.1/8 scope host lo
2: gretap0@NONE: <BROADCAST,MULTICAST> mtu 1462 qdisc noop **state DOWN** qlen 1000
link/ether 00:00:00:00:00:00 brd ff:ff:ff:ff:ff:ff
3: eth0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue **state UP**
inet 172.17.0.2/16 brd 172.17.255.255 scope global eth0
ネットワーク探索
次に使用しているホストをさがすためにnmap というツールを使います。
ただしnmapはAlpineの場合入ってないことがあるので、インストールする必要があります。
# パッケージリストを更新
apk update
# 後でAPIを叩くのでcurlもついでにインストール
apk add nmap curl
インストールできたら、以下のコマンドを実行します。
これで、どのIPアドレスが使われているか調べることができます。
※ /16の範囲になると65,000台スキャンすることになり時間がかかるため、/24の範囲に限定しています。
/app # nmap -sn 172.17.0.0/24
Starting Nmap 7.95 ( https://nmap.org ) at 2025-06-04 13:25 UTC
Nmap scan report for 172.17.0.1
Host is up (0.000069s latency).
MAC Address: 3E:1D:3D:3D:56:E2 (Unknown)
Nmap scan report for facde6f0036b (172.17.0.2)
Host is up.
Nmap done: 256 IP addresses (2 hosts up) scanned in 1.98 seconds
以下の2つが使われていることが分かりました。
-
172.17.0.1: Dockerホスト(ゲートウェイ) -
172.17.0.2: 自分のコンテナ
通信テスト
使われているプライベートIPアドレスがわかったので、PINGで接続確認します。
ping -c 3 172.17.0.2
PING 172.17.0.2 (172.17.0.2): 56 data bytes
64 bytes from 172.17.0.2: seq=0 ttl=64 time=0.067 ms
64 bytes from 172.17.0.2: seq=1 ttl=64 time=0.208 ms
64 bytes from 172.17.0.2: seq=2 ttl=64 time=0.211 ms
--- 172.17.0.2 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 0.067/0.162/0.211 ms
外部への接続確認
外部のDNSサーバーに接続できるかを確認することもできます。
コンテナ内部から以下のコマンドを実行。
ping -c 3 8.8.8.8
PING 8.8.8.8 (8.8.8.8): 56 data bytes
64 bytes from 8.8.8.8: seq=0 ttl=63 time=13.063 ms
64 bytes from 8.8.8.8: seq=1 ttl=63 time=12.075 ms
64 bytes from 8.8.8.8: seq=2 ttl=63 time=18.232 ms
--- 8.8.8.8 ping statistics ---
3 packets transmitted, 3 packets received, 0% packet loss
round-trip min/avg/max = 12.075/14.456/18.232 ms
自分自身のAPIテスト
外部への通信がOKだったので、次は自分自身への接続確認をします。
コンテナ内からcurlを使って、APIを呼び出します。
curl http://172.17.0.2:3000/api/health
{"status":"ok"}
curl http://172.17.0.2:3000/api/details
{"host":"facde6f0036b","time":"Wed Jun 04 2025 22:37:09 GMT+0900 (Japan Standard Time)"}
5. まとめ
この記事では、Honoで構築したアプリケーションのDockerfileを作成し、コンテナで動作させる方法と、コンテナへの通信を使った動作確認について解説しました。
Dockerfileを自力で作成したり、各種通信状態の確認を実際に行ったことで今後のトラブルシューティングに活かせる知識やDockerfileが作成の手順が理解できました。
今後も学びを共有していこうと思います。
記事内に誤りなどあれば、コメントでお知らせいただけるとありがたいです。
ここまでお読みいただきありがとうございました。
Discussion