Building small Docker images faster

2025/12/12 19:23

Building small Docker images faster

RSS: https://news.ycombinator.com/rss

要約

Japanese Translation:

要約

著者は、Python を中心とした $DAYJOB で Go の RCE‑as‑a‑service を構築しました。速度と人気を理由に Rust や Nix よりも Go を選択しました。
最初は Nix を使って OCI イメージを作成し、最小例(

pkgs.dockerTools.streamLayeredImage
)で
/hello
だけが入った 45.8 MB のイメージを生成しました。
既に Docker/Docker Compose が使用されていたため、それへ切り替え、Go バイナリを静的リンク(
CGO_ENABLED=0
)し、マルチステージビルドで
scratch
まで減らすことで Goose マイグレーションツールのイメージを 15.9 MB に削減する方法を示しました。

投稿では実際のビルド時最適化も紹介しています:

  • ビルドコンテキストを小さく保つ—ディレクトリ内全てが送られますが、
    .dockerignore
    で除外できます。推奨エントリーは
    *
    Dockerfile*
    docker-compose.yml
    (デフォルトでは
    .git
    は無視されません)。
  • ソースファイルにはバインドマウントを使い、Go のビルドキャッシュはキャッシュマウントで共有し、コピーは最小限に。
  • 複数アプリのステージを分けると BuildKit が並列で構築できます。
  • 外部ダウンロードや Git リポジトリには
    ADD
    を使い、ローカルファイルには
    COPY
    を使用します。

最後に Docker Compose の watch モードはコンテキストを監視し、ファイルが変更されると自動でイメージを再構築して開発中の高速反復を可能にします。
イメージサイズを 45 MB から 16 MB 未満に削減することで、チームはストレージコストを節約し、ネットワーク転送時間と CI パイプラインの実行時間を短縮、クラウドデプロイメントコストも低減でき、開発者・DevOps 及び業界全体にメリットが広がります。

本文

Goを選んだ理由

$DAYJOB で初めて Go サービスを構築することになりました。社内はほぼ Python 専門ですが、なぜ Go を選択したかというと、同僚の中にはゴーファー(Gopher)好きもいるし、Go は簡潔で大手企業にサポートがあり、Python よりずっと高速なので、Rust や残念ながら Nix ではなく Go を推進しやすいという判断でした。

私の担当したプロジェクトは「RCE‑as‑a‑Service」的なものです。リモートでコードを実行できる言語の中で信頼できるのが Go の実装しか無く、いつも通りに「スネークロップ」をやらずに一息つける余地があると感じました。Nix に特化した環境を作ろうと短期的に試してみたものの、結局 Docker と Docker Compose(我々が通常使っているツール)へ切り替えました。


Nix で最小限のイメージ

OCI イメージ構築には Nix が得意です。以下はサービスだけを入れた極小イメージ(

/bin/sh
は含まない)の例です。

{ pkgs ? import <nixpkgs> {} }:

pkgs.dockerTools.streamLayeredImage {
  name = "someimage";
  tag  = "latest";
  config.Cmd = [
    "${pkgs.hello}/bin/hello"
  ];
}

nix-build docker-test.nix
でビルドし、
./result
に生成されたスクリプトを実行してイメージをロードします。

./result | docker load
docker image ls

結果は 45.8 MB。ほとんどが glibc のサイズです。


Docker で同等の構築

静的 Go バイナリ(

CGO_ENABLED=0
)をビルドし、便利機能(例:coreutils)をやめれば、ほぼ同じ結果に仕上げられます。

サンプル:最小イメージ

データベースマイグレーションには goose を使っています。

docker-compose.yml
では DB が起動した直後にそのコンテナを走らせています。

services:
  migrate:
    image: migrate:latest
    pull_policy: build
    build:
      context: https://github.com/pressly/goose.git#v3.26.0
      dockerfile: $PWD/Dockerfile.migrate
    environment:
      GOOSE_DBSTRING: postgresql://AzureDiamond:hunter2@db:5432/bobby
      GOOSE_MIGRATION_DIR: /migrations
      GOOSE_DRIVER: postgres
    depends_on:
      db:
        condition: service_started
    volumes:
      - ./migrations:/migrations

build.context
はイメージ構築時に Docker が利用できるファイルの集合です。通常は
.
(現在のディレクトリ)ですが、GitHub の URL とタグを指定すれば、そのコミットのリポジトリルートが使われます。Docker で可能なのに知られていないテクニックです。

