Skip to content

Server Deployment ​

aerini-server is the headless binary that runs Aerini without the desktop app. It has two modes: serve, which runs one workflow exported from the desktop app, and api, which runs a full multi-workflow REST server. This page covers getting either one running, on Docker or on a bare-metal Linux box. For the security model behind the flags used here (SSRF protection, dangerous-node gating, credential encryption), see Security §Hardening a server deployment; that page explains the why, this one covers the how. An exhaustive flag and REST route reference lives in Server CLI Reference and Server API Reference; this page sticks to what you need to get a deployment running and keep it running.

Choosing a mode ​

Export a single workflow from the desktop app (Export → Export for Server, covered in Background Runs §Beyond the desktop app) and you get serve mode: one workflow, its own run history, a status page, no other configuration to manage. This is the right choice if you have one or two workflows and want the least amount of moving parts.

api mode runs many workflows behind a real REST API with bearer-token auth, its own encrypted credential store, and scoped tokens (read/write/admin). It has no desktop-generated export package. You run it directly and create workflows through the API (or a future admin UI). Choose this if you're running more than a handful of workflows on one box, or you need another program to create and manage workflows programmatically.

Both modes bind 127.0.0.1 by default, disable Shell Command, Code (JS), and Database nodes until you explicitly pass --allow-shell / --allow-code / --allow-database, and print a warning if you widen either of those defaults without acknowledging the tradeoff. None of that is repeated here; see the security page linked above.

The Export for Server panel ​

With a workflow open on the canvas, Export → Export for Server opens a panel that generates the serve-mode package serve mode itself runs (below). It shows:

Export for Server dialog with the Linux Server tab selected, listing required credentials and the status page port

  • Trigger: what the exported package will start on, read straight from the workflow, for example Webhook on :3456/webhook or a schedule's interval.
  • 🖥 Linux Server / 🐳 Docker tabs: which package format to generate, not which server mode. Both tabs still export the same single-workflow serve mode described throughout this page; the Docker tab's output is the self-contained image covered in §The desktop's "Export for Server → Docker" package below, a different thing from api mode's own docker-compose.yml.
  • Credentials: one row per credential the workflow actually references, a ✓ or ⚠ showing whether it's currently filled in on the desktop app, the environment variable name it'll export as, and which node uses it. Only names are ever written into the package; you set the real values as environment variables on the server yourself. If the workflow also uses a {{$vars.x}} variable, a Required variables table above this one lists each one's suggested environment key the same way.
  • Status page port: the port the exported package's status page listens on, 7700 by default, must be between 1024 and 65535. On the Docker tab this maps that port on the host; the container always listens on 7700 internally regardless of what you set here.

Click Generate Linux Package or Generate Docker Package, choose where to save the zip, and the panel tells you the exact next command to run (./install.sh, or docker compose up --build -d). The steps below pick up from there.

Getting the aerini-server binary ​

A prebuilt aerini-server container image is published to GHCR with every release, tagged with the release version and latest: docker pull ghcr.io/panchak2d/aerini-server:<version> is the fastest way to get a working binary, no clone or build step needed. There's still no standalone, downloadable aerini-server executable — the Releases page publishes desktop app installers (.dmg, .msi, .exe, .deb, .AppImage) and the aerini-mcp programs described in MCP Server for AI Agents, but no aerini-server. install.sh, generated by Export for Server for the bare-metal path below, checks for aerini-server in its own folder and points you at that same Releases page if it's missing; that check is for the bare-metal binary specifically and isn't affected by the container image. If you want to customize the image itself rather than run it as published, build it locally instead: docker compose up --build, or build from source, both covered below.

This image is self-contained: the build stage compiles aerini-server as a static musl binary and fetches a matching Node.js runtime for Code (JS) nodes, and the runtime image ships both side by side, so nothing needs installing on the host.

The fastest way to run it is to pull the published image directly, no clone needed:

bash
docker run -d --name aerini \
  -p 127.0.0.1:7700:7700 \
  -e AERINI_TOKEN=$(openssl rand -base64 32) \
  -v aerini_data:/data \
  ghcr.io/panchak2d/aerini-server:latest

If you want to customize the Dockerfile itself, the repository also ships it with a matching docker-compose.yml at its root, and building it locally works the same way:

bash
git clone https://github.com/Panchak2d/aerini
cd aerini
cp .env.example .env   # set AERINI_TOKEN (32+ characters: openssl rand -base64 32)
docker compose up -d

docker-compose.yml requires AERINI_TOKEN to be set (it fails fast with a clear error if you skip this) and publishes the port as 127.0.0.1:7700:7700, host-loopback only. Either way, the image defaults to api mode and answers its health check on GET /api/health. Widen 127.0.0.1:7700 to 0.0.0.0:7700 only once you've read the SSRF and --trusted-proxy-count guidance in Security, and put a reverse proxy with TLS in front rather than exposing this port directly.

Running serve mode in the official image ​

The image's ENTRYPOINT is aerini-server itself; only the default CMD (api) is Compose-specific. To run serve mode instead, mount your exported aerini-server.json into /data and override the command:

