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
4 changes: 3 additions & 1 deletion packages/_example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@
"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",
"start:with-executor:multiple-instance": "concurrently --kill-others --names 'agent,executors' \"yarn start\" \"bash scripts/start-docker-executors.sh executors\"",
"start:with-executor:multiple-instance:build": "concurrently --kill-others --names 'agent,executors' \"yarn start\" \"bash scripts/start-docker-executors.sh executors:build\""
},
"devDependencies": {
"@types/node": "^20.12.12",
Expand Down
28 changes: 28 additions & 0 deletions packages/_example/scripts/start-docker-executors.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# Starts the multi-instance docker executors (nginx gateway on :3400), configured ENTIRELY from
# packages/_example/.env. Invoked by the start:with-executor:multiple-instance[:build] scripts.
#
# The executor example's own `executors` script stays config-agnostic (pure `docker compose up`):
# this wrapper sources _example/.env, translates host-local URLs so the containers can reach the
# host, waits for the agent, then delegates. Run standalone from the executor example dir to use
# that package's own .env instead.
set -euo pipefail
set -a
# shellcheck disable=SC1091
source .env

# localhost / 127.0.0.1 -> host.docker.internal (containers reach services on the host).
to_host() {
local v="${1/localhost/host.docker.internal}"
echo "${v/127.0.0.1/host.docker.internal}"
}

AGENT_URL="$(to_host "$EXECUTOR_AGENT_URL")"
DATABASE_URL="$(to_host "$EXECUTOR_DATABASE_URL")"
FOREST_SERVER_URL="$(to_host "${FOREST_SERVER_URL:-}")"

# Executors probe AGENT_URL on startup — wait for the host agent first (avoids restart noise).
until curl -s "$EXECUTOR_AGENT_URL" >/dev/null 2>&1; do sleep 1; done

# Exported vars above are inherited by docker compose interpolation in the example package.
yarn workspace workflow-executor-example "${1:-executors}"
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
67 changes: 67 additions & 0 deletions packages/workflow-executor/example/docker-compose.executors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# 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.
#
# Config comes entirely from packages/_example/.env — there is nothing to edit here.
# Run it via the example package script, which sources that .env, remaps
# EXECUTOR_AGENT_URL/EXECUTOR_DATABASE_URL -> AGENT_URL/DATABASE_URL and rewrites
# localhost -> host.docker.internal so the containers can reach your host:
#
# yarn workspace workflow-executor-example executors[:build]
# # or, with the agent, from packages/_example:
# yarn start:with-executor:multiple-instance[: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).

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"
# Forest local-dev domains resolve to 127.0.0.1 via the host's /etc/hosts (the orchestrator
# runs on the host). Inside containers 127.0.0.1 is the container itself, so map them to the
# host. Inert for non-dev setups (prod api.forestadmin.com is unaffected).
- "api.development.forestadmin.com:host-gateway"
- "app.development.forestadmin.com:host-gateway"
- "static.development.forestadmin.com:host-gateway"

services:
executor-1:
<<: *executor-common
build:
# Repo root (relative to this compose file): the Dockerfile does `COPY . .` over the
# whole monorepo, so the build context must be the root, not packages/.
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;
}
}
4 changes: 3 additions & 1 deletion packages/workflow-executor/example/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
"db:up": "docker compose up -d",
"db:down": "docker compose down",
"db:reset": "docker compose down -v && docker compose up -d",
"db:psql": "docker compose exec postgres psql -U executor -d workflow_executor"
"db:psql": "docker compose exec postgres psql -U executor -d workflow_executor",
"executors": "docker compose -f docker-compose.executors.yml up",
"executors:build": "docker compose -f docker-compose.executors.yml up --build"
},
"dependencies": {
"@forestadmin/workflow-executor": "*"
Expand Down
Loading