Docker + WebAssembly - コンテナの新時代

2025.12.13

公式ドキュメント

この記事の要点

• DockerがWebAssembly(Wasm)ワークロードをネイティブサポート開始
• 従来のLinuxコンテナと比較して起動時間が数ms、イメージサイズが数百KB〜数MB
• WASIp2の成熟でHTTPサーバーなど実用的なアプリケーションが構築可能に
• エッジ・サーバーレス・プラグインシステムなど多様なユースケース

Docker + WebAssemblyの概要

DockerはWebAssembly(Wasm)ワークロードのネイティブサポートを開始しました。従来のLinuxコンテナとWasmコンテナを同じDocker CLI/Docker Desktop/Docker Composeで管理でき、より軽量で高速な実行環境が利用可能になります。Wasmは本来ブラウザ向けのバイナリ形式として誕生しましたが、WASI(WebAssembly System Interface)の成熟によりサーバーサイドでも実用的に動作する環境が整ってきました。

背景

WebAssemblyは2017年にW3Cで標準化されたポータブルなバイナリ命令セットです。ブラウザのJavaScriptエンジンで高速実行できることに加え、CPUアーキテクチャやOSに依存しない中立的なバイナリ形式であることから、サーバーサイド・エッジ・組み込みなどへの応用が急速に広がっています。Dockerはこうした流れを受けて、containerdrunwasiプロジェクトを通じてWasmランタイム(WasmEdge、Wasmtime、Spinなど)を統合し、「Linuxコンテナと同じ操作感でWasmを実行する」体験を提供しています。

flowchart TB
    subgraph Docker["Docker Engine"]
        CLI["docker CLI / Compose"]
        ctrd["containerd"]
        runc["runc<br/>(Linux)"]
        runwasi["runwasi shim<br/>(Wasm)"]
    end
    CLI --> ctrd
    ctrd --> runc
    ctrd --> runwasi
    runwasi --> WE["WasmEdge / Wasmtime / Spin"]

なぜWasmなのか

ポイント: Wasmコンテナは起動時間・イメージサイズ・メモリ使用量で従来のLinuxコンテナを大きく上回る軽量さを持っています。

コンテナとWasmの比較

項目LinuxコンテナWasmコンテナ
起動時間数百ms〜秒単位数ms〜数十ms
イメージサイズ数十MB〜GB数百KB〜数MB
メモリ使用量数十MB〜数MB〜
セキュリティ名前空間+cgroup分離Capability-basedサンドボックス
ポータビリティOS/CPU依存完全に独立
言語サポート任意(Linuxで動作すれば)Rust/Go/C/C++/AssemblyScript等
システムコール自由WASIで限定

Wasmコンテナの作成

Rustでの例

// src/main.rs
use std::env;

fn main() {
    let name = env::args().nth(1).unwrap_or_else(|| "World".into());
    println!("Hello, {} from WebAssembly!", name);
}
# Cargo.toml
[package]
name = "hello"
version = "0.1.0"
edition = "2021"

[[bin]]
name = "hello"
path = "src/main.rs"
# Wasmターゲットをインストール
rustup target add wasm32-wasip1

# WASI向けにビルド
cargo build --target wasm32-wasip1 --release

# 出力: target/wasm32-wasip1/release/hello.wasm

Goでの例

// main.go
package main

import "fmt"

func main() {
    fmt.Println("Hello from Go + Wasm!")
}
# TinyGoを使った軽量ビルド
tinygo build -o hello.wasm -target=wasi ./main.go

Dockerfileの作成

# syntax=docker/dockerfile:1
FROM scratch
COPY ./target/wasm32-wasip1/release/hello.wasm /hello.wasm
ENTRYPOINT ["/hello.wasm"]

ビルドと実行

# Wasmイメージをビルド(マルチプラットフォーム)
docker buildx build \
  --platform wasi/wasm \
  --provenance=false \
  -t hello-wasm .

# 実行(WasmEdgeランタイム)
docker run --rm \
  --runtime=io.containerd.wasmedge.v1 \
  --platform=wasi/wasm \
  hello-wasm

# 実行(Wasmtime)
docker run --rm \
  --runtime=io.containerd.wasmtime.v1 \
  --platform=wasi/wasm \
  hello-wasm

WASIp2(WASI Preview 2)

新しいWASI仕様により、より多くの機能が利用可能になりました。ネットワーキング、非同期I/O、コンポーネントモデルなどが整備され、実用的なサーバーアプリケーションをWasmで記述できます

