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 files from a store through the file transformer, see Including files
- ✅ Production-ready - Health checks, non-root user, signal handling
What it doesn't do
- ❌ No configuration generation - Configuration comes from your bundle or flow config
This page covers pre-built bundles, which start without npm downloads or a build step. The same image also accepts a flow.json at BUNDLE and bundles it at startup, downloading the packages it names; that mode is described on the Runner page.
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
Configuration
The Docker container is configured via environment variables:
Server flow
docker run -p 8080:8080 \
-v $(pwd)/dist:/app/flow:ro \
-e PORT=8080 \
walkeros/flow:latest| Environment Variable | Required | Default | Description |
|---|---|---|---|
BUNDLE | No | /app/flow/flow.mjs | Path inside the container to the bundle entry, or to a .tar.gz bundle archive |
PORT | No | 8080 | Server port |
A server bundle is a directory: walkeros bundle flow.json -o dist/ writes dist/flow.mjs, dist/package.json, and dist/node_modules/. Step packages stay external and load from that node_modules/, so always ship the whole directory, not flow.mjs alone.
Bundle input methods
The image's entrypoint checks that BUNDLE points at an existing file inside the container and exits with bundle not found otherwise. Two file forms work:
Bundle directory (default): mount or bake the output of walkeros bundle flow.json -o dist/ at /app/flow/:
docker run -p 8080:8080 \
-v $(pwd)/dist:/app/flow:ro \
walkeros/flow:latestArchive file: bundle to an archive (walkeros bundle flow.json -o flow.tar.gz), mount it, and point BUNDLE at it. The runtime extracts it into /app/flow/ at startup, so leave that directory writable:
docker run -p 8080:8080 \
-v $(pwd)/flow.tar.gz:/app/bundle/flow.tar.gz:ro \
-e BUNDLE=/app/bundle/flow.tar.gz \
walkeros/flow:latestA bundle URL or a bundle piped on stdin is not accepted by the image, because the entrypoint's file check runs first. walkeros run outside the image accepts a URL; see the CLI run command.
Including files
The include field in your flow config copies folders into the bundle output directory at build time. Copying that directory into the image brings the included folders along.
The include field
{
"version": 4,
"include": ["./public"],
"flows": {
"default": {
"config": { "platform": "server" },
"stores": {
"files": {
"package": "@walkeros/server-store-fs",
"config": { "settings": { "basePath": "./public" }, "file": true }
}
},
"sources": {
"http": {
"package": "@walkeros/server-source-express",
"config": {
"settings": { "paths": ["/collect", "/static/*path"] },
"ingest": {
"map": {
"method": { "key": "method" },
"path": { "key": "path" }
}
}
},
"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 source only fills ingest.method and ingest.path when config.ingest maps them, so without that block the match never passes and the file transformer never runs. paths must cover the served files: the express source registers only /collect by default, and /static/*path is Express 5 wildcard syntax. file: true makes the fs store return the files byte-exact.
The include field copies folders into the output directory (where the bundle is written). If your output is dist/ 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
Bundle into a directory:
walkeros bundle flow.json -o dist/This writes dist/flow.mjs, dist/package.json, dist/node_modules/, and the included dist/public/. A Dockerfile that copies the whole directory to /app/flow/, where the image expects flow.mjs, needs no BUNDLE setting:
FROM walkeros/flow:latest
COPY dist/ /app/flow/
EXPOSE 8080The runner sets the working directory to the bundle's directory, so "basePath": "./public" resolves to /app/flow/public.
Volume 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.json -o dist/This creates dist/flow.mjs, dist/package.json, and dist/node_modules/.
2. Run with Docker
docker run -d \
-p 8080:8080 \
-v $(pwd)/dist:/app/flow:ro \
--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 add a Dockerfile
walkeros bundle production.json -o dist/This creates dist/flow.mjs, dist/package.json, and dist/node_modules/. Add dist/Dockerfile:
FROM walkeros/flow:latest
COPY . /app/flow/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 -o dist/
# Runtime stage
FROM walkeros/flow:latest
COPY --from=builder /build/dist/ /app/flow/
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:
PORT: 8080
volumes:
- ./dist:/app/flow:ro
ports:
- "8080:8080"
restart: unless-stopped
healthcheck:
test: ["CMD", "node", "-e", "require('http').get('http://localhost:8080/ready', (r) => process.exit(r.statusCode === 200 ? 0 : 1)).on('error', () => process.exit(1))"]
interval: 30s
timeout: 3s
retries: 3Run it:
docker-compose up -d
docker-compose logs -fCloud 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 -o dist/3. Create Dockerfile
FROM walkeros/flow:latest
COPY dist/ /app/flow/
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.
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: 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" }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:
# - bundle not found at $BUNDLE (default /app/flow/flow.mjs)
# - 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:/app/flow:ro walkeros/flow
# Check that flow.mjs and node_modules/ exist in the container
docker run --rm -v $(pwd)/dist:/app/flow:ro --entrypoint ls walkeros/flow -la /app/flow/
# Point BUNDLE at a non-default path
docker run -v $(pwd)/dist:/srv/flow:ro -e BUNDLE=/srv/flow/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