Edgeweir
Deployment

Fly.io

Deploy the console on Fly.io from the console image, with an external PostgreSQL 18: Dashboard steps with flyctl equivalents.

Requirements

ItemRequirement
Fly.ioAn organization that can create apps
flyctlThe fly CLI after fly auth login; app creation, deployment, and upgrades use flyctl, see Methods
Console imageghcr.io/marvinli001/edgeweir:<YYYYMMDD>-<commit>, public; tag rules in Versions, upgrades, and rollback
PostgreSQLPostgreSQL 18 reachable from the Machine. Fly Managed Postgres offers only 16 and 17 (October 2026) and does not qualify
Public addressesDedicated IPv4 (USD 2 a month) and dedicated IPv6
Master keyGenerated with openssl rand -base64 32; stored outside Fly.io and apart from database backups
Always onauto_stop_machines = "off"
Local commandsfly, openssl, curl

Topology

Fly.io resourceTargetCarries
App edgeweir-console, 1 Machine—ROLE=all (image default): web UI, API, node channel, pg-boss worker
[http_service]: 80 (HTTP, redirects to HTTPS), 443 (TLS terminated by Fly Proxy)Container port 3000Browsers, /api/v1, /install.sh, /healthz
[[services]]: 8443/TCP, no handlerContainer port 8443Node channel; Fly Proxy forwards TCP as is, the console terminates TLS and mTLS
Dedicated IPv4, dedicated IPv6, host name edgeweir-console.fly.devAll ports aboveA shared IPv4 does not forward 8443
External PostgreSQL 18Secret DATABASE_URLDatabase

General rules for ports and the node channel certificate: Ports, reverse proxy, and trusted proxies.

Methods

App creation, image deployment, and upgrades use flyctl; the other steps can be done in the Dashboard, with flyctl as the equivalent.

StepDashboardflyctl
1. Create the app—fly apps create
2. Write fly.toml—Local file
3. Set secretsSecretsfly secrets set
4. Deploy—fly deploy
5. Check the public addresses—fly ips
6. Run setupSearch logs in Grafanafly logs
Custom domainsCertificatesfly certs add
Upgrade—fly deploy

1. Create the app

fly apps create edgeweir-console --org <organization>

App names are globally unique and set the default host name <app name>.fly.dev.

2. Write fly.toml

Create fly.toml in an empty directory. Adjust app, primary_region, the image tag, and the app name in the URLs.

fly.toml
app = "edgeweir-console"
primary_region = "nrt"

[build]
  image = "ghcr.io/marvinli001/edgeweir:20260929-a1b2c3d"

[env]
  EDGEWEIR_PUBLIC_URL = "https://edgeweir-console.fly.dev"
  EDGEWEIR_NODE_API_URL = "https://edgeweir-console.fly.dev:8443"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = "off"
  auto_start_machines = true
  min_machines_running = 1

  [[http_service.checks]]
    grace_period = "30s"
    interval = "15s"
    timeout = "5s"
    method = "GET"
    path = "/healthz"

[[services]]
  internal_port = 8443
  protocol = "tcp"
  auto_stop_machines = "off"
  auto_start_machines = true
  min_machines_running = 1

  [[services.ports]]
    port = 8443

  [[services.tcp_checks]]
    grace_period = "30s"
    interval = "15s"
    timeout = "2s"

[[vm]]
  size = "shared-cpu-1x"
  memory = "1gb"
SettingEffect
[build] imageDeploys this image; no build
[http_service]: internal_port = 3000, force_https = true80 redirects to HTTPS; Fly Proxy terminates TLS on 443 and forwards to 3000
auto_stop_machines = "off"Fly Proxy does not stop an idle Machine; min_machines_running then has no effect
[[http_service.checks]]GET /healthz directly on the Machine over the private network; must return 2xx
[[services]]: internal_port = 8443; [[services.ports]]: port = 8443, no handlersFly Proxy forwards TCP as is
[[services.tcp_checks]]Checks that 8443 accepts connections
[[vm]]The console sets no minimum size; the example uses shared-cpu-1x, 1 GB