// ファイルシステムアクセス
use std::fs;

fn main() {
    let content = fs::read_to_string("/data/config.txt")
        .expect("failed to read config");
    println!("Config: {}", content);
}
# ホストディレクトリをマウント
docker run --rm \
  --runtime=io.containerd.wasmedge.v1 \
  --platform=wasi/wasm \
  -v ./data:/data \
  hello-wasm

HTTPサーバーの例(WasmEdge)

use hyper::service::{make_service_fn, service_fn};
use hyper::{Body, Request, Response, Server};

async fn handle(_req: Request<Body>) -> Result<Response<Body>, hyper::Error> {
    Ok(Response::new(Body::from("Hello from Wasm HTTP server")))
}

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let make_svc = make_service_fn(|_| async {
        Ok::<_, hyper::Error>(service_fn(handle))
    });
    let addr = ([0, 0, 0, 0], 8080).into();
    Server::bind(&addr).serve(make_svc).await.unwrap();
}

Docker Composeとの統合

# docker-compose.yml
services:
  api:
    image: my-wasm-api
    platform: wasi/wasm
    runtime: io.containerd.wasmedge.v1
    ports:
      - "8080:8080"
    environment:
      LOG_LEVEL: info

  web:
    image: nginx:alpine
    ports:
      - "80:80"
    depends_on:
      - api
    volumes:
      - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro

Kubernetesでの実行

runwasiを組み込んだcontainerdを使えば、KubernetesのPodとしてWasmコンテナを実行できます。

apiVersion: node.k8s.io/v1
kind: RuntimeClass
metadata:
  name: wasmedge
handler: wasmedge
---
apiVersion: v1
kind: Pod
metadata:
  name: wasm-app
spec:
  runtimeClassName: wasmedge
  containers:
  - name: app
    image: registry.example.com/hello-wasm:latest

ユースケース

エッジコンピューティング

flowchart LR
    Cloud["クラウド<br/>(フル機能)"]
    Edge["エッジノード<br/>(リソース制限)<br/>Wasmで軽量実行"]
    Device["IoTデバイス"]

    Cloud --> Edge --> Device

実践メモ: 低遅延・低電力が要求されるエッジ環境で、Wasmの小さなメモリフットプリントと高速起動は特に有効です。

サーバーレス関数

  • コールドスタートが数ms
  • メモリ効率が良く単位コストが下がる
  • マルチテナント環境でもサンドボックスで安全
  • FastlyのCompute@EdgeFermyonのSpinなどが採用

プラグインシステム

  • ホストアプリにサンドボックス化されたプラグインを組み込める
  • 言語非依存のインターフェース(WITインターフェース)
  • Envoy Proxy、Istio、Lapce、Figma等で実績

CI/CDのジョブランナー

  • Pull毎の軽量サンドボックスとして活用
  • ネットワーク・ファイルアクセスを明示的に許可する設計がセキュア

サポートされるランタイム

ランタイム特徴代表的な用途
WasmEdge高性能、AI/ML推論対応、TensorFlow統合Edge、AI、サーバーレス
WasmtimeBytecode Alliance、標準準拠汎用、研究開発
SpinFermyon製、HTTPハンドラに特化サーバーレス関数
wasmerマルチランタイム、幅広い言語SDKCLI、組み込み
# ランタイムの確認
docker info | grep -i wasm

# Docker Desktopで有効化:
# Settings > Features in development > Use containerd > Enable Wasm

実践: RustでHTTP APIを作りDocker化

// src/main.rs (Axum + tokio)
use axum::{routing::get, Router, Json};
use serde::Serialize;

#[derive(Serialize)]
struct Health { status: &'static str }

#[tokio::main(flavor = "current_thread")]
async fn main() {
    let app = Router::new()
        .route("/health", get(|| async { Json(Health { status: "ok" }) }))
        .route("/", get(|| async { "Hello, Wasm!" }));

    let listener = tokio::net::TcpListener::bind("0.0.0.0:8080")
        .await.unwrap();
    axum::serve(listener, app).await.unwrap();
}
# syntax=docker/dockerfile:1
FROM rust:1.80 AS builder
WORKDIR /app
COPY . .
RUN rustup target add wasm32-wasip1 \
 && cargo build --target wasm32-wasip1 --release

FROM scratch
COPY --from=builder /app/target/wasm32-wasip1/release/api.wasm /api.wasm
ENTRYPOINT ["/api.wasm"]
docker buildx build --platform wasi/wasm -t api-wasm .
docker run --rm -p 8080:8080 \
  --runtime=io.containerd.wasmedge.v1 \
  --platform=wasi/wasm \
  api-wasm

ベストプラクティス

実践メモ: Wasmコンテナを本番運用する際は、以下のベストプラクティスに従うことで安定した環境を構築できます。

