Skip to main content
Ask your AI

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 .mjs bundles 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 VariableRequiredDefaultDescription
BUNDLENo/app/flow/flow.mjsPath inside the container to the bundle entry, or to a .tar.gz bundle archive
PORTNo8080Server 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:latest

Archive 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:latest

A 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.

Avoid circular includes

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"]: copies shared/ into the output directory
  • "include": ["./credentials"]: copies credentials/ 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 8080

The 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/flow

Directory 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:latest

3. 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-flow

Workflow 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.0

4. 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.0

Workflow 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 8080

Build and run:

docker build -t analytics-collector .
docker run -d -p 8080:8080 analytics-collector

Docker 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: 3

Run it:

docker-compose up -d
docker-compose logs -f

Cloud 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 }
    }
  }
}
Starting with Console

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=8080

4. 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_NAME

5. 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 60s

6. 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.app

7. 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 50

You 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 check

Kubernetes

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: LoadBalancer

Apply:

kubectl apply -f deployment.yaml
kubectl get services walkeros-flow

Health checks

The Docker container provides a /health endpoint:

curl http://localhost:8080/health

Response:

{ "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-flow

Cloud 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.json

Reference 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:

MetricIdleActive (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=100

Troubleshooting

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 use

Events 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-flow

Port conflicts

# Use a different host port
docker run -p 8081:8080 walkeros/flow

# Or find and kill the conflicting process
lsof -ti:8080 | xargs kill

Bundle 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/flow

Next steps

💡 Need implementation support?
elbwalker offers hands-on support: setup review, measurement planning, destination mapping, and live troubleshooting. Book a 2-hour session (€399)