Deployment
Remote Docker
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:
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, default10001:10001) withno-new-privileges:trueand all Linux capabilities dropped; - gives the executor a read-only root filesystem with tmpfs scratch mounts (
/tmp,/app/.envoy,/app/logs) and boundedpids_limitandmem_limit; - binds host directories instead of named volumes:
/opt/envoy/state,/opt/envoy/data, and/opt/envoy/dbsmap to/app/state,/app/data, and/app/dbs; - publishes the control plane only on loopback and defaults
ENVOY_LOG_STOREtodb; - 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: 60sand 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 oruser@host;GHCR_USERNAMEandGHCR_TOKEN— GitHub Container Registry credentials (write:packages);ENVOY_ENCRYPTION_KEYandENVOY_EXECUTOR_KEY— two independent high-entropy values (generate withopenssl rand -hex 32). On re-deploys the script reuses the values already stored in the remote.envwhen 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.shThe script performs, in order:
- Change detection — diffs against the per-customer
remote-deployedgit tag and decides whether to rebuild the image and which services to redeploy.FORCE_DEPLOY=trueredeploys everything;DEPLOY_SCOPE=cporDEPLOY_SCOPE=exforces one service;SKIP_BUILD=trueskips the binary build. - Build — runs
bun scripts/envoy-builder.ts <customer> --all --out=dist, thendocker buildforlinux/amd64. - Push — logs in to GHCR and pushes the timestamped image tag plus
latest. - Deploy over SSH — creates
/opt/envoy/{state,data,dbs}, writes the generated.env, copiesdocker-compose.remote.yml(asdocker-compose.yml) andharden-egress.shto the host, patches the persistedenvoy-server.yaml(branding URLs, missingai:section whenANTHROPIC_API_KEYis set, connector upload limit, Postgres pool settings), pulls the image, installs the egress-hardening systemd unit, and runsdocker compose up -d --remove-orphansfor 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.shDrain 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=requireThe 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
- Open the HTTPS URL and complete First-time setup.
- Check Executors for a healthy heartbeat and expected capacity.
- Test one non-destructive Connector.
- Run a bounded Task and inspect its logs.
- Confirm
curl http://127.0.0.1:<ENVOY_HOST_PORT>/api/healthon the host returns a healthy response.
Related
- Docker — the image contract and entrypoint behavior
- Server configuration — the YAML the entrypoint generates and the script patches
- Backups and upgrades — Litestream, managed databases, and drain-safe upgrades
- Control plane and executors — the split-runtime model