yaml
services:
  aerini:
    build: .
    ports:
      - "127.0.0.1:7700:7700"
    volumes:
      - ./aerini-server.json:/data/aerini-server.json:ro
      - aerini_data:/data
    environment:
      RUST_LOG: info
    command: ["serve", "--config", "/data/aerini-server.json", "--bind", "0.0.0.0"]
    restart: unless-stopped

volumes:
  aerini_data:

--bind 0.0.0.0 here is the container's internal listen address, not a host-facing exposure setting. Docker only forwards what the ports: mapping publishes; the actual boundary is still the 127.0.0.1:7700:7700 line above.

The desktop's "Export for Server → Docker" package ​

The Docker tab in the Export for Server panel generates a different Dockerfile: one that git clones the Aerini repository at build time and produces a plain debian:bookworm-slim image, rather than reusing the official one above. That generated image downloads and checksum-verifies the same pinned Node.js runtime the official image uses (from the cloned repository's own NODE_VERSION file) and places it next to aerini-server as node-bundled, which is the only place Aerini's Code node ever looks for it (it never searches PATH, and never did; see below). A workflow with a Code (JS) node runs the same way in this package as it does in the official image above, once --allow-code is set. Workflows without Code nodes are unaffected either way.

Bare metal (Linux) ​

Building aerini-server from source ​

aerini-server builds like any other crate in the workspace; you don't need Tauri's desktop dependencies (webkit2gtk and friends) to build it, only Rust and one system library:

bash
git clone https://github.com/Panchak2d/aerini
cd aerini
sudo apt install -y libdbus-1-dev   # Debian/Ubuntu; needed for OS-keyring support
cargo build --release -p aerini-server

Rust 1.96+ is required to build aerini-server and the desktop app, as Installation states. The compiled binary lands at target/release/aerini-server. This does not build or fetch a Node.js runtime; see the Code node section below if your workflow needs one.

serve mode ​

Unzip the package from Export for Server (Export → Export for Server → 🖥 Linux Server) next to the binary you just built, then either run ./install.sh (installs a systemd user service, handles .env, enables lingering so it survives logout) or run it directly:

bash
./aerini-server serve --config aerini-server.json --port 7700

The status page, plain HTML at /, is public by default and shows workflow name, run count, last/next run. POST /api/run (manual trigger), GET /api/logs (the last 50 entries; ?n= takes 1 to 1000), and GET /api/runs all require the run_secret you set at export time, sent as Authorization: Bearer <run_secret>. If the workflow contains a File node, pass --file-sandbox-dir or File nodes can read and write anywhere the server process can reach; if it contains Shell Command, Code, or Database nodes, they stay disabled until you pass the matching --allow-* flag after auditing the workflow (the generated aerini.service and README.txt both name the exact flag needed, but neither adds it for you).

The aerini-server.json file ​

The export writes one config file, and serve mode reads it at start-up. You rarely edit it, but every field is plain JSON:

FieldWhat it holds
workflow_nameName shown on the status page.
workflow_jsonThe whole workflow, in the same format the desktop app saves.
status_portPort for the status page. --port overrides it.
exported_atWhen the package was generated, shown on the status page.
credential_env_varsMaps each saved credential's ID to the environment variable the server reads its secret from.
run_secretargon2id hash of the secret that guards POST /api/run, /api/logs and /api/runs. Optional: with no run_secret those endpoints stay disabled. The raw secret is shown once in the desktop app and is not in this file.

credential_env_vars looks like this:

json
"credential_env_vars": {
  "openai-prod": "AERINI_CRED_OPENAI_PROD",
  "slack-bot":   "AERINI_CRED_SLACK_BOT"
}

The export fills it in from the credentials your workflow uses, and the generated .env.example lists the same variables. Set each one in the server's environment (the .env file the systemd unit loads, docker run -e, the environment: block in Compose) before starting. The file holds names only, never secrets.

Rules the server checks at start-up:

  • It reads only the variables listed in the mapping. A credential ID that isn't in it is not looked up by its name, so a node using that credential runs with the field left empty and a warning in the log.
  • Every variable name must start with AERINI_CRED_, followed by uppercase letters, digits or underscores. This stops the file from pointing a credential at an unrelated variable such as AERINI_TOKEN.
  • Two credential IDs can't share one variable.
  • Each variable must be set, non-empty and valid Unicode.

You can edit a variable name to match how you store secrets, for example AERINI_CRED_OPENAI_PROD to AERINI_CRED_OPENAI_LIVE, as long as you keep the prefix and set the new name in the environment. Don't change the credential ID keys, since they must match the IDs inside workflow_json. Re-exporting from the desktop app regenerates the file and overwrites such edits.

If serve refuses to start, Troubleshooting lists the messages and fixes.

api mode ​

No export package or config file; everything is flags or environment variables.

bash
export AERINI_TOKEN=$(openssl rand -base64 32)
./aerini-server api --port 7700

AERINI_TOKEN/--token must be at least 32 characters; the server refuses to start otherwise. Leave it unset on first run and the server generates one for you and prints it once to stderr, along with a reminder to set AERINI_TOKEN so a restart doesn't discard it. That first token is granted all three scopes (read, write, admin); create narrower tokens for anything you don't fully trust:

bash
./aerini-server tokens create --label ci --scopes read,write --token $AERINI_TOKEN

Changing AERINI_TOKEN does not replace the token it held before: the server adds each value to its token store the first time it starts with it, and the earlier one stays valid until you revoke it. A token you revoked stays revoked even if AERINI_TOKEN still holds it. To rotate the first token, create a new one with --scopes read,write,admin, use that one to run tokens revoke <token_id> on the old one (its id is in tokens list; a call can't revoke the token it authenticates with), then set AERINI_TOKEN to the new value.

Once running, manage workflows from another terminal without touching the REST API directly:

bash
./aerini-server list                              --token $AERINI_TOKEN
./aerini-server stop    <workflow-name-or-id>      --token $AERINI_TOKEN
./aerini-server start   <workflow-name-or-id>      --token $AERINI_TOKEN
./aerini-server restart <workflow-name-or-id>      --token $AERINI_TOKEN

These accept a partial name or ID match and only fail if that's ambiguous, in which case they print every candidate so you can be specific.

Code (JS) nodes need a bundled runtime, not a system Node install ​

Aerini's Code node never searches PATH for node, on the desktop app or on the server. It looks for exactly one thing: a binary named node-bundled (or node-bundled.exe on Windows) sitting in the same directory as the running executable, or an AERINI_NODE_BIN environment variable pointing at a Node binary directly. install.sh's own check (command -v node) tests for something the server code doesn't actually use, so a system-wide apt install nodejs alone will not make Code nodes work; that warning is misleading, not the actual requirement. Building from source doesn't fetch Node.js either; only the official Docker image and the desktop's Docker export do that automatically. To make Code nodes work in a bare-metal deployment, do one of:

<workflow> in these paths is the workflow's name in lowercase letters and digits, with every run of other characters replaced by one hyphen and at most 48 characters, so My Flow! becomes my-flow. The service is named aerini-<workflow>.

bash
# Option A: point at any working Node.js install (simplest)
echo 'AERINI_NODE_BIN=/usr/bin/node' >> ~/.aerini-server/<workflow>/.env

# Option B: place a binary literally named node-bundled next to aerini-server
cp "$(command -v node)" ~/.aerini-server/<workflow>/node-bundled

Either works for serve mode's systemd service (.env is loaded via EnvironmentFile=) or for api mode (export AERINI_NODE_BIN in the shell or unit that starts it). Workflows without Code nodes are unaffected either way.

Credentials ​

serve mode never touches the encrypted credential store the desktop app uses. It reads environment variables instead, one per credential, named and generated automatically at export time; see Credentials §Running headless for the exact naming rule and the generated .env.example.

api mode is different: it has its own encrypted credential store (POST/GET/DELETE /api/credentials, gated by write/read scope), separate from both the desktop app's store and serve mode's environment variables. Pass --keychain to back its encryption key with the OS keyring (falls back to a file automatically, with a warning, if the keyring isn't available); without it, the key lives in a file under --data-dir. If credentials are already saved and the key can't be found (the key file was deleted, or the keyring entry is gone with no fallback file), the server exits with an error instead of creating a new key that can't decrypt them. Restore the original key file; deleting credentials.db as well starts an empty store.

Verifying the deployment ​

Starting api mode with no token set prints the auto-generated one once, along with a couple of startup warnings (the file sandbox default, and SSRF's DNS-rebinding gap, both explained in full in Security and Server CLI Reference §api) and the control/token-management commands from §api mode:

aerini-server api startup banner, showing the auto-generated token and startup log lines

api mode answers GET /api/health with no authentication required, returning the engine version and whether the bundled Node.js runtime actually spawns:

bash
curl http://localhost:7700/api/health

curl against /api/health returning the engine and bundled-Node status

serve mode has no equivalent JSON health route; check the status page at /, or GET /api/status (unauthenticated for the non-sensitive fields, run_secret-gated for last_error) if you're wiring up your own monitoring.

./aerini-server list confirms api mode sees every workflow you've created and their next scheduled run:

bash
./aerini-server list --token $AERINI_TOKEN

aerini-server list output, showing each workflow's status and next run

Everything above also works as plain REST calls instead of the CLI wrapper, useful if another program needs to create or start a workflow rather than a person running a command. Creating a workflow this way needs its workflow_json double-encoded, the JSON document as a JSON string value, since POST /api/workflows takes the whole workflow as one field:

Creating and starting a workflow entirely via POST /api/workflows and POST /api/scheduler/:id/start

See Server API Reference §Workflows for the full request/response shape of both routes.

Updating ​

There's no in-place binary updater for aerini-server; every deployment shape here updates by rebuilding, not by pulling a published image or binary. See Updating §Server (aerini-server) for the exact commands for each of the three shapes above (official Docker image, bare metal, and the desktop's generated Docker export, which has its own cache gotcha the other two don't), plus what happens when a workflow's schema version doesn't match the binary loading it.

What's next ​