build.dockerfile
$PWD
を書く理由は、Compose がコンテキスト外ファイルを参照する際に絶対パスしか受け付けないためです。他のディレクトリから
docker compose
を実行すると壊れる可能性がありますが、ここでは問題ありません。

Dockerfile

FROM golang:1.25-alpine3.23 AS builder
WORKDIR /build

ARG CGO_ENABLED=0
ARG GOCACHE=/root/.cache/go-build
ARG GOMODCACHE=/root/.cache/go-mod

RUN --mount=type=cache,target=$GOCACHE \
    --mount=type=cache,target=$GOMODCACHE \
    --mount=type=bind,source=.,target=/build \
    go build -tags='no_clickhouse no_libsql no_sqlite3 no_mssql no_vertica no_mysql no_ydb' \
    -o /goose ./cmd/goose

FROM scratch
COPY --from=builder /goose /goose
CMD ["/goose", "up"]
  • Alpine をビルダーに使うことで軽量化。
  • CGO_ENABLED=0
    で静的リンク済みバイナリを保証。
  • --mount=type=cache
    により BuildKit が Go のビルドキャッシュを再利用。
  • ソースは
    --mount=type=bind,source=.,target=/build
    でコピーせずにマウントし、余計なコピーを防止。
  • 最終ステージは
    scratch
    にしてイメージ内にバイナリだけ残す。

結果は 15.9 MB の単層イメージで、ビルド・ロード・起動が瞬時です。


ビルドコンテキスト

Docker は

docker build
を実行したディレクトリ以下の 全て をビルダーに送ります。不要なファイルが多いと転送量が増えるので、
.dockerignore
で除外します。

# .dockerignore
.*
Dockerfile*
docker-compose.yml

重要点:コンテキストには

.dockerignore
にリストされていない全てのファイルが含まれます。
.git
,
./.jj
, Dockerfile 自体、ビルドアーティファクトなどはすべて送られます。


レイヤーを細分化

レイヤーを分割・並べ替えることでビルド時間とキャッシュ利用率が向上します。以下はもう少し複雑な例です。

FROM golang:1.25-alpine3.23 AS builder
WORKDIR /build

ARG CGO_ENABLED=0
ARG GOCACHE=/root/.cache/go-build
ARG GOMODCACHE=/root/.cache/go-mod

# 依存ツールを早めにインストール(例:orchestrion)
RUN --mount=type=cache,target=$GOCACHE \
    --mount=type=cache,target=$GOMODCACHE \
    go install github.com/DataDog/orchestrion@latest

# 依存関係のダウンロード(変更頻度が低い)
RUN --mount=type=bind,source=go.mod,target=go.mod \
    --mount=type=bind,source=go.sum,target=go.sum \
    --mount=type=cache,target=$GOCACHE \
    --mount=type=cache,target=$GOMODCACHE \
    go mod download

# バイナリのビルド
RUN --mount=type=bind,source=go.mod,target=go.mod \
    --mount=type=bind,source=go.sum,target=go.sum \
    --mount=type=cache,target=$GOCACHE \
    --mount=type=cache,target=$GOMODCACHE \
    --mount=type=bind,source=internal,target=internal \
    --mount=type=bind,source=cmd,target=cmd \
    --mount=type=bind,source=orchestrion.tool.go,target=orchestrion.tool.go \
    orchestrion go build -o server ./cmd/server

FROM alpine:3.23 AS prod
WORKDIR /app
COPY --from=builder /build/server .
ENTRYPOINT ["/app/server"]
  • 変更頻度が低いツール(
    orchestrion
    等)を早期にインストール。
  • 依存関係はコードより先にダウンロードしてキャッシュ化。
  • 各ステップで必要なディレクトリだけをバインドマウントし、レイヤーサイズとキャッシュ効率を最適化。

結論

テクニック何ができるか
キャッシュ活用Docker の「Optimize cache usage in builds」ページ参照。
レイヤー順序
ENV
,
ARG
,
WORKDIR
を先に置き、頻繁に変わるステージは後ろへ。
マルチステージ最終イメージを
scratch
,
busybox
,
alpine
で縮小。
アプリ別ビルド各アプリを独立したステージで構築し、BuildKit が並列実行。
.dockerignore
.git
等を除外してコンテキスト転送を高速化。
キャッシュマウントGo のビルドキャッシュを永続化(GitHub Actions で外部キャッシュ利用可)。
バインドマウント vs COPYソースは
bind
を優先、外部ファイル取得には
ADD
を使用。
小さなベースイメージパッケージマネージャが必要なら中間イメージを作り、ランナーが Debian ミラーにアクセスしないようにする。
Compose watchDocker Compose はファイル変更時に自動で再ビルドできるウォッチモード付き。

