Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
**/node_modules
**/dist
**/coverage
**/.nx
.git
.github
**/.env
**/.env.*
!**/.env.example
**/*.log
**/*.tsbuildinfo
api-reference
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ lerna-debug.log
# Configuration
.env
.env-test
.env.executors
.forestadmin-schema.json

# yarn
Expand Down
3 changes: 2 additions & 1 deletion packages/_example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,8 @@
"start:with-executor:ai-error": "concurrently --kill-others --names 'agent,executor' \"yarn start\" \"bash -c 'set -a && source .env && AGENT_URL=\\$EXECUTOR_AGENT_URL && DATABASE_URL=\\$EXECUTOR_DATABASE_URL && FORCE_AI_ERROR=true && until curl -s \\$EXECUTOR_AGENT_URL >/dev/null 2>&1; do sleep 1; done && tsx watch ../workflow-executor/src/cli.ts --pretty'\"",
"db:executor:up": "cd ../workflow-executor/example && docker compose up -d",
"db:executor:down": "cd ../workflow-executor/example && docker compose down",
"db:executor:reset": "cd ../workflow-executor/example && docker compose down -v && docker compose up -d"
"db:executor:reset": "cd ../workflow-executor/example && docker compose down -v && docker compose up -d",
"executor:docker": "docker compose --env-file ../workflow-executor/example/.env.executors -f ../workflow-executor/example/docker-compose.executors.yml up"
},
"devDependencies": {
"@types/node": "^20.12.12",
Expand Down
37 changes: 37 additions & 0 deletions packages/workflow-executor/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# syntax=docker/dockerfile:1
#
# Local/dev image for @forestadmin/workflow-executor.
# The build context MUST be the monorepo root (yarn workspaces + yarn.lock):
#
# docker build -f packages/workflow-executor/Dockerfile -t forest-workflow-executor:local .
#
# It is NOT optimized for production (ships the full workspace node_modules).

FROM node:22-bookworm-slim AS builder
WORKDIR /app

# --ignore-scripts skips husky, native (node-gyp) builds and binary downloads
# we don't need here: the runtime path is Postgres and `pg` is pure JS. The only
# native dep (sqlite3) is dev-only and unused at runtime.
COPY . .
RUN yarn install --frozen-lockfile --ignore-scripts

# Build the executor and only its workspace dependencies (6 packages), in order.
RUN node_modules/.bin/lerna run build \
--scope @forestadmin/workflow-executor \
--include-dependencies

FROM node:22-bookworm-slim AS runtime
WORKDIR /app
ENV NODE_ENV=production

# Hoisted node_modules symlink into packages/*, so the whole tree must come along
# for the @forestadmin/* workspace symlinks to resolve.
COPY --from=builder /app ./

USER node

# HTTP server (GET /runs/:runId, POST /runs/:runId/trigger). Override with HTTP_PORT.
EXPOSE 3400

CMD ["node", "packages/workflow-executor/dist/cli.js", "--json"]
25 changes: 25 additions & 0 deletions packages/workflow-executor/example/.env.executors.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# Copy to .env.executors and fill in. Used by docker-compose.executors.yml.

# Forest Admin secrets — Settings -> Environments. FOREST_AUTH_SECRET MUST match
# the auth secret of the agent that signs forest_session_token, or every request
# proxied to the executor gets a 401.
FOREST_ENV_SECRET=
FOREST_AUTH_SECRET=

# IMPORTANT: inside the containers, localhost/127.0.0.1 means the container
# itself, NOT your host. Anything on your machine (agent, Forest backend,
# Postgres) must be reached via host.docker.internal with its real port.

# Your existing local Postgres run store. Both executors share it.
DATABASE_URL=postgres://user:password@host.docker.internal:5432/workflow_executor

# Your agent, reachable from inside the containers.
AGENT_URL=http://host.docker.internal:3351

# The Forest orchestrator. Defaults to https://api.forestadmin.com. For a LOCAL
# backend use http(s)://host.docker.internal:<port> (NOT localhost). Use http://
# if it serves plaintext, else you'll hit an SSL "wrong version number" error.
# FOREST_SERVER_URL=http://host.docker.internal:3001

# Optional — default shown.
POLLING_INTERVAL_MS=5000
56 changes: 56 additions & 0 deletions packages/workflow-executor/example/docker-compose.executors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Two workflow-executor instances behind an nginx round-robin gateway, sharing a
# single Postgres run store (so the write-ahead idempotency log is shared across
# instances). The store is YOUR existing local Postgres, reached via DATABASE_URL.
#
# cp .env.executors.example .env.executors # fill in secrets + DATABASE_URL
# docker compose --env-file .env.executors -f docker-compose.executors.yml up --build
#
# Point your agent at the gateway: workflowExecutorUrl: "http://localhost:3400".
# Your agent must be running on the host (executors probe AGENT_URL on startup).
#
# DATABASE_URL must use host.docker.internal (NOT localhost) to reach a Postgres
# running on your host, e.g. postgres://user:pass@host.docker.internal:5432/db

name: workflow-executor-gateway

x-executor-env: &executor-env
FOREST_ENV_SECRET: ${FOREST_ENV_SECRET}
FOREST_AUTH_SECRET: ${FOREST_AUTH_SECRET}
AGENT_URL: ${AGENT_URL:-http://host.docker.internal:3351}
FOREST_SERVER_URL: ${FOREST_SERVER_URL:-https://api.forestadmin.com}
DATABASE_URL: ${DATABASE_URL}
POLLING_INTERVAL_MS: ${POLLING_INTERVAL_MS:-5000}
HTTP_PORT: "3400"
NODE_TLS_REJECT_UNAUTHORIZED: 0

x-executor-common: &executor-common
image: forest-workflow-executor:local
restart: unless-stopped
extra_hosts:
- "host.docker.internal:host-gateway"

services:
executor-1:
<<: *executor-common
build:
context: ../..
dockerfile: packages/workflow-executor/Dockerfile
environment: *executor-env

executor-2:
<<: *executor-common
environment: *executor-env
depends_on:
executor-1:
condition: service_started

gateway:
image: nginx:1.27-alpine
restart: unless-stopped
ports:
- "3400:3400"
volumes:
- ./nginx-executor-gateway.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- executor-1
- executor-2
26 changes: 26 additions & 0 deletions packages/workflow-executor/example/nginx-executor-gateway.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# Round-robin gateway in front of the two workflow-executor instances.
# Stateless: trigger re-fetches the run server-side and getRun reads the shared
# DB, so any instance serves any request — no sticky sessions needed.

upstream executors {
server executor-1:3400;
server executor-2:3400;
}

server {
listen 3400;

location / {
proxy_pass http://executors;

proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
# Authorization / Cookie are forwarded by default; AI/MCP steps can be slow.
proxy_read_timeout 600s;
proxy_send_timeout 600s;

# Retry the other instance on connection/5xx failure.
proxy_next_upstream error timeout http_502 http_503 http_504;
}
}
Loading