Start typing to search.

Deployment

Remote Docker

View Markdown

Deploy Envoy to a Linux host with deploy-remote.sh: hardened compose topology, required keys, drain checks, and verification.

Remote Docker

The standard remote topology uses docker-compose.remote.yml to run two containers from the same customer image:

flowchart LR browser["Browser"] --> proxy["HTTPS reverse proxy"] proxy --> cp["Control plane :3000"] cp <--> ex["Executor :9090"] cp --> state["state volume"] cp --> data["data and dbs volumes"] ex --> data
Remote Docker topology

The control-plane port binds to host loopback only (127.0.0.1:${ENVOY_HOST_PORT:-3010}:3000). Put Caddy, Nginx, or another reverse proxy on the host and expose only HTTPS.

Differences from the local compose file

Compared with the local docker-compose.yaml, the remote file:

  • runs both containers as an unprivileged user (ENVOY_RUN_UID/ENVOY_RUN_GID, default 10001:10001) with no-new-privileges:true and all Linux capabilities dropped;
  • gives the executor a read-only root filesystem with tmpfs scratch mounts (/tmp, /app/.envoy, /app/logs) and bounded pids_limit and mem_limit;
  • binds host directories instead of named volumes: /opt/envoy/state, /opt/envoy/data, and /opt/envoy/dbs map to /app/state, /app/data, and /app/dbs;
  • publishes the control plane only on loopback and defaults ENVOY_LOG_STORE to db;
  • supports Postgres state/log backends, seed-import skipping (ENVOY_SKIP_SEED_IMPORT), public dashboard toggles, catalog variables, and Litestream replication through pass-through environment variables;
  • gives the executor a stop_grace_period: 60s and optional IPv6 egress (ENVOY_NETWORK_IPV6).

Because the executor's root filesystem is read-only, file connectors must use absolute paths under writable mounts (/app/data, /app/dbs). A relative path resolves against /app, which is read-only, and fails.

Required inputs

deploy-remote.sh loads .env.deploy.remote (override the file with DEPLOY_ENV_FILE). Required values:

  • DEPLOY_HOST — SSH config alias or user@host;
  • GHCR_USERNAME and GHCR_TOKEN — GitHub Container Registry credentials (write:packages);
  • ENVOY_ENCRYPTION_KEY and ENVOY_EXECUTOR_KEY — two independent high-entropy values (generate with openssl rand -hex 32). On re-deploys the script reuses the values already stored in the remote .env when they are not supplied locally.

The build machine needs Bun and Docker; the remote host needs Docker with Compose and SSH access. Keep the env file outside version control — it carries registry and database credentials.

What the script automates

./scripts/deploy-remote.sh

The script performs, in order:

  1. Change detection — diffs against the per-customer remote-deployed git tag and decides whether to rebuild the image and which services to redeploy. FORCE_DEPLOY=true redeploys everything; DEPLOY_SCOPE=cp or DEPLOY_SCOPE=ex forces one service; SKIP_BUILD=true skips the binary build.
  2. Build — runs bun scripts/envoy-builder.ts <customer> --all --out=dist, then docker build for linux/amd64.
  3. Push — logs in to GHCR and pushes the timestamped image tag plus latest.
  4. Deploy over SSH — creates /opt/envoy/{state,data,dbs}, writes the generated .env, copies docker-compose.remote.yml (as docker-compose.yml) and harden-egress.sh to the host, patches the persisted envoy-server.yaml (branding URLs, missing ai: section when ANTHROPIC_API_KEY is set, connector upload limit, Postgres pool settings), pulls the image, installs the egress-hardening systemd unit, and runs docker compose up -d --remove-orphans for the selected services.

Drain check before executor restarts

When the deploy includes the executor, the script reads activeRunsByLease from GET /api/health. If active runs exist it aborts, unless drain waiting is enabled:

WAIT_FOR_DRAIN=true ./scripts/deploy-remote.sh

Drain waiting polls every 10 seconds up to DRAIN_TIMEOUT (default 600 seconds) and still aborts if runs remain. Recreating the executor kills in-flight task processes, so this gate is deliberate.

Egress hardening

The script installs harden-egress.sh as /usr/local/sbin/envoy-harden-egress with a systemd unit (envoy-egress-hardening.service) that reapplies it after Docker or host restarts. The rules drop container traffic to the instance metadata ranges 169.254.0.0/16 and fe80::a9fe:a9fe/128 in the DOCKER-USER chain. See Hardening, RBAC, and secrets.

State backends

SQLite is the default (ENVOY_STORE_TYPE=sqlite, ENVOY_LOG_STORE=db), with the database under /opt/envoy/state. Optional Litestream replication activates only when the S3-compatible variables are complete — see Backups and upgrades.

For PostgreSQL:

ENVOY_STORE_TYPE=postgres
ENVOY_LOG_STORE=postgres
ENVOY_POSTGRES_CONNECTION_STRING=postgres://user:password@host:5432/envoy
ENVOY_POSTGRES_SSL=require

The script refuses a postgres backend without ENVOY_POSTGRES_CONNECTION_STRING. Litestream is skipped for non-SQLite stores; use the database provider's backups.

Multi-instance and shared-executor options

One host can run several instances by overriding ENVOY_HOST_PORT, COMPOSE_PROJECT_NAME, ENVOY_CP_CONTAINER_NAME, and ENVOY_EX_CONTAINER_NAME per deployment. DEPLOY_EXECUTOR=false deploys a control plane that shares an existing executor; ENVOY_EXTERNAL_NETWORK joins its Compose network, and ENVOY_CONTROL_PLANES lists every control plane the shared executor serves.

Public dashboard controls

Three independent pass-through variables:

  • ENVOY_PUBLIC_DASHBOARDS_ENABLED — share-link viewing (defaults on when empty);
  • ENVOY_PUBLIC_DASHBOARD_INPUT_ENABLED — public input-widget editing (defaults off);
  • ENVOY_PUBLIC_DASHBOARD_ACTIONS_ENABLED — public action widgets (defaults off).

Keep the write controls off unless a reviewed Dashboard requires them.

Verify the deployment

  1. Open the HTTPS URL and complete First-time setup.
  2. Check Executors for a healthy heartbeat and expected capacity.
  3. Test one non-destructive Connector.
  4. Run a bounded Task and inspect its logs.
  5. Confirm curl http://127.0.0.1:<ENVOY_HOST_PORT>/api/health on the host returns a healthy response.