以上です。お役に立てれば幸いです。 Happy building!

同じ日のほかのニュース

一覧に戻る →

2025/12/13 5:57

GNU Unifont

2025/12/13 7:02

Show HN: Tiny VM sandbox in C with apps in Rust, C and Zig

## Japanese Translation: uvm32 は、単一の C ファイルで書かれたミニマリストで依存関係を持たない仮想マシンサンドボックスです。 STM32L0 のような非常に小型のマイクロコントローラ上で動作し、4 KB 未満のフラッシュと 1 KB の RAM を使用します。静的割り当てのみで非同期設計となっています。 この VM は RISC‑V のサブセットを実装しており、軽量な管理インタフェースを公開しています。「if‑this‑then‑that」ロジックのために Lua、Duktape、MicroPython などの軽量スクリプトエンジンを置き換え、信頼できないコンポーネントや不安定な部品をサンドボックス化し、ターゲットコンパイラなしでモダン言語の「一度書けばどこでも実行できる」スクリプトを書けるようにすることが目的です。 主な特徴: - バイトコードアプリは C、Zig、Rust、またはアセンブリで記述可能。 - 非ブロッキングでシンプルな実行モデル。安全かつ最小限の型付けを備えた FFI を使用し、ホスト IO(stdio/ネットワーク)は想定していません。 - デザインは高速よりも安全性を優先しています。 - すべてのソースコードは `uvm32/` ディレクトリにあります。最小限のホスト例は `host‑mini` にあり、より高度なホストは `host/`、`host-parallel`、`host-arduino` にあります。 サンプルアプリケーションは VM の機能を示しています(C: helloworld, heap, conio, lissajous, maze, fib, sketch; Zig: zig‑mandel, zigtris, zigalloc, zigdoom; Rust: rust‑hello; アセンブリ: hello‑asm)。 ビルドとテスト用の Dockerfile が提供されており、`make dockerbuild`、`make dockershell` で構築・起動し、その後 `make` を実行してサンプルをコンパイル・実行します。ドキュメントはヘッダファイル `uvm32/uvm32.h` と `doc/README.md` にあります。本プロジェクトは MIT ライセンスで公開されています。

2025/12/13 5:15

Rats Play DOOM

## Japanese Translation: > **概要:** > 著者らは、ラットがDOOMをプレイできる完全にオープンソースの仮想現実装置をリリースしました。ゼロから構築されたこのシステムには、ハードウェア設計・ファームウェア・ソフトウェアがGitHubに掲載されており、他研究室でも簡単に再現または改良できます。バージョン 1(v1)はニューヨークのヴィクトール・トー氏によって開発され、ラットにDOOMコリドーを走行させるよう訓練しました。この実装はViceとPC Gamerで紹介されました。 > > 改良版(v2)はよりモジュラー化され、180°×80°の視野を持つ折りたたみ可能なAMOLEDスクリーン、新しいボールドライバー、強化された給餌器、ゲームイベントに同期した正確な10 µLの砂糖水投与が可能な改良リワード回路を備えています。追加センサーとランニングマシンのボール周囲に設置された光学フロー運動捕捉システムでトラッキング精度が向上しています。 > > ソフトウェアはPythonベースのモジュラースタック(arena_scenario.py)で、PC上で実行され、Raspberry PiとTCP経由で通信します。Piはリアルタイムセンサー読み取り・ボール駆動・リワード制御を担当します。すべてのコンポーネントはGitHubに文書化されており、3Dプリント可能なパーツや回路図も公開されています。 > > チーム(ヴィクトール・トー=ゲーマーラットコーチ、サンドル・マクラ=電気技師、アコス・ブラシュェク=ドキュメントリード)は、ラットが約2週間で慣れることを示しましたが、完全な訓練はまだ完了していません。 > > 現在の制限として、自動キャリブレーションスイートが無いため、ユーザーはセンサーの整列とリワードタイミングを手動で検証する必要があります。 > > 今後の課題は完全な訓練プロトコルの完成、キャリブレーションツールの改良、および他の行動タスクや種へ装置を拡張することです。低コストで完全にオープンなプラットフォームを提供することで、このプロジェクトは世界中の神経科学研究室の参入障壁を下げ、動物VR実験に依存する研究のスピードアップに寄与できる可能性があります。