この記事の要点
• 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はこうした流れを受けて、containerdのrunwasiプロジェクトを通じて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@Edge、FermyonのSpinなどが採用
プラグインシステム
- ホストアプリにサンドボックス化されたプラグインを組み込める
- 言語非依存のインターフェース(WITインターフェース)
- Envoy Proxy、Istio、Lapce、Figma等で実績
CI/CDのジョブランナー
- Pull毎の軽量サンドボックスとして活用
- ネットワーク・ファイルアクセスを明示的に許可する設計がセキュア
サポートされるランタイム
| ランタイム | 特徴 | 代表的な用途 |
|---|---|---|
| WasmEdge | 高性能、AI/ML推論対応、TensorFlow統合 | Edge、AI、サーバーレス |
| Wasmtime | Bytecode Alliance、標準準拠 | 汎用、研究開発 |
| Spin | Fermyon製、HTTPハンドラに特化 | サーバーレス関数 |
| wasmer | マルチランタイム、幅広い言語SDK | CLI、組み込み |
# ランタイムの確認
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 /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コンテナを本番運用する際は、以下のベストプラクティスに従うことで安定した環境を構築できます。
- 言語選び: Rust/TinyGoがもっとも成熟。C/C++もOK
scratchベースイメージ: Wasmは静的リンクなのでベース不要- マウントは最小限に: WASIのCapabilityモデルでパスを絞る
- CIでWasmビルドを固定:
rustup target addを自動化 - 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〜400ms | 5〜30ms |
| 常駐メモリ | 20〜40 MB | 3〜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 /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 Edge | Fastly Compute, Cloudflare Workers |
| サーバーレス | Fermyon Cloud, Wasm Cloud |
| データベース | SingleStoreのWasm UDF |
| プラグイン | Envoy, Istio, Lapce, Figma |
| AI推論 | WasmEdge + TensorFlow Lite |
| 組み込み | 産業機器、ロボティクス |