3. Set secrets

  1. Generate the master key locally:

    umask 077
    openssl rand -base64 32 > edgeweir-master-key
  2. Dashboard → edgeweir-console → Secrets, add:

    SecretValue
    EDGEWEIR_MASTER_KEYContents of edgeweir-master-key, as is
    DATABASE_URLpostgres://edgeweir:<password>@<host>:5432/edgeweir
  3. Keep them staged; do not click Deploy Secrets. The secrets take effect with the deployment in step 4.

flyctl:

fly secrets set --stage \
  EDGEWEIR_MASTER_KEY="$(cat edgeweir-master-key)" \
  DATABASE_URL='postgres://edgeweir:<password>@<host>:5432/edgeweir'

Fly.io does not reveal secret values; store edgeweir-master-key offline, apart from database backups; see Backup and recovery.

4. Deploy

In the directory that holds fly.toml:

fly deploy --ha=false

--ha=false creates a single Machine. Because [[services]] has a non-HTTP port such as 8443, the first deployment asks Would you like to allocate dedicated ipv4 and ipv6 addresses now?: answer y to get a dedicated IPv4 (USD 2 a month) and a dedicated IPv6, and no shared IPv4. Answering n, or deploying without a terminal, allocates no address at all and the deployment still completes; add them in step 5.

5. Check the public addresses

fly ips list

Expected: one dedicated IPv4 (v4) and one dedicated IPv6 (v6), no shared IPv4 (shared_v4).

CaseAction
No addressesfly ips allocate-v6, then fly ips allocate-v4 --yes (--yes accepts the charge for the dedicated IPv4)
A shared IPv4 (the app was deployed earlier with another fly.toml)fly ips allocate-v4 --yes, then fly ips release <shared IPv4>

edgeweir-console.fly.dev resolves to the dedicated addresses once DNS caches expire. Do not go back to a shared IPv4: it does not forward 8443.

6. Run setup

An uninitialized console logs the same setup token on every start.

  1. Dashboard → edgeweir-console → Logs & Errors → Search logs in Grafana, query "first-run setup". Logs are kept for 7 days.
  2. The line's setupToken field is the setup token; its url field is the setup wizard address (<EDGEWEIR_PUBLIC_URL>/setup).
  3. Open the wizard and enter the setup token; see Quick start.

flyctl:

fly logs --no-tail | grep setupToken

fly logs --no-tail returns only recent logs; if the line is missing, run fly apps restart edgeweir-console and read it again.

Variables

VariableWhereValueNotes
DATABASE_URLSecretPostgreSQL 18 connection stringRequired.
EDGEWEIR_MASTER_KEYSecretOutput of openssl rand -base64 32Required.
EDGEWEIR_PUBLIC_URL[env]https://edgeweir-console.fly.devRequired. With a custom domain, use that domain.
EDGEWEIR_NODE_API_URL[env]https://edgeweir-console.fly.dev:8443The host name must resolve to the dedicated IPv4 and IPv6; it is added to the node channel certificate automatically. After setup it can also be changed under "Node channel" in System settings, without a deployment; a URL saved there wins over this variable.
EDGEWEIR_NODE_API_HOSTNAMES[env]EmptyExtra names for the node channel certificate, comma separated.
EDGEWEIR_TRUSTED_PROXIES—EmptySee Limits.
EDGEWEIR_VERSION—Not setThe running version built into the image; the tag in [build] image sets the version.
BETTER_AUTH_SECRETSecretNot setKeep the existing value when migrating a deployment that set it.

A secret takes precedence over an [env] entry with the same name. ROLE, HOST, PORT, and NODE_API_PORT keep the image defaults all, 0.0.0.0, 3000, and 8443, which match internal_port. All variables: Environment variables.

Verification

CheckDashboard or commandExpected
MachineMachines; fly status1 Machine, state started, all checks passing
Public addressesfly ips listDedicated IPv4 and IPv6, no shared IPv4
Web and APIcurl -fsS https://edgeweir-console.fly.dev/healthz{"status":"ok","version":"20260929-a1b2c3d"}
Node channel TLSopenssl command belowIssuer Edgeweir Node Channel CA; SAN includes edgeweir-console.fly.dev
Node channel URLSystem settings, "Node channel"https://edgeweir-console.fly.dev:8443
Node enrollmentClusters & nodes → Add node (the dialog shows the install command as it opens)--server is https://edgeweir-console.fly.dev:8443; running it on a node: Adding nodes
openssl s_client -connect edgeweir-console.fly.dev:8443 -servername edgeweir-console.fly.dev </dev/null 2>/dev/null \
  | openssl x509 -noout -text | grep -E 'Issuer:|Subject:|DNS:'

Expected:

        Issuer: CN=Edgeweir Node Channel CA, O=Edgeweir
        Subject: CN=edgeweir-node-api, O=Edgeweir
                DNS:localhost, IP Address:127.0.0.1, IP Address:0:0:0:0:0:0:0:1, DNS:<Machine host name>, DNS:edgeweir-console.fly.dev

Any other issuer means a device in the path terminates TLS.

Custom domains

Complete this before enrolling nodes.

  1. Dashboard → edgeweir-console → Certificates, add console.example.com; create the A (dedicated IPv4) and AAAA records shown there in DNS. flyctl: fly certs add console.example.com.

  2. Change [env] in fly.toml:

    fly.toml
    [env]
      EDGEWEIR_PUBLIC_URL = "https://console.example.com"
      EDGEWEIR_NODE_API_URL = "https://console.example.com:8443"

    When a URL is saved under "Node channel" in System settings, it wins over EDGEWEIR_NODE_API_URL: change it there to https://console.example.com:8443. Port 8443 needs no fly certs add, but the name must resolve straight to the dedicated addresses: turn the proxy off (DNS only) in Cloudflare or similar DNS, since Cloudflare's proxy also handles HTTPS on port 8443 and would terminate TLS.

  3. Deploy:

    fly deploy
  4. Verify: run the curl and openssl commands from Verification against console.example.com.

Changing the node channel address for enrolled nodes: Node channel URL and certificate.

Upgrade

  1. Back up the database; see Backup and recovery.

  2. Change [build] image in fly.toml to the new tag.

  3. Deploy:

    fly deploy
  4. Verify:

    curl -fsS https://edgeweir-console.fly.dev/healthz

    Expected: version is the new tag.

Migrations, signature verification, and rollback: Versions, upgrades, and rollback.

Limits

ItemBehaviorEffect
Client IPFly Proxy passes the client address in Fly-Client-IP and X-Forwarded-For; the source range Fly Proxy connects to the Machine from is not published; the console does not read Fly-Client-IPLeave EDGEWEIR_TRUSTED_PROXIES empty; audit log IPs and sign-in rate limiting use the Fly Proxy address; see Trusted proxies and client IP
Shared IPv4Forwards only 80, 443, and ports with the tls handlerNodes connecting to 8443 over IPv4 need a dedicated IPv4
Nodes' source addressFly Proxy forwards 8443 as TCP without the node's address"Connects from" in the node details is not the node's public address. Do not enable the proxy_proto handler on 8443: the console does not parse PROXY protocol headers, so handshakes fail
Outbound addressMachine egress IPs are not fixed and may change with restarts or platform changesWhen PostgreSQL allows clients by source address, give the app a static egress IP: fly ips allocate-egress --app edgeweir-console -r <region> (USD 3.60 a month)
No dedicated IPv48443 is reachable only over the dedicated IPv6Nodes without IPv6 cannot connect
Deployment strategyDefault rolling: each old Machine is stopped and replaced in turn. fly secrets set without --stage, fly scale vm, fly scale memory, and fly apps restart restart the Machine tooWith one Machine, the web console and the node channel are down during a deployment or restart; conditions for several instances: Deployment overview
Auto stopWith auto_stop_machines set to stop or suspend, Fly Proxy stops idle MachinesMust be off: a stopped Machine halts the worker's scheduled jobs and the node channel
Configuration sourceMachine size and HTTP service settings changed in the Dashboard or with fly scale vm / fly scale memory are reset to fly.toml on the next fly deployChange them in fly.toml
SecretsThe Dashboard, API, and flyctl do not return values; anyone with deploy or SSH access can read them inside the MachineKeep the master key outside Fly.io
/downloads/*No downloads mirror directory in the container; EDGEWEIR_DOWNLOADS_DIR unsetReturns 404; install.sh downloads from GitHub; see Adding nodes
Edit on GitHub

On this page