Fly.io
Deploy the console on Fly.io from the console image, with an external PostgreSQL 18: Dashboard steps with flyctl equivalents.
Requirements
| Item | Requirement |
|---|---|
| Fly.io | An organization that can create apps |
| flyctl | The fly CLI after fly auth login; app creation, deployment, and upgrades use flyctl, see Methods |
| Console image | ghcr.io/marvinli001/edgeweir:<YYYYMMDD>-<commit>, public; tag rules in Versions, upgrades, and rollback |
| PostgreSQL | PostgreSQL 18 reachable from the Machine. Fly Managed Postgres offers only 16 and 17 (October 2026) and does not qualify |
| Public addresses | Dedicated IPv4 (USD 2 a month) and dedicated IPv6 |
| Master key | Generated with openssl rand -base64 32; stored outside Fly.io and apart from database backups |
| Always on | auto_stop_machines = "off" |
| Local commands | fly, openssl, curl |
Topology
| Fly.io resource | Target | Carries |
|---|---|---|
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 3000 | Browsers, /api/v1, /install.sh, /healthz |
[[services]]: 8443/TCP, no handler | Container port 8443 | Node channel; Fly Proxy forwards TCP as is, the console terminates TLS and mTLS |
Dedicated IPv4, dedicated IPv6, host name edgeweir-console.fly.dev | All ports above | A shared IPv4 does not forward 8443 |
| External PostgreSQL 18 | Secret DATABASE_URL | Database |
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.
| Step | Dashboard | flyctl |
|---|---|---|
| 1. Create the app | — | fly apps create |
| 2. Write fly.toml | — | Local file |
| 3. Set secrets | Secrets | fly secrets set |
| 4. Deploy | — | fly deploy |
| 5. Check the public addresses | — | fly ips |
| 6. Run setup | Search logs in Grafana | fly logs |
| Custom domains | Certificates | fly 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.
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"| Setting | Effect |
|---|---|
[build] image | Deploys this image; no build |
[http_service]: internal_port = 3000, force_https = true | 80 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 handlers | Fly 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
-
Generate the master key locally:
umask 077 openssl rand -base64 32 > edgeweir-master-key -
Dashboard →
edgeweir-console→ Secrets, add:Secret Value EDGEWEIR_MASTER_KEYContents of edgeweir-master-key, as isDATABASE_URLpostgres://edgeweir:<password>@<host>:5432/edgeweir -
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 listExpected: one dedicated IPv4 (v4) and one dedicated IPv6 (v6), no shared IPv4 (shared_v4).
| Case | Action |
|---|---|
| No addresses | fly 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.
- Dashboard →
edgeweir-console→ Logs & Errors → Search logs in Grafana, query"first-run setup". Logs are kept for 7 days. - The line's
setupTokenfield is the setup token; itsurlfield is the setup wizard address (<EDGEWEIR_PUBLIC_URL>/setup). - Open the wizard and enter the setup token; see Quick start.
flyctl:
fly logs --no-tail | grep setupTokenfly logs --no-tail returns only recent logs; if the line is missing, run fly apps restart edgeweir-console and read it again.
Variables
| Variable | Where | Value | Notes |
|---|---|---|---|
DATABASE_URL | Secret | PostgreSQL 18 connection string | Required. |
EDGEWEIR_MASTER_KEY | Secret | Output of openssl rand -base64 32 | Required. |
EDGEWEIR_PUBLIC_URL | [env] | https://edgeweir-console.fly.dev | Required. With a custom domain, use that domain. |
EDGEWEIR_NODE_API_URL | [env] | https://edgeweir-console.fly.dev:8443 | The 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] | Empty | Extra names for the node channel certificate, comma separated. |
EDGEWEIR_TRUSTED_PROXIES | — | Empty | See Limits. |
EDGEWEIR_VERSION | — | Not set | The running version built into the image; the tag in [build] image sets the version. |
BETTER_AUTH_SECRET | Secret | Not set | Keep 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
| Check | Dashboard or command | Expected |
|---|---|---|
| Machine | Machines; fly status | 1 Machine, state started, all checks passing |
| Public addresses | fly ips list | Dedicated IPv4 and IPv6, no shared IPv4 |
| Web and API | curl -fsS https://edgeweir-console.fly.dev/healthz | {"status":"ok","version":"20260929-a1b2c3d"} |
| Node channel TLS | openssl command below | Issuer Edgeweir Node Channel CA; SAN includes edgeweir-console.fly.dev |
| Node channel URL | System settings, "Node channel" | https://edgeweir-console.fly.dev:8443 |
| Node enrollment | Clusters & 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.devAny other issuer means a device in the path terminates TLS.
Custom domains
Complete this before enrolling nodes.
-
Dashboard →
edgeweir-console→ Certificates, addconsole.example.com; create theA(dedicated IPv4) andAAAArecords shown there in DNS. flyctl:fly certs add console.example.com. -
Change
[env]infly.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 tohttps://console.example.com:8443. Port 8443 needs nofly 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. -
Deploy:
fly deploy -
Verify: run the
curlandopensslcommands from Verification againstconsole.example.com.
Changing the node channel address for enrolled nodes: Node channel URL and certificate.
Upgrade
-
Back up the database; see Backup and recovery.
-
Change
[build] imageinfly.tomlto the new tag. -
Deploy:
fly deploy -
Verify:
curl -fsS https://edgeweir-console.fly.dev/healthzExpected:
versionis the new tag.
Migrations, signature verification, and rollback: Versions, upgrades, and rollback.
Limits
| Item | Behavior | Effect |
|---|---|---|
| Client IP | Fly 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-IP | Leave EDGEWEIR_TRUSTED_PROXIES empty; audit log IPs and sign-in rate limiting use the Fly Proxy address; see Trusted proxies and client IP |
| Shared IPv4 | Forwards only 80, 443, and ports with the tls handler | Nodes connecting to 8443 over IPv4 need a dedicated IPv4 |
| Nodes' source address | Fly 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 address | Machine egress IPs are not fixed and may change with restarts or platform changes | When 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 IPv4 | 8443 is reachable only over the dedicated IPv6 | Nodes without IPv6 cannot connect |
| Deployment strategy | Default 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 too | With one Machine, the web console and the node channel are down during a deployment or restart; conditions for several instances: Deployment overview |
| Auto stop | With auto_stop_machines set to stop or suspend, Fly Proxy stops idle Machines | Must be off: a stopped Machine halts the worker's scheduled jobs and the node channel |
| Configuration source | Machine 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 deploy | Change them in fly.toml |
| Secrets | The Dashboard, API, and flyctl do not return values; anyone with deploy or SSH access can read them inside the Machine | Keep the master key outside Fly.io |
/downloads/* | No downloads mirror directory in the container; EDGEWEIR_DOWNLOADS_DIR unset | Returns 404; install.sh downloads from GitHub; see Adding nodes |
Railway
Deploy the console on Railway from the console image, with Railway PostgreSQL 18: web console steps with CLI equivalents.
bunny.net Magic Containers
Deploy the console on bunny.net Magic Containers from the console image, with an external PostgreSQL 18: Dashboard steps with bunny CLI equivalents.