walkerOS Docker
The walkerOS Docker image (walkeros/flow) is a pure runtime container for executing pre-built flow bundles in production. It's designed for fast startup (< 1 second), minimal footprint (~150-200MB), and cloud-native deployment.
Key philosophy: Docker handles runtime, CLI handles build-time. You bundle with the CLI, deploy with Docker.
What it does
- ✅ Executes pre-built
.mjsbundles from the CLI - ✅ Runs collection servers (HTTP endpoints for event ingestion)
- ✅ Serves static files (web tracking scripts)
- ✅ Sub-1 second startup - No npm downloads, no build steps
- ✅ Production-ready - Health checks, non-root user, signal handling
What it doesn't do
- ❌ No bundling - Flows must be pre-built with the CLI
- ❌ No package downloads - All dependencies must be bundled
- ❌ No configuration generation - Configuration comes from your bundle
Think of it as the deployment target, not the build tool.
Build → deploy workflow
┌─────────────────────────────────────────────────────────────────┐
│ DEVELOPMENT │
├─────────────────────────────────────────────────────────────────┤
│ │
│ flow.json ──► walkeros bundle ──► flow.mjs │
│ │ (CLI) │ │
│ │ │ │
│ Config as code Pre-built bundle │
│ (sources, destinations) (all deps included) │
│ │
└────────────────────────────────┬────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ PRODUCTION │
├─────────────────────────────────────────────────────────────────┤
│ │
│ flow.mjs ──► Docker Container ──► HTTP Server │
│ │ (runtime only) │ │
│ │ │ │
│ Mount or bake in < 1 second startup │
│ /health, /collect │
│ │
└─────────────────────────────────────────────────────────────────┘
This separation means:
- CLI handles complexity (npm resolution, bundling, TypeScript)
- Docker stays simple (just runs JavaScript)
- Startup is instant (no npm install, no build step)
Prerequisites
- Docker installed (Get Docker)
- (Optional) Pre-built flow bundle from the CLI
Quick start with demos
The Docker image includes demo bundles for instant testing.
Demo: event collection
docker run -p 8080:8080 \
-e FLOW=/app/demos/demo-collect.mjs \
walkeros/flow:latestOutput:
✓ Loading flow: /app/demos/demo-collect.mjs
✓ Starting collection server on port 8080
✓ Health check available at /health
[Demo Output] page view
[Demo Output] product add
The demo is self-contained and generates test events automatically.
Demo: static file serving
docker run -p 3000:3000 \
-e PORT=3000 \
-e STATIC_DIR=/app/demos \
walkeros/flow:latestVisit http://localhost:3000/demo-serve.js to download the demo web bundle.
Configuration
The Docker container is configured via environment variables:
Server flow
docker run -p 8080:8080 \
-v $(pwd)/flow.mjs:/app/flow.mjs \
-e FLOW=/app/flow.mjs \
-e PORT=8080 \
-e HOST=0.0.0.0 \
walkeros/flow:latest| Environment Variable | Required | Default | Description |
|---|---|---|---|
BUNDLE | Yes | - | Path, URL, or piped content of a pre-built .mjs bundle |
PORT | No | 8080 | Server port |
HOST | No | 0.0.0.0 | Bind address |
Web flow
docker run -p 3000:3000 \
-v $(pwd)/dist:/app/static \
-e STATIC_DIR=/app/static \
-e PORT=3000 \
walkeros/flow:latest| Environment Variable | Required | Default | Description |
|---|---|---|---|
STATIC_DIR | No | /app/dist | Directory to serve |
PORT | No | 3000 | Server port |
HOST | No | 0.0.0.0 | Bind address |
Bundle input methods
The runtime accepts bundles from three sources (checked in priority order):
File path (default): mount or bake the bundle into the container:
docker run -p 8080:8080 \
-v $(pwd)/flow.mjs:/app/flow/bundle.mjs \
walkeros/flow:latestURL: fetch the bundle from a remote location at startup:
docker run -p 8080:8080 \
-e BUNDLE=https://cdn.example.com/flow.mjs \
walkeros/flow:latestWorks with presigned S3/GCS URLs, CDNs, or any HTTP(S) endpoint.
Stdin pipe: pipe the bundle directly into the container:
cat flow.mjs | docker run -i -p 8080:8080 walkeros/flow:latestStdin takes highest priority. If data is piped, the BUNDLE env var is ignored.
Including files
The include field in your flow config copies folders into the bundle output directory at build time. When combined with --dockerfile, these folders are automatically added as COPY instructions in the generated Dockerfile.
The include field
{
"version": 4,
"include": ["./public"],
"flows": {
"default": {
"config": { "platform": "server" },
"stores": {
"files": {
"package": "@walkeros/server-store-fs",
"config": { "settings": { "basePath": "./public" } }
}
},
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"next": [
{
"match": { "key": "ingest.method", "operator": "eq", "value": "GET" },
"next": "file"
}
]
}
},
"transformers": {
"file": {
"package": "@walkeros/server-transformer-file",
"config": { "settings": { "prefix": "/static" } },
"env": { "store": "$store.files" }
}
}
}
}
}The include field copies folders into the output directory (where the bundle is written). If your output is dist/bundle.mjs and you set include: ["./dist"], the bundler will try to copy dist/ into dist/dist/, a circular operation that will error.
Fix: Only include folders that are not the output directory:
"include": ["./shared"]: copiesshared/into the output directory"include": ["./credentials"]: copiescredentials/into the output directory
Build with Dockerfile generation:
walkeros bundle flow.json -o dist/ --dockerfileThe generated dist/Dockerfile includes COPY lines for both the bundle and the included folder:
FROM walkeros/flow:latest
COPY bundle.mjs /app/flow/bundle.mjs
COPY public/ /app/flow/public/
ENV BUNDLE=/app/flow/bundle.mjs
EXPOSE 8080Volume mount for development
During development, mount folders directly instead of baking them into the image:
docker run -v ./public:/app/flow/public -p 8080:8080 walkeros/flowDirectory separation
Keep served files and credentials in separate directories:
public/: Files served to clients (static assets, templates)credentials/: Secret files (API keys, service accounts)
The fs store only serves files under its configured basePath. Never point it at a directory containing secrets.
Deployment workflows
Workflow 1: volume mount (development)
Mount pre-built bundles directly into the container.
1. Build with CLI
walkeros bundle server-collect.jsonThis creates dist/flow.mjs.
2. Run with Docker
docker run -d \
-p 8080:8080 \
-v $(pwd)/dist/flow.mjs:/app/flow.mjs \
-e FLOW=/app/flow.mjs \
--name walkeros-flow \
walkeros/flow:latest3. Test it
curl -X POST http://localhost:8080/collect \
-H "Content-Type: application/json" \
-d '{"name":"page view","data":{"title":"Home"}}'4. View logs
docker logs -f walkeros-flowWorkflow 2: custom Docker image (production)
Build a custom Docker image with your bundle baked in.
1. Build with CLI and generate Dockerfile
walkeros bundle production.json --dockerfileThis creates dist/bundle.mjs and dist/Dockerfile.
2. Build image
cd dist
docker build -t my-analytics:v1.0.0 .3. Run it
docker run -d -p 8080:8080 my-analytics:v1.0.04. Push to registry
docker tag my-analytics:v1.0.0 gcr.io/my-project/analytics:v1.0.0
docker push gcr.io/my-project/analytics:v1.0.0Workflow 3: multi-stage build
Build and bundle in a single Dockerfile.
# Build stage
FROM node:18-alpine AS builder
WORKDIR /build
RUN npm install -g @walkeros/cli
COPY flow.json .
RUN walkeros bundle flow.json --output flow.mjs
# Runtime stage
FROM walkeros/flow:latest
COPY --from=builder /build/flow.mjs /app/flow.mjs
ENV BUNDLE=/app/flow.mjs
EXPOSE 8080Build and run:
docker build -t analytics-collector .
docker run -d -p 8080:8080 analytics-collectorDocker Compose
Single service
version: '3.8'
services:
collector:
image: walkeros/flow:latest
environment:
FLOW: /app/flow.mjs
PORT: 8080
volumes:
- ./dist/flow.mjs:/app/flow.mjs
ports:
- "8080:8080"
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 3s
retries: 3Run it:
docker-compose up -d
docker-compose logs -fMultiple services (web + server)
version: '3.8'
services:
# Server-side event collection
collector:
image: walkeros/flow:latest
environment:
FLOW: /app/server-flow.mjs
volumes:
- ./dist/server-flow.mjs:/app/server-flow.mjs
ports:
- "8080:8080"
restart: unless-stopped
# Web bundle serving
tracker:
image: walkeros/flow:latest
environment:
STATIC_DIR: /app/static
PORT: 3000
volumes:
- ./dist:/app/static
ports:
- "3000:3000"
restart: unless-stoppedCloud deployment
Google Cloud Run
Cloud Run is ideal for serverless deployment of walkerOS flows.
Deploy collection endpoint
1. Create server flow
Create bigquery-collect.json:
{
"version": 4,
"flows": {
"default": {
"config": {
"platform": "server",
"bundle": {
"packages": {
"@walkeros/collector": { "version": "latest" },
"@walkeros/server-source-express": { "version": "latest" },
"@walkeros/server-destination-gcp": { "version": "latest" },
"@walkeros/destination-demo": { "version": "latest" }
}
}
},
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"config": {
"settings": {
"path": "/collect",
"port": 8080,
"cors": true
}
}
}
},
"destinations": {
"bigquery": {
"package": "@walkeros/server-destination-gcp",
"config": {
"settings": {
"projectId": "my-project",
"datasetId": "analytics",
"tableId": "events"
}
}
},
"console": {
"package": "@walkeros/destination-demo",
"config": {
"settings": { "name": "Logger" }
}
}
},
"collector": { "run": true }
}
}
}The config above includes both BigQuery and console destinations. This is a best practice:
- Console logs help debug issues in Cloud Run logs
- Start with console only, verify events flow correctly
- Then uncomment BigQuery destination for production
To start console-only: remove the bigquery destination block.
2. Bundle the flow
walkeros bundle bigquery-collect.json --output flow.mjs3. Create Dockerfile
FROM walkeros/flow:latest
COPY flow.mjs /app/flow.mjs
ENV BUNDLE=/app/flow.mjs
ENV PORT=80804. Build and push to Google Container Registry
# Set your GCP project
export PROJECT_ID=my-project
export SERVICE_NAME=analytics-collector
export REGION=us-central1
# Build and push
docker build -t gcr.io/$PROJECT_ID/$SERVICE_NAME .
docker push gcr.io/$PROJECT_ID/$SERVICE_NAME5. Deploy to Cloud Run
gcloud run deploy $SERVICE_NAME \
--image gcr.io/$PROJECT_ID/$SERVICE_NAME \
--platform managed \
--region $REGION \
--allow-unauthenticated \
--port 8080 \
--memory 512Mi \
--cpu 1 \
--max-instances 10 \
--timeout 60s6. Get the URL
gcloud run services describe $SERVICE_NAME \
--platform managed \
--region $REGION \
--format 'value(status.url)'
# Output: https://analytics-collector-xxxxx-uc.a.run.app7. Test it
curl -X POST https://analytics-collector-xxxxx-uc.a.run.app/collect \
-H "Content-Type: application/json" \
-d '{
"name": "page view",
"data": {
"title": "Landing Page",
"path": "/landing"
},
"user": {
"id": "user123"
}
}'8. View logs
gcloud run logs read $SERVICE_NAME \
--region $REGION \
--limit 50You should see console logs showing the received events.
Deploy web bundle server
1. Create web flow
Create web-tracker.json:
{
"version": 4,
"flows": {
"default": {
"config": {
"platform": "web",
"bundle": {
"packages": {
"@walkeros/collector": { "version": "latest" },
"@walkeros/web-source-browser": { "version": "latest" },
"@walkeros/web-destination-api": { "version": "latest" }
}
}
},
"sources": {
"browser": {
"package": "@walkeros/web-source-browser",
"config": {
"settings": {
"pageview": true,
"elb": "track"
}
}
}
},
"destinations": {
"api": {
"package": "@walkeros/web-destination-api",
"config": {
"settings": {
"url": "https://analytics-collector-xxxxx-uc.a.run.app/collect",
"method": "POST"
}
}
}
},
"collector": { "run": true }
}
}
}2. Bundle
walkeros bundle web-tracker.jsonThis creates tracker.js.
3. Create Dockerfile for serving
FROM walkeros/flow:latest
COPY tracker.js /app/static/tracker.js
ENV STATIC_DIR=/app/static
ENV PORT=80804. Deploy to Cloud Run
# Build and push
docker build -t gcr.io/$PROJECT_ID/tracker-server .
docker push gcr.io/$PROJECT_ID/tracker-server
# Deploy
gcloud run deploy tracker-server \
--image gcr.io/$PROJECT_ID/tracker-server \
--platform managed \
--region $REGION \
--allow-unauthenticated \
--port 80805. Use in your website
<script src="https://tracker-server-xxxxx-uc.a.run.app/tracker.js"></script>
<script>
// Track custom events
track('button click', { label: 'signup' });
</script>For production, consider:
- Use a CDN: Serve static files from Cloud Storage + Cloud CDN instead of Cloud Run
- Custom domain: Map your own domain (e.g.,
cdn.example.com/tracker.js) - Versioning: Include version in filename (e.g.,
tracker.v1.2.3.js) - Caching: Set appropriate cache headers
AWS (ECS/Fargate)
Similar workflow for AWS:
# Push to ECR
aws ecr get-login-password --region us-east-1 | docker login --username AWS --password-stdin $AWS_ACCOUNT.dkr.ecr.us-east-1.amazonaws.com
docker tag my-analytics-collector:latest $AWS_ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/analytics-collector:latest
docker push $AWS_ACCOUNT.dkr.ecr.us-east-1.amazonaws.com/analytics-collector:latest
# Create task definition and service using ECS console or CLI
# Configure port 8080, environment variables, and health checkKubernetes
Deploy to any Kubernetes cluster:
apiVersion: apps/v1
kind: Deployment
metadata:
name: walkeros-flow
spec:
replicas: 3
selector:
matchLabels:
app: walkeros-flow
template:
metadata:
labels:
app: walkeros-flow
spec:
containers:
- name: collector
image: gcr.io/my-project/analytics-collector:v1.0.0
ports:
- containerPort: 8080
env:
- name: FLOW
value: "/app/flow.mjs"
- name: PORT
value: "8080"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 30
resources:
requests:
memory: "256Mi"
cpu: "250m"
limits:
memory: "512Mi"
cpu: "500m"
---
apiVersion: v1
kind: Service
metadata:
name: walkeros-flow
spec:
selector:
app: walkeros-flow
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancerApply:
kubectl apply -f deployment.yaml
kubectl get services walkeros-flowHealth checks
The Docker container provides a /health endpoint:
curl http://localhost:8080/healthResponse:
{
"status": "ok",
"timestamp": 1701234567890,
"file": "/app/flow.mjs"
}Use this for:
- Docker health checks
- Kubernetes liveness/readiness probes
- Load balancer health checks
- Monitoring systems
Monitoring and logs
View Docker logs
# Follow logs
docker logs -f walkeros-flow
# Last 100 lines
docker logs --tail 100 walkeros-flow
# With timestamps
docker logs -t walkeros-flowCloud Run logs
# Recent logs
gcloud run logs read $SERVICE_NAME --region $REGION --limit 50
# Tail logs
gcloud run logs tail $SERVICE_NAME --region $REGION
# Filter by severity
gcloud run logs read $SERVICE_NAME --region $REGION --log-filter="severity>=ERROR"Structured logging
For better observability, use structured logging in your destinations:
{
"destinations": {
"structured-logger": {
"package": "@walkeros/destination-demo",
"config": {
"settings": {
"format": "json"
}
}
}
}
}Security
The Docker image follows security best practices:
- ✅ Non-root user - Runs as
walker(UID 1001) - ✅ Minimal base - Alpine Linux (~150-200MB)
- ✅ No build tools - Production dependencies only
- ✅ Signal handling - Graceful shutdown with Tini
- ✅ Health checks - Built-in endpoint for orchestrators
Environment secrets
For sensitive configuration (API keys, credentials), use environment variables:
# Docker
docker run -e BIGQUERY_KEY="$(cat key.json)" walkeros/flow
# Cloud Run
gcloud run deploy $SERVICE_NAME \
--set-secrets="BIGQUERY_KEY=bigquery-key:latest"
# Kubernetes
kubectl create secret generic analytics-secrets \
--from-file=bigquery-key=key.jsonReference secrets in your flow configuration using environment variable substitution.
Performance
Startup time
- Cold start: < 1 second (flow already bundled)
- Warm start: < 100ms (container reuse)
Resource usage
Typical resource consumption:
| Metric | Idle | Active (1000 req/min) |
|---|---|---|
| Memory | ~50MB | ~100-150MB |
| CPU | < 1% | 5-15% |
| Startup | < 1s | < 1s |
Scaling
The container is designed for horizontal scaling:
# Docker Swarm
docker service scale walkeros-flow=5
# Kubernetes
kubectl scale deployment walkeros-flow --replicas=5
# Cloud Run (auto-scaling)
gcloud run services update $SERVICE_NAME --max-instances=100Troubleshooting
Container won't start
# Check logs
docker logs walkeros-flow
# Common issues:
# - FLOW path doesn't exist
# - Port already in useEvents not being received
# Verify container is running
docker ps | grep walkeros
# Check health endpoint
curl http://localhost:8080/health
# Test event submission
curl -X POST http://localhost:8080/collect \
-H "Content-Type: application/json" \
-d '{"name":"test event","data":{}}'
# Check logs for errors
docker logs walkeros-flowPort conflicts
# Use a different host port
docker run -p 8081:8080 walkeros/flow
# Or find and kill the conflicting process
lsof -ti:8080 | xargs killBundle not found
# Verify volume mount
docker run -v $(pwd)/dist/flow.mjs:/app/flow.mjs walkeros/flow
# Check if file exists in container
docker run --rm -it walkeros/flow ls -la /app/
# Verify FLOW environment variable
docker run -e FLOW=/app/flow.mjs walkeros/flowNext steps
- CLI - Learn how to build flows
- Flow Configuration - Understand flow structure
- Sources - Configure event sources
- Destinations - Set up analytics destinations
- Docker Hub - Official Docker images