Fenil Sonani

Docker+WASM Practical Guide: Running WebAssembly in Containers

1 min read

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

  1. Docker+WASM Architecture
  2. Setting Up Docker with WASM Support
  3. Building Your First WASM Container
  4. Runtime Configuration and Options
  5. Multi-Language WASM Development
  6. Networking and Service Communication
  7. Volume Mounts and File System Access
  8. Production Deployment Strategies
  9. Performance Optimization
  10. 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

  1. Docker CLI receives commands as usual
  2. Docker Engine determines runtime based on image
  3. containerd routes to appropriate runtime
  4. WASM Shim executes WebAssembly modules
  5. 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 --from=builder /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

FeatureWasmtimeWasmEdgeSpin
WASI Preview 1
WASI Preview 2🚧
NetworkingBasicFullHTTP/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 --from=builder /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 --from=builder /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 --from=builder /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 --from=builder /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 --platform=$BUILDPLATFORM 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

  1. Use WASM for compute-intensive, stateless workloads
  2. Choose the right runtime based on your needs
  3. Optimize builds for size and startup time
  4. Leverage volumes for configuration and state
  5. 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! 🚀

Share this content

Reading time: 1 minutes
Progress: 0%
#Docker#Containers#DevOps#Performance#Optimization#Production
Docker+WASM Practical Guide: Running WebAssembly in Containers - Fenil Sonani