> Part of the walkerOS documentation. Project overview and full index: <https://www.walkeros.io/llms.txt>

# 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](/preview/pr-720/docs/apps/cli.md) handles build-time. You bundle with the CLI, deploy with Docker.

## What it does[​](#what-it-does "Direct link to 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](#including-files)
* ✅ **Production-ready** - Health checks, non-root user, signal handling

## What it doesn't do[​](#what-it-doesnt-do "Direct link to 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](/preview/pr-720/docs/apps/runner.md) page.

## Build → deploy workflow[​](#build--deploy-workflow "Direct link to 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[​](#prerequisites "Direct link to Prerequisites")

* Docker installed ([Get Docker](https://docs.docker.com/get-docker/))
* (Optional) Pre-built flow bundle from the [CLI](/preview/pr-720/docs/apps/cli.md)

## Configuration[​](#configuration "Direct link to Configuration")

The Docker container is configured via environment variables:

### Server flow[​](#server-flow "Direct link to 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[​](#bundle-input-methods "Direct link to 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](/preview/pr-720/docs/apps/cli.md#run-command).

## Including files[​](#including-files "Direct link to 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[​](#the-include-field "Direct link to 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[​](#volume-mount-for-development "Direct link to 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[​](#directory-separation "Direct link to 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[​](#deployment-workflows "Direct link to Deployment workflows")

### Workflow 1: volume mount (development)[​](#workflow-1-volume-mount-development "Direct link to 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)[​](#workflow-2-custom-docker-image-production "Direct link to 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[​](#workflow-3-multi-stage-build "Direct link to 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[​](#docker-compose "Direct link to Docker Compose")

### Single service[​](#single-service "Direct link to 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[​](#cloud-deployment "Direct link to Cloud deployment")

### Google Cloud Run[​](#google-cloud-run "Direct link to Google Cloud Run")

Cloud Run is ideal for serverless deployment of walkerOS flows.

#### Deploy collection endpoint[​](#deploy-collection-endpoint "Direct link to 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)[​](#aws-ecsfargate "Direct link to 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[​](#kubernetes "Direct link to 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[​](#health-checks "Direct link to 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[​](#monitoring-and-logs "Direct link to Monitoring and logs")

### View Docker logs[​](#view-docker-logs "Direct link to 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[​](#cloud-run-logs "Direct link to 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[​](#structured-logging "Direct link to Structured logging")

For better observability, use structured logging in your destinations:

```
{
  "destinations": {
    "structured-logger": {
      "package": "@walkeros/destination-demo",
      "config": {
        "settings": {
          "format": "json"
        }
      }
    }
  }
}
```

## Security[​](#security "Direct link to 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[​](#environment-secrets "Direct link to 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[​](#performance "Direct link to Performance")

### Startup time[​](#startup-time "Direct link to Startup time")

* **Cold start**: < 1 second (flow already bundled)
* **Warm start**: < 100ms (container reuse)

### Resource usage[​](#resource-usage "Direct link to Resource usage")

Typical resource consumption:

| Metric  | Idle   | Active (1000 req/min) |
| ------- | ------ | --------------------- |
| Memory  | \~50MB | \~100-150MB           |
| CPU     | < 1%   | 5-15%                 |
| Startup | < 1s   | < 1s                  |

### Scaling[​](#scaling "Direct link to 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[​](#troubleshooting "Direct link to Troubleshooting")

### Container won't start[​](#container-wont-start "Direct link to 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[​](#events-not-being-received "Direct link to 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[​](#port-conflicts "Direct link to 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[​](#bundle-not-found "Direct link to 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[​](#next-steps "Direct link to Next steps")

* **[CLI](/preview/pr-720/docs/apps/cli.md)** - Learn how to build flows
* **[Flow Configuration](/preview/pr-720/docs/getting-started/modes/bundled.md)** - Understand flow structure
* **[Sources](/preview/pr-720/docs/sources.md)** - Configure event sources
* **[Destinations](/preview/pr-720/docs/destinations.md)** - Set up analytics destinations
* **[Docker Hub](https://hub.docker.com/r/walkeros/flow)** - Official Docker images