  1. 言語選び: Rust/TinyGoがもっとも成熟。C/C++もOK
  2. scratchベースイメージ: Wasmは静的リンクなのでベース不要
  3. マウントは最小限に: WASIのCapabilityモデルでパスを絞る
  4. CIでWasmビルドを固定: rustup target addを自動化
  5. OCIイメージサイズ監視: 小さなバイナリを維持するよう意識

よくある落とし穴とトラブルシューティング

注意: Wasmコンテナは従来のLinuxコンテナとは異なる制約があります。以下のよくある落とし穴に注意してください。

1. ネットワークが使えない

WASIp1ではTCPソケットが限定的でした。WASIp2またはWasmEdgeの拡張を使う必要があります。

2. target=wasi/wasmでビルドできない

Docker Desktopでcontainerd image storeを有効化していないとWasmイメージがロードされません。

3. 既存のLinuxバイナリは動かない

Wasmはあくまで独立した命令セット。ELFバイナリとの互換性はありません。ソースからWasm向けに再コンパイルが必要です。

4. ファイルアクセスが失敗する

WASIはプリオープンされたディレクトリのみアクセス可能です。-vで明示的にマウントしたパス以外は開けません。

5. スレッド・プロセスの制限

WASIp1ではマルチスレッドが未対応。Tokio等はcurrent_threadフレーバーを使う必要があります。

導入手順

# 1. Docker Desktop 4.15+ をインストール
# 2. Settings > Features in development でcontainerd image storeとWasmを有効化

# 3. 動作確認
docker run --rm \
  --runtime=io.containerd.wasmedge.v1 \
  --platform=wasi/wasm \
  michaelirwin244/wasm-example

# 4. 自身のプロジェクトをビルド
cargo build --target wasm32-wasip1 --release
docker buildx build --platform wasi/wasm -t my-wasm .

パフォーマンスとベンチマーク

参考値(小規模HTTP APIの例、環境により変動):

指標Alpine + RustコンテナWasm + WasmEdge
イメージサイズ約15 MB約2 MB
起動時間200〜400ms5〜30ms
常駐メモリ20〜40 MB3〜8 MB
HTTP RPS基準70〜90%程度

起動速度とメモリ効率ではWasmが優位、絶対スループットでは現状ネイティブがやや優れる傾向があります。WASIp2の成熟とAOTコンパイルの進化により差は縮まっています。

制限事項

現時点での制限:

  • ネットワーク機能は限定的(WASIp2で改善中)
  • マルチスレッド・シグナル・forkは限定的
  • GPU/ハードウェア直接アクセスはランタイム依存
  • 既存のLinuxバイナリは直接実行不可

FAQ

Q1. 既存のDockerイメージをそのままWasmに変換できますか? A. できません。Wasmは独立した命令セットのため、ソースコードからWasm向けに再コンパイルする必要があります。

Q2. どの言語が一番書きやすいですか? A. Rustがもっとも成熟しています。TinyGoもGoプログラマにおすすめです。C/C++はEmscripten経由で可能です。

Q3. 本番運用に使えますか? A. エッジ・サーバーレス領域ではすでに実績があります(Fastly Compute、Fermyon Cloudなど)。汎用サーバー用途は引き続き検証段階ですが、着実に成熟しています。

Q4. KubernetesでWasmを動かすには? A. runwasi対応のcontainerdをノードにセットアップし、RuntimeClassを作成してPodから指定します。

Q5. イメージにOSは入っていますか? A. 入っていません。FROM scratchで十分です。Wasmは静的にリンクされた単一ファイルとして実行されます。

まとめ

Docker + WebAssemblyは、コンテナ技術の新しい選択肢を提供します。軽量で高速起動が必要なワークロード、特にエッジやサーバーレス環境での活用が期待されます。Linuxコンテナを置き換えるのではなく、両者を適材適所で使い分けるのが現実的なアプローチです。今後、WASIp2の成熟に伴い、より多くのユースケースに対応していくでしょう。

実践: Spinフレームワークでサーバーレス関数

FermyonのSpinはWasm向けのサーバーレスランタイムで、DockerイメージとしてもパッケージングできるためKubernetes統合がしやすいのが特徴です。

// src/lib.rs
use spin_sdk::http::{IntoResponse, Request, Response};
use spin_sdk::http_component;

#[http_component]
fn hello(req: Request) -> anyhow::Result<impl IntoResponse> {
    let name = req.query().get("name").unwrap_or("World");
    Ok(Response::builder()
        .status(200)
        .header("content-type", "text/plain")
        .body(format!("Hello, {}!", name))
        .build())
}
# spin.toml
spin_manifest_version = 2

[application]
name = "hello-spin"
version = "0.1.0"

[[trigger.http]]
route = "/..."
component = "hello"

[component.hello]
source = "target/wasm32-wasip1/release/hello.wasm"
[component.hello.build]
command = "cargo build --target wasm32-wasip1 --release"
spin build
spin up

コンポーネントモデル(WIT)

WASIp2で導入されたコンポーネントモデルは、Wasmモジュール同士を型付きのインターフェースで接続する仕組みです。WIT(Wasm Interface Type)でAPIを定義し、異なる言語のモジュールを組み合わせられます。

// wit/calc.wit
package example:calc@0.1.0;

interface arithmetic {
  add: func(a: s32, b: s32) -> s32;
  sub: func(a: s32, b: s32) -> s32;
}

world calc {
  export arithmetic;
}
// Rust実装
wit_bindgen::generate!({ world: "calc" });

struct Component;

impl Guest for Component {
    fn add(a: i32, b: i32) -> i32 { a + b }
    fn sub(a: i32, b: i32) -> i32 { a - b }
}

export!(Component);

この仕組みにより、Rustで書いたコンポーネントを、JavaScriptやPythonのWasmランタイムから型安全に呼び出せるようになります。

マルチステージビルドのベストプラクティス

# syntax=docker/dockerfile:1
FROM rust:1.80 AS builder
WORKDIR /app

# 依存関係キャッシュ層
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo 'fn main(){}' > src/main.rs \
 && rustup target add wasm32-wasip1 \
 && cargo build --target wasm32-wasip1 --release \
 && rm -rf src

# 実ソースビルド
COPY src ./src
RUN touch src/main.rs && cargo build --target wasm32-wasip1 --release \
 && wasm-strip target/wasm32-wasip1/release/api.wasm || true

FROM scratch
COPY --from=builder /app/target/wasm32-wasip1/release/api.wasm /api.wasm
ENTRYPOINT ["/api.wasm"]

イメージ最適化のポイント

  • wasm-opt -Ozで最適化(binaryenツールキット)
  • wasm-stripでデバッグシンボル除去
  • cargo build --releaseにLTOを追加
  • panic = "abort"でpanic処理を小さく
# Cargo.toml
[profile.release]
opt-level = "z"
lto = true
codegen-units = 1
panic = "abort"
strip = true

OCI Artifactsとしての配布

Wasmモジュールは通常のOCIコンテナイメージとしてレジストリにPushできます。Docker Hub、GitHub Container Registry、Amazon ECR、Google Artifact Registryなどが利用可能です。

# ビルド → Push
docker buildx build --platform wasi/wasm \
  -t ghcr.io/example/api-wasm:v1.0.0 --push .

# Pull → 実行
docker run --rm \
  --runtime=io.containerd.wasmedge.v1 \
  --platform=wasi/wasm \
  ghcr.io/example/api-wasm:v1.0.0

署名・検証(cosign)

# 署名
cosign sign ghcr.io/example/api-wasm:v1.0.0

# 検証
cosign verify ghcr.io/example/api-wasm:v1.0.0 \
  --certificate-identity-regexp='.*' \
  --certificate-oidc-issuer-regexp='.*'

エコシステムの現状

領域採用例
CDN EdgeFastly Compute, Cloudflare Workers
サーバーレスFermyon Cloud, Wasm Cloud
データベースSingleStoreのWasm UDF
プラグインEnvoy, Istio, Lapce, Figma
AI推論WasmEdge + TensorFlow Lite
組み込み産業機器、ロボティクス

参考リンク

この技術を体系的に学びたいですか?

未来学では東証プライム上場企業のITエンジニアが24時間サポート。月額24,800円から、退会金0円のオンラインIT塾です。

メールで無料相談する
← 一覧に戻る