📦

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は後で、動作確認に使用します。

src/main.ts
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
package.json

{
  "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」と検索すればでてくるので、自分の使う言語で検索してみると良いと思います。

https://hub.docker.com/r/oven/bun

Variantsとは?

Variants(バリアント)とは、同じDockerイメージでも 異なるベースOS構成で提供されているバージョンのことのようです。
Bunの場合、以下のようなVariantsが提供されています。

Variantsには、それぞれ下記の特徴があるようです。
この辺りは詳しくないため詳しく語ることはできませんが、記事もありますので興味のある方は深堀りしていただけるとよさそうです。

今回はalpineを使っています。

https://zenn.dev/fuuji/articles/9eb7f2aefcd6c5

Variant サイズ 用途 特徴
latest 開発環境 フル機能、デバッグツール豊富
alpine 本番環境 軽量、セキュリティ良好
slim バランス重視 必要最小限のパッケージ
distroless 最小 本番環境 シェルなし、超セキュア

OverviewからTagsというページに切り替えて、alpineを探します。
ページの下の方にありました。

docker pull oven/bun:alpine

Dockerfileをルートディレクトリに作成し、使用するimageを記述しておきます。

Dockerfile
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で作らず手を動かしてみてください。

段階的に、やりたい処理を記述していきます。

Dockerfile
FROM oven/bun:alpine AS builder

RUN bun run build ← ①最終的にビルドを実行したい

ビルドはtscを使うので、tsconfig.jsonとソースコードが必要です。

Dockerfile
FROM oven/bun:alpine AS builder

WORKDIR /app ← ②慣習的なディレクトリを作成し`/app`で作業開始

COPY tsconfig.json ./ ← ②必要な設定ファイルをコピー
COPY ./src/ ./src/ ← ②ソースコードをコピー

RUN bun run build

あとは、開発用のパッケージも必要になるので、package.jsonとbun.lockもコピーします

Dockerfile
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

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 UNKNOWNstate 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