- CLI Reference
- Commands
- ironflow serve
ironflow serve
Start the Ironflow server with embedded NATS JetStream, database, and web dashboard.
ironflow serve [flags]Flags:
| Flag | Short | Type | Default | Description |
|---|---|---|---|---|
--config | -f | string | Path to ironflow.yaml configuration file | |
--port | -p | int | 9123 | HTTP server port. Use 0 to let the OS assign a free port (pair with --port-file so a supervisor can discover it). |
--host | string | HTTP bind host. Empty (default) binds all interfaces; pass 127.0.0.1 for loopback-only (desktop/dev must not LAN-expose the engine). | ||
--metrics-port | int | 0 | Serve /metrics on this separate port. 0 keeps it on the API port. See Separate metrics port. | |
--port-file | string | Path to write the bound HTTP port as JSON {"http_port":N} (mode 0600). Pair with --port 0 so a supervising parent can discover the OS-assigned port. | ||
--db | -d | string | .ironflow/ironflow.db | SQLite database path |
--open | -o | bool | false | Open dashboard in browser after startup |
--headless | bool | false | Serve no dashboard. Every non-API path returns a plain 404. Same as spec.headless: true in YAML. Refused with --open. See Headless Mode. | |
--nats-store-dir | string | (auto-derived from --db) | Embedded NATS JetStream storage directory. Default: {dirname(--db)}/ironflow-nats. Pass empty (--nats-store-dir="") for memory mode opt-out. With Postgres or :memory: SQLite, must be set explicitly or ironflow serve fails fast. (Or set NATS_STORE_DIR; explicit flag wins over env var.) | |
--nats-sync-interval | string | always | Embedded JetStream fsync policy. always fsyncs every write before NATS acknowledges it. A Go duration such as 1s fsyncs on that interval: faster, but a power loss or OS crash can lose writes that were already acknowledged. Ignored in memory mode and with --nats-url, where the NATS server’s own sync_interval applies. YAML: spec.nats.syncInterval. (Or set NATS_SYNC_INTERVAL; explicit flag wins over env var.) See ADR 0134. | |
--nats-url | string | (none) | External NATS URL — nats://host:4222, tls://host:4222, or comma-separated seeds (or set NATS_URL). Requires PostgreSQL (IRONFLOW_DATABASE_URL), an s3:// blob URL (IRONFLOW_BLOB_URL) and NATS 2.14.7 or later on every server. serve refuses to start without the blob URL or against an older server. On a NATS 2.15 or later server, set default_max_consumers: -1 in its jetstream { limits { } } block; see External NATS. | |
--nats-creds | string | (none) | Path to NATS .creds file for JWT/NKey authentication (or set NATS_CREDS_FILE). | |
--nats-port | int | 4222 | Embedded NATS bind port. Use -1 for an ephemeral/random port so multiple ironflow serve instances don’t collide on the NATS socket. This only de-conflicts the NATS port — to run instances side-by-side also give each a distinct --db and --nats-store-dir (they otherwise share .ironflow/ironflow.db and .ironflow/ironflow-nats). CLI-only (no YAML equivalent). Ignored when --nats-url is set (external NATS). | |
--node-id | string | (random UUID) | Stable node identifier for cluster coordination (or set IRONFLOW_NODE_ID). | |
--dev | bool | false | Enable dev mode: bypasses all authentication (API + dashboard) | |
--pprof | bool | false | Enable pprof debug endpoints on a separate loopback listener at 127.0.0.1:6060 | |
--bootstrap-key-file | string | (auto-derived) | Path to write the first-boot admin API key (mode 0400). Default: <db-dir>/.ironflow_bootstrap_key.json under SQLite, required explicitly under PostgreSQL, via this flag or spec.auth.bootstrapKeyFile. Production meta-cluster systemd sets /run/ironflow/bootstrap-key.json. Not written when IRONFLOW_BOOTSTRAP_ADMIN_KEY seeds the key. | |
--jwt-secret-file | string | Existing private raw hex JWT key file; rotation persists here. Conflicts with inline jwtSecret (YAML: spec.auth.jwtSecretFile). Env: IRONFLOW_JWT_SECRET_FILE (flags-only). A missing or untrusted file refuses startup. | ||
--discovery-file | string | (auto-derived) | Path to write the engine discovery file {"version":1,"url","api_key","environment"} (mode 0600) after the server listens. SDKs and the CLI read it when IRONFLOW_SERVER_URL and IRONFLOW_API_KEY are unset. Default: <db-dir>/engine.json under SQLite; nothing under PostgreSQL. Nothing is written when --host is a non-loopback address. api_key is the bootstrap key file’s key, omitted under --dev. Removed on graceful shutdown. ADR 0098. | |
--reset | bool | false | Delete local dev state (SQLite db, NATS store, blobs, JWT secret, bootstrap key) before starting, then boot fresh. Requires --dev; SQLite only (refused under PostgreSQL). The single-node SQLite tier is a production mode, so the --dev gate is what makes this dev-only. |
Env vars apply to the flags-only path only. The (or set ...) fallbacks above — NATS_URL, NATS_CREDS_FILE, NATS_STORE_DIR, NATS_SYNC_INTERVAL, IRONFLOW_NODE_ID, and IRONFLOW_DATABASE_URL — are read when you start serve with flags. With -f file.yaml the file is the source of truth and none of them are applied (IRONFLOW_DATABASE_URL only as the URL source under driver: postgres); IRONFLOW_STALE_CLAIM_THRESHOLD, IRONFLOW_STALE_CLAIM_RECOVERY_INTERVAL, IRONFLOW_MASTER_KEY, IRONFLOW_JWT_SECRET, IRONFLOW_MCP_REMOTE, NATS_FILE_STORAGE and NATS_STREAM_REPLICAS are dropped the same way. Set the YAML field, or reference the variable as ${VAR}. serve logs a warning for each one set but ignored, and ironflow validate -f lists them in its summary.
Bootstrap seed variables. Two variables let you supply the first-boot admin credentials instead of reading a generated key file. Both apply to the flags-only path only. With -f file.yaml they are ignored, and serve logs a warning for each one that is set.
| Variable | Format | Effect |
|---|---|---|
IRONFLOW_BOOTSTRAP_ADMIN_KEY | ifkey_ followed by 32 or more lowercase hex characters. No spaces, no line breaks. | First boot stores this value as the admin key ak_admin_bootstrap. No key file is written and no BOOTSTRAP_ADMIN_KEY_ISSUED event is emitted. If the value differs from the stored key at a later boot, the stored key is replaced (rotation). If it is unchanged or unset, nothing is written. If the ak_admin_bootstrap row no longer exists, the seed is not applied and serve logs a warning. The format is checked, and a key with fewer than 8 distinct hex characters (a placeholder such as 32 zeros) is refused. Generate the key with a random source, for example openssl rand -hex 16. A malformed value stops startup with an error that names the variable, never the value. |
IRONFLOW_BOOTSTRAP_ADMIN_PASSWORD | A non-empty string of 72 bytes or less, with no line break and no space at the start or end. | Dashboard admin password. Used on first boot only. Needs IRONFLOW_BOOTSTRAP_ADMIN_KEY: a password without a key is a startup error. On a later boot, a value that does not match the stored password is ignored with a warning. |
Other environment variables. serve --help also documents these. IRONFLOW_POLICY_ENFORCEMENT applies to the flags-only path only. The IRONFLOW_OTEL_* and IRONFLOW_METRICS_ENABLED variables apply with and without -f. With -f, a field that the file sets (spec.observability) wins over the variable. See Configuration for the YAML equivalents.
| Variable | Default | Effect |
|---|---|---|
IRONFLOW_POLICY_ENFORCEMENT | false | Enforce stored Rego policy sets on top of RBAC |
IRONFLOW_OTEL_ENDPOINT | (disabled) | OTLP gRPC endpoint for tracing |
IRONFLOW_OTEL_SAMPLE_RATE | 1.0 | Trace sampling rate, 0.0 to 1.0 |
IRONFLOW_OTEL_SERVICE_NAME | ironflow | OpenTelemetry service name |
IRONFLOW_OTEL_INSECURE | true | Use plaintext gRPC for OTLP |
IRONFLOW_METRICS_ENABLED | false | Serve Prometheus metrics at /metrics |
See Seed the key from a secret store.
What it starts:
- Embedded NATS JetStream (messaging)
- Database — SQLite by default, PostgreSQL if
IRONFLOW_DATABASE_URLis set - Workflow engine (step execution, memoization, retry)
- PubSub bridge (real-time WebSocket event delivery)
- Consumer group manager (load-balanced event delivery)
- HTTP server with ConnectRPC API and web dashboard
For PostgreSQL, set IRONFLOW_DATABASE_URL to a URI or keyword/value DSN. See
connection-string escaping and upgrade behavior.
Examples:
# Start with defaults (SQLite, port 9123)ironflow serve
# Custom port, open dashboardironflow serve --port 8080 --open
# PostgreSQL backend (embedded NATS needs an explicit store directory)# Set IRONFLOW_JWT_SECRET to a persistent hex key generated with openssl rand -hex 32.IRONFLOW_DATABASE_URL="postgres://user:pass@localhost:5432/ironflow" \IRONFLOW_BLOB_URL="s3://ironflow-artifacts?endpoint=https://s3.example.com" \IRONFLOW_JWT_SECRET="$IRONFLOW_JWT_SECRET" \ ironflow serve --nats-store-dir ./data/nats --bootstrap-key-file ./data/bootstrap-key.json
# Persistent NATS storage (default since #614 — derived from --db path)# To override the auto-derived path:ironflow serve --nats-store-dir ./data/nats
# Memory-mode opt-out (tests, demos, ephemeral CI)ironflow serve --nats-store-dir=""
# Multi-node cluster (external NATS + PostgreSQL + s3:// blob URL required)IRONFLOW_DATABASE_URL="postgres://user:pass@localhost:5432/ironflow" \IRONFLOW_BLOB_URL="s3://ironflow-artifacts?endpoint=https://seaweedfs.internal:8333" \IRONFLOW_JWT_SECRET="$IRONFLOW_JWT_SECRET" \ ironflow serve --bootstrap-key-file /run/ironflow/bootstrap-key.json --nats-url nats://nats:4222 --node-id node-1
# Cluster with NATS authenticationIRONFLOW_DATABASE_URL="postgres://..." IRONFLOW_BLOB_URL="s3://ironflow-artifacts?endpoint=https://seaweedfs.internal:8333" \IRONFLOW_JWT_SECRET="$IRONFLOW_JWT_SECRET" \ ironflow serve --bootstrap-key-file /run/ironflow/bootstrap-key.json --nats-url nats://nats:4222 --nats-creds /etc/ironflow/node.creds
# Auth is always on — first boot writes the admin API key to the bootstrap key file and prints its path
# Custom bootstrap key file (for example a systemd unit with a tmpfs path)ironflow serve --bootstrap-key-file /run/ironflow/bootstrap-key.json
# Dev mode — skip auth for local developmentironflow serve --dev
# Fresh start — delete local dev state (db, NATS, blobs, secrets), then boot# Requires --dev; SQLite only. Refused under PostgreSQL and without --dev so it# can never wipe a single-node SQLite production install.# Note: with a custom --db, --reset also removes derived siblings in that dir# (ironflow-nats/, blobs/, .ironflow_jwt_secret, .ironflow_bootstrap_key.json).ironflow serve --dev --resetLocal state
Section titled “Local state”serve writes local state to .ironflow/ in the working directory. This holds the SQLite database, the embedded NATS store, blobs/, .ironflow_bootstrap_key.json (an admin API key), .ironflow_jwt_secret and engine.json. Add .ironflow/ to .gitignore. The ironflow init templates already do this. A different --db path, or spec.storage.path in the YAML file, moves the database, blobs/, the JWT secret, the bootstrap key and engine.json to the directory of that path. Under PostgreSQL, serve requires an explicit s3:// blob URL, hex JWT secret material, and bootstrap key output path before starting NATS or the database. It writes no implicit JWT or bootstrap file. No engine.json is written unless you set --discovery-file.
Dev Mode
Section titled “Dev Mode”The --dev flag disables all authentication for local development:
ironflow serve --devIn dev mode:
- API requests are granted admin-level access without an API key
- Dashboard loads without requiring login
- All requests use
org_default,env_default, and theadminrole - A prominent warning is shown in the startup banner
- CORS is restricted to loopback origins. Because auth is bypassed, a browser
request is only answered cross-origin when its
Originislocalhost,127.0.0.1, or::1— so a local app onhttp://localhost:3000works, while a remote site the developer happens to visit cannot read the local engine. Outside dev mode the API staysAccess-Control-Allow-Origin: *(no credentials are ever allowed, so a caller still needs an API key)
Without --dev, authentication is on, unless the file you pass with -f sets spec.auth.devMode: true. With dev mode off, a request with no valid API key or session fails with HTTP 401 and the message authentication required. To clear it, restart with --dev or send the bootstrap API key as IRONFLOW_API_KEY.
Warning: Never use
--devin production. It is a CLI flag only (no env var) to ensure it is an intentional choice on every startup.
Headless Mode
Section titled “Headless Mode”Use --headless when Ironflow is the backend of another application, for example a desktop app, and the users of that application must not see Ironflow. The application calls Ironflow through the API, the SDKs, workers and MCP.
ironflow serve --headless --host 127.0.0.1In headless mode:
- The dashboard is not served.
/,/login,/platformand every other non-API path return404 page not foundas plain text, with no Ironflow headers. - Everything else is unchanged.
/api/v1/*, ConnectRPC,/ws,/health,/ready,/metricsand MCP work as usual. Authentication is unchanged. - The startup check for the embedded dashboard is skipped, because nothing is served from it.
--openis refused, because there is no dashboard to open.
With -f, set spec.headless: true in the YAML file, or pass --headless. An explicit flag wins over the file: --headless=false turns headless mode off when the file sets it.
The startup banner shows no dashboard URL. The admin password signs in through POST /api/v1/auth/login, and the platform operator bootstraps through POST /api/v1/platform/bootstrap.
Separate metrics port
Section titled “Separate metrics port”Use --metrics-port 9223, or YAML spec.observability.metrics.port: 9223, to serve metrics on a separate listener bound to the server host. The flag wins over the file, and metrics must be enabled for the listener to open. The API port then answers 404 for /metrics. /metrics needs no credentials, so do not publish this port. Zero/unset preserves the API-port endpoint. Startup reports the effective metrics URL after both listeners bind; collisions and binding failures abort startup. A serving failure terminates the process through its normal ordered cleanup.
--jwt-secret-file PATH selects an existing private raw hex signing key file. It conflicts with inline jwtSecret and supports durable API rotation. The flags-only environment equivalent is IRONFLOW_JWT_SECRET_FILE; YAML uses spec.auth.jwtSecretFile. Missing or untrusted files refuse startup without creating them.