Docker+WASM Practical Guide: Running WebAssembly in Containers
Docker+WASM Practical Guide: Running WebAssembly in Containers
Docker's integration with WebAssembly represents a fundamental shift in how we think about containers. By combining Docker's ecosystem with WASM's lightweight runtime, we get the best of both worlds: familiar tooling with revolutionary performance. This guide provides hands-on implementation details for running WebAssembly modules in Docker containers.
Table of Contents
- Docker+WASM Architecture
- Setting Up Docker with WASM Support
- Building Your First WASM Container
- Runtime Configuration and Options
- Multi-Language WASM Development
- Networking and Service Communication
- Volume Mounts and File System Access
- Production Deployment Strategies
- Performance Optimization
- Troubleshooting and Best Practices
Docker+WASM Architecture
Docker's WebAssembly integration leverages containerd's extensible runtime architecture, allowing WASM modules to run alongside traditional containers.
Architecture Overview
Docker+WASM Stack:
┌─────────────────────────────────────────┐
│ Docker CLI/API │
├─────────────────────────────────────────┤
│ Docker Engine │
├─────────────────────────────────────────┤
│ containerd │
├─────────────────┬───────────────────────┤
│ runC │ WASM Shim │
│ (Containers) │ (WASM Modules) │
├─────────────────┴───────────────────────┤
│ Linux Kernel │
└─────────────────────────────────────────┘
How It Works
- Docker CLI receives commands as usual
- Docker Engine determines runtime based on image
- containerd routes to appropriate runtime
- WASM Shim executes WebAssembly modules
- Kernel provides system resources
Supported WASM Runtimes
# Available runtimes in Docker+WASM
runtimes:
- name: wasmtime
handler: containerd-shim-wasmtime-v1
features: ["wasi", "components"]
- name: wasmedge
handler: containerd-shim-wasmedge-v1
features: ["wasi", "networking", "gpu"]
- name: spin
handler: containerd-shim-spin-v1
features: ["http", "redis", "key-value"]
- name: slight
handler: containerd-shim-slight-v1
features: ["spidermonkey", "messaging"]
Setting Up Docker with WASM Support
Installation Methods
Method 1: Docker Desktop with WASM
# Enable WASM in Docker Desktop
# Settings > Features in development > Enable WASM
# Verify installation
docker run --runtime=io.containerd.wasmtime.v1 \
--platform=wasi/wasm32 \
michaelirwin244/wasm-example
Method 2: Manual containerd Configuration
# Install containerd with WASM support
wget https://github.com/containerd/containerd/releases/download/v1.7.0/containerd-1.7.0-linux-amd64.tar.gz
tar -xvf containerd-1.7.0-linux-amd64.tar.gz -C /usr/local
# Install WASM shims
wget https://github.com/deislabs/containerd-wasm-shims/releases/download/v0.9.0/containerd-wasm-shims-v1-linux-x86_64.tar.gz
tar -xvf containerd-wasm-shims-v1-linux-x86_64.tar.gz -C /usr/local/bin
containerd Configuration
# /etc/containerd/config.toml
version = 2
[plugins]
[plugins."io.containerd.grpc.v1.cri"]
[plugins."io.containerd.grpc.v1.cri".containerd]
default_runtime_name = "runc"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes]
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.runc]
runtime_type = "io.containerd.runc.v2"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmtime]
runtime_type = "io.containerd.wasmtime.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.wasmedge]
runtime_type = "io.containerd.wasmedge.v1"
[plugins."io.containerd.grpc.v1.cri".containerd.runtimes.spin]
runtime_type = "io.containerd.spin.v1"
Docker Daemon Configuration
{
"runtimes": {
"wasmtime": {
"path": "/usr/local/bin/containerd-shim-wasmtime-v1"
},
"wasmedge": {
"path": "/usr/local/bin/containerd-shim-wasmedge-v1"
},
"spin": {
"path": "/usr/local/bin/containerd-shim-spin-v1"
}
}
}
Building Your First WASM Container
Simple WASM Module
1. Create Rust WASM Module
// src/main.rs
use std::env;
use std::fs;
fn main() {
println!("🚀 Hello from WebAssembly in Docker!");
// Environment variables
for (key, value) in env::vars() {
println!(" {} = {}", key, value);
}
// Command line arguments
let args: Vec<String> = env::args().collect();
println!("Arguments: {:?}", args);
// File system test
if let Ok(contents) = fs::read_to_string("/config/app.conf") {
println!("Config: {}", contents);
}
}
2. Build Configuration
# Cargo.toml
[package]
name = "wasm-docker-app"
version = "0.1.0"
edition = "2021"
[dependencies]
wasi = "0.11"
[profile.release]
opt-level = "z"
lto = true
strip = true
3. Build WASM Module
# Add WASI target
rustup target add wasm32-wasi
# Build release version
cargo build --target wasm32-wasi --release
# Optimize with wasm-opt
wasm-opt -Os \
target/wasm32-wasi/release/wasm-docker-app.wasm \
-o wasm-docker-app.wasm
Creating Docker Image
Method 1: FROM scratch
# Dockerfile.wasm
FROM scratch
COPY wasm-docker-app.wasm /app.wasm
ENTRYPOINT ["/app.wasm"]
Method 2: Multi-stage Build
# Dockerfile
FROM rust:1.75 AS builder
WORKDIR /usr/src/app
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN rustup target add wasm32-wasi && \
cargo build --target wasm32-wasi --release
FROM scratch
COPY /usr/src/app/target/wasm32-wasi/release/wasm-docker-app.wasm /app.wasm
ENTRYPOINT ["/app.wasm"]
Building and Running
# Build Docker image
docker build -t my-wasm-app:latest -f Dockerfile.wasm .
# Run with wasmtime runtime
docker run \
--runtime=io.containerd.wasmtime.v1 \
--platform=wasi/wasm32 \
my-wasm-app:latest
# Run with WasmEdge runtime
docker run \
--runtime=io.containerd.wasmedge.v1 \
--platform=wasi/wasm32 \
my-wasm-app:latest
# With environment variables
docker run \
--runtime=io.containerd.wasmtime.v1 \
--platform=wasi/wasm32 \
-e APP_MODE=production \
-e LOG_LEVEL=debug \
my-wasm-app:latest
Runtime Configuration and Options
Wasmtime Configuration
# docker-compose.yml with Wasmtime
version: '3.9'
services:
wasm-service:
image: my-wasm-app:latest
runtime: io.containerd.wasmtime.v1
platform: wasi/wasm32
environment:
- WASMTIME_BACKTRACE_DETAILS=1
- WASMTIME_CACHE_CONFIG=/cache/config.toml
volumes:
- ./cache:/cache
- ./config:/config:ro
deploy:
resources:
limits:
memory: 50M
cpus: '0.5'
WasmEdge Configuration
# WasmEdge with GPU support
services:
ml-inference:
image: wasmedge-tensorflow:latest
runtime: io.containerd.wasmedge.v1
platform: wasi/wasm32
devices:
- /dev/dri:/dev/dri # GPU access
environment:
- WASMEDGE_PLUGIN_PATH=/usr/local/lib/wasmedge
- WASMEDGE_TENSORFLOW_LITE=1
volumes:
- ./models:/models:ro
Spin Configuration
# Spin HTTP application
services:
spin-api:
image: spin-hello-world:latest
runtime: io.containerd.spin.v1
platform: wasi/wasm32
ports:
- "3000:80"
environment:
- SPIN_HTTP_LISTEN_ADDR=0.0.0.0:80
labels:
- "traefik.enable=true"
- "traefik.http.routers.api.rule=Host(`api.example.com`)"
Runtime Feature Comparison
Feature | Wasmtime | WasmEdge | Spin |
---|---|---|---|
WASI Preview 1 | ✅ | ✅ | ✅ |
WASI Preview 2 | ✅ | 🚧 | ❌ |
Networking | Basic | Full | HTTP/Redis |
GPU Support | ❌ | ✅ | ❌ |
Component Model | ✅ | ❌ | ❌ |
AOT Compilation | ✅ | ✅ | ✅ |
Multi-Language WASM Development
Go WASM Container
// main.go
package main
import (
"fmt"
"os"
"net/http"
)
func main() {
fmt.Println("Go WASM server starting...")
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Hello from Go WASM!")
})
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
fmt.Printf("Listening on port %s\n", port)
http.ListenAndServe(":"+port, nil)
}
# Dockerfile.go
FROM golang:1.21 AS builder
WORKDIR /app
COPY go.mod go.sum ./
COPY *.go ./
RUN GOOS=wasip1 GOARCH=wasm go build -o app.wasm
FROM scratch
COPY /app/app.wasm /app.wasm
ENTRYPOINT ["/app.wasm"]
Python WASM Container
# app.py
import os
import sys
def main():
print("Python WASM Container!")
print(f"Python version: {sys.version}")
print(f"Environment: {dict(os.environ)}")
# Simple web server
from http.server import HTTPServer, BaseHTTPRequestHandler
class Handler(BaseHTTPRequestHandler):
def do_GET(self):
self.send_response(200)
self.end_headers()
self.wfile.write(b"Hello from Python WASM!")
server = HTTPServer(('0.0.0.0', 8080), Handler)
server.serve_forever()
if __name__ == "__main__":
main()
# Dockerfile.python
FROM python:3.11 AS builder
RUN pip install pyodide-build
COPY app.py .
RUN pyodide build app.py -o app.wasm
FROM scratch
COPY /app.wasm /app.wasm
ENTRYPOINT ["/app.wasm"]
C/C++ WASM Container
// main.c
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, char* argv[]) {
printf("C WASM Container Running!\n");
// Print arguments
for (int i = 0; i < argc; i++) {
printf("Arg[%d]: %s\n", i, argv[i]);
}
// Environment variables
char* path = getenv("PATH");
if (path) {
printf("PATH: %s\n", path);
}
return 0;
}
# Dockerfile.c
FROM emscripten/emsdk AS builder
COPY main.c .
RUN emcc main.c -o app.wasm \
-s STANDALONE_WASM \
-s EXPORTED_FUNCTIONS="['_main']" \
--no-entry
FROM scratch
COPY /src/app.wasm /app.wasm
ENTRYPOINT ["/app.wasm"]
Networking and Service Communication
HTTP Server in WASM
// HTTP server using wasi-http
use wasi_http::{Request, Response, Server};
fn handle_request(req: Request) -> Response {
match req.path() {
"/" => Response::ok("Welcome to WASM HTTP Server"),
"/health" => Response::ok("healthy"),
"/api/data" => {
let json = r#"{"status": "success", "data": []}"#;
Response::json(json)
},
_ => Response::not_found("Page not found"),
}
}
fn main() {
let server = Server::new("0.0.0.0:8080");
server.run(handle_request);
}
Docker Compose Networking
version: '3.9'
networks:
wasm-net:
driver: bridge
services:
wasm-api:
image: wasm-api:latest
runtime: io.containerd.wasmtime.v1
platform: wasi/wasm32
networks:
- wasm-net
ports:
- "8080:8080"
environment:
- SERVICE_NAME=api
- BACKEND_URL=http://wasm-backend:9090
wasm-backend:
image: wasm-backend:latest
runtime: io.containerd.wasmtime.v1
platform: wasi/wasm32
networks:
- wasm-net
expose:
- "9090"
environment:
- SERVICE_NAME=backend
- DATABASE_URL=postgresql://db:5432/myapp
db:
image: postgres:15
networks:
- wasm-net
environment:
- POSTGRES_DB=myapp
- POSTGRES_PASSWORD=secret
Service Discovery
// Service discovery in WASM
use std::env;
use reqwest;
async fn discover_service(service_name: &str) -> Result<String, Box<dyn std::error::Error>> {
// Docker internal DNS
let url = format!("http://{}:8080", service_name);
// Or use environment variable
let url = env::var(format!("{}_URL", service_name.to_uppercase()))
.unwrap_or(url);
// Health check
let response = reqwest::get(format!("{}/health", url)).await?;
if response.status().is_success() {
Ok(url)
} else {
Err("Service unhealthy".into())
}
}
Volume Mounts and File System Access
WASI File System Access
// File operations in WASM
use std::fs;
use std::io::{Read, Write};
use std::path::Path;
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Read configuration
let config_path = "/config/app.toml";
if Path::new(config_path).exists() {
let config = fs::read_to_string(config_path)?;
println!("Config loaded: {}", config);
}
// Write to data directory
let data_dir = "/data";
fs::create_dir_all(data_dir)?;
let output_file = format!("{}/output.txt", data_dir);
let mut file = fs::File::create(output_file)?;
file.write_all(b"WASM output data\n")?;
// List directory contents
for entry in fs::read_dir("/app")? {
let entry = entry?;
println!("Found: {:?}", entry.path());
}
Ok(())
}
Docker Volume Configuration
# docker-compose with volumes
version: '3.9'
services:
wasm-processor:
image: wasm-processor:latest
runtime: io.containerd.wasmtime.v1
platform: wasi/wasm32
volumes:
# Read-only config
- ./config:/config:ro
# Read-write data
- wasm-data:/data
# Bind mount for development
- ./src:/app/src:ro
# Named pipe for IPC
- /tmp/wasm-pipe:/tmp/pipe
environment:
- CONFIG_PATH=/config/settings.yaml
- DATA_PATH=/data
- WASI_SDK_PATH=/opt/wasi-sdk
volumes:
wasm-data:
driver: local
Persistent Storage Pattern
// State management with volumes
use serde::{Deserialize, Serialize};
use std::fs;
#[derive(Serialize, Deserialize)]
struct AppState {
counter: u64,
last_updated: String,
data: Vec<String>,
}
impl AppState {
fn load() -> Result<Self, Box<dyn std::error::Error>> {
let state_file = "/data/state.json";
if Path::new(state_file).exists() {
let contents = fs::read_to_string(state_file)?;
Ok(serde_json::from_str(&contents)?)
} else {
Ok(Self::default())
}
}
fn save(&self) -> Result<(), Box<dyn std::error::Error>> {
let state_file = "/data/state.json";
let json = serde_json::to_string_pretty(self)?;
fs::write(state_file, json)?;
Ok(())
}
}
Production Deployment Strategies
Multi-Stage Deployment
# Production WASM deployment
apiVersion: apps/v1
kind: Deployment
metadata:
name: wasm-api
labels:
app: wasm-api
spec:
replicas: 100
selector:
matchLabels:
app: wasm-api
template:
metadata:
labels:
app: wasm-api
spec:
runtimeClassName: wasmtime
containers:
- name: wasm
image: registry.example.com/wasm-api:v1.2.3
ports:
- containerPort: 8080
resources:
requests:
memory: "10Mi"
cpu: "50m"
limits:
memory: "50Mi"
cpu: "200m"
env:
- name: RUST_LOG
value: "info"
- name: SERVICE_NAME
valueFrom:
fieldRef:
fieldPath: metadata.name
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 3
periodSeconds: 5
CI/CD Pipeline
# .gitlab-ci.yml for WASM containers
stages:
- build
- test
- deploy
variables:
WASM_IMAGE: $CI_REGISTRY_IMAGE/wasm
build-wasm:
stage: build
image: rust:1.75
script:
- rustup target add wasm32-wasi
- cargo build --target wasm32-wasi --release
- |
cat > Dockerfile.wasm << EOF
FROM scratch
COPY target/wasm32-wasi/release/app.wasm /app.wasm
ENTRYPOINT ["/app.wasm"]
EOF
- docker build -f Dockerfile.wasm -t $WASM_IMAGE:$CI_COMMIT_SHA .
- docker push $WASM_IMAGE:$CI_COMMIT_SHA
test-wasm:
stage: test
script:
- |
docker run \
--runtime=io.containerd.wasmtime.v1 \
--platform=wasi/wasm32 \
$WASM_IMAGE:$CI_COMMIT_SHA \
--test
deploy-wasm:
stage: deploy
script:
- kubectl set image deployment/wasm-api wasm=$WASM_IMAGE:$CI_COMMIT_SHA
- kubectl rollout status deployment/wasm-api
Blue-Green Deployment
#!/bin/bash
# Blue-green deployment for WASM
NEW_VERSION=$1
NAMESPACE="production"
# Deploy green version
kubectl apply -f - <<EOF
apiVersion: apps/v1
kind: Deployment
metadata:
name: wasm-api-green
namespace: $NAMESPACE
spec:
replicas: 50
selector:
matchLabels:
app: wasm-api
version: green
template:
metadata:
labels:
app: wasm-api
version: green
spec:
runtimeClassName: wasmtime
containers:
- name: wasm
image: registry.example.com/wasm-api:$NEW_VERSION
ports:
- containerPort: 8080
EOF
# Wait for green deployment
kubectl wait --for=condition=available \
--timeout=300s \
deployment/wasm-api-green \
-n $NAMESPACE
# Switch traffic
kubectl patch service wasm-api \
-n $NAMESPACE \
-p '{"spec":{"selector":{"version":"green"}}}'
# Delete blue deployment
kubectl delete deployment wasm-api-blue -n $NAMESPACE
# Rename green to blue
kubectl patch deployment wasm-api-green \
-n $NAMESPACE \
--type='json' \
-p='[{"op": "replace", "path": "/metadata/name", "value":"wasm-api-blue"}]'
Performance Optimization
Build Optimization
# Cargo.toml optimization
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Link-time optimization
codegen-units = 1 # Single codegen unit
strip = true # Strip symbols
panic = "abort" # Smaller panic handler
[profile.release.package."*"]
opt-level = "z" # Optimize all dependencies
Runtime Optimization
# Optimized Dockerfile
FROM rust:1.75-alpine AS builder
RUN apk add --no-cache musl-dev
WORKDIR /app
COPY Cargo.toml Cargo.lock ./
RUN mkdir src && echo "fn main() {}" > src/main.rs
RUN cargo build --target wasm32-wasi --release
COPY src ./src
RUN touch src/main.rs && \
cargo build --target wasm32-wasi --release && \
wasm-opt -Os target/wasm32-wasi/release/app.wasm -o app.wasm
FROM scratch
COPY /app/app.wasm /
ENTRYPOINT ["/app.wasm"]
Memory Optimization
// Memory-efficient WASM
#![no_std]
#![no_main]
extern crate alloc;
use alloc::vec::Vec;
use wee_alloc::WeeAlloc;
// Use smaller allocator
#[global_allocator]
static ALLOC: WeeAlloc = WeeAlloc::INIT;
#[no_mangle]
pub extern "C" fn _start() {
main();
}
fn main() {
// Pre-allocate collections
let mut data = Vec::with_capacity(1000);
// Reuse buffers
let mut buffer = [0u8; 4096];
// Aggressive memory cleanup
drop(data);
}
#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
core::arch::wasm32::unreachable()
}
Startup Optimization
// Lazy initialization
use once_cell::sync::Lazy;
static CONFIG: Lazy<Config> = Lazy::new(|| {
Config::from_env()
});
static CONNECTION_POOL: Lazy<Pool> = Lazy::new(|| {
Pool::new(10)
});
fn main() {
// Fast startup - don't initialize until needed
println!("WASM container started in < 1ms");
// Initialize on first request
serve_http(|req| {
Lazy::force(&CONFIG);
Lazy::force(&CONNECTION_POOL);
handle_request(req)
});
}
Troubleshooting and Best Practices
Common Issues and Solutions
1. Module Not Found
# Error: "module not found"
# Solution: Ensure correct platform
docker run \
--runtime=io.containerd.wasmtime.v1 \
--platform=wasi/wasm32 \
my-wasm-app
# Verify image architecture
docker inspect my-wasm-app | grep Architecture
2. Permission Denied
// Error: "permission denied"
// Solution: Use proper WASI capabilities
fn main() {
// Check available permissions
if std::env::var("WASI_SDK_PATH").is_err() {
eprintln!("Warning: Limited WASI permissions");
}
}
3. Memory Limits
# Error: "out of memory"
# Solution: Adjust memory limits
services:
wasm-app:
deploy:
resources:
limits:
memory: 100M # Increase as needed
Best Practices
1. Image Building
# Multi-architecture support
FROM rust:1.75 AS builder
ARG TARGETPLATFORM
RUN case "$TARGETPLATFORM" in \
"wasi/wasm32") target="wasm32-wasi" ;; \
"linux/amd64") target="x86_64-unknown-linux-musl" ;; \
esac && \
rustup target add $target && \
cargo build --target $target --release
2. Security
# Security best practices
services:
wasm-secure:
image: wasm-app:latest
runtime: io.containerd.wasmtime.v1
platform: wasi/wasm32
security_opt:
- no-new-privileges:true
read_only: true
user: "1000:1000"
cap_drop:
- ALL
3. Logging and Monitoring
// Structured logging
use serde_json::json;
fn log_event(level: &str, message: &str, data: serde_json::Value) {
let log = json!({
"timestamp": std::time::SystemTime::now(),
"level": level,
"message": message,
"data": data,
"runtime": "wasm",
"version": env!("CARGO_PKG_VERSION"),
});
println!("{}", log);
}
Development Workflow
# Development script
#!/bin/bash
# Hot reload for WASM development
while true; do
clear
echo "Building WASM module..."
cargo build --target wasm32-wasi
echo "Running in Docker..."
docker run --rm \
--runtime=io.containerd.wasmtime.v1 \
--platform=wasi/wasm32 \
-v $(pwd)/target/wasm32-wasi/debug:/app \
scratch /app/myapp.wasm
inotifywait -e modify src/*.rs
done
Conclusion
Docker+WASM integration represents a paradigm shift in container technology. By combining Docker's mature ecosystem with WebAssembly's performance and security benefits, we can build applications that are:
- ✅ 10-100x smaller than traditional containers
- ✅ Startup in milliseconds instead of seconds
- ✅ Truly portable across architectures
- ✅ Secure by default with sandboxed execution
- ✅ Resource efficient for massive scale
Key Takeaways
- Use WASM for compute-intensive, stateless workloads
- Choose the right runtime based on your needs
- Optimize builds for size and startup time
- Leverage volumes for configuration and state
- Monitor performance to validate benefits
Next Steps
- Experiment with different WASM runtimes
- Migrate suitable workloads to WASM
- Explore hybrid container/WASM architectures
- Contribute to the growing ecosystem
Ready to explore WASM performance benefits? Check out our next article on benchmarking WASM vs traditional containers!
Resources
The future of containerization is here, and it's smaller, faster, and more secure than ever! 🚀