From 386e459c121a87b4dbdf1040e598d7d3871b660c Mon Sep 17 00:00:00 2001 From: rzuasti Date: Thu, 18 Jun 2026 09:00:07 -0400 Subject: [PATCH] Add Fly.io deployment for the App Store review test server Adds a flyImage Nix output (backend + bundled web UI with an env-driven startup config wrapper), a fly.toml with a persistent volume, and a deploy README. Scanners are disabled and the API key comes from a Fly secret so nothing sensitive is baked into the image. Also adds flyctl + skopeo to the dev shell for daemon-less build/push. Co-Authored-By: Claude Opus 4.8 --- deploy/fly/README.md | 89 ++++++++++++++++++++++++++++++++++++++++++++ deploy/fly/fly.toml | 40 ++++++++++++++++++++ flake.nix | 74 ++++++++++++++++++++++++++++++++++-- 3 files changed, 199 insertions(+), 4 deletions(-) create mode 100644 deploy/fly/README.md create mode 100644 deploy/fly/fly.toml diff --git a/deploy/fly/README.md b/deploy/fly/README.md new file mode 100644 index 0000000..0a315f8 --- /dev/null +++ b/deploy/fly/README.md @@ -0,0 +1,89 @@ +# Deploying the OOTT test server to Fly.io + +A throwaway, pay-as-you-go backend (REST API + bundled web UI) on +[Fly.io](https://fly.io), for Apple to exercise the iOS app during App Store +review. Tear it down when review is done so you stop paying. + +The image is the reproducible Nix build (same backend + bundled Flutter web as +the production image). Config is rendered at startup from the environment, so: + +- the **API key** comes from a Fly **secret** (never baked into the image or git), +- the **SQLite database** lives on a Fly **volume** (survives restarts/redeploys), +- the **network scanners are disabled** — a cloud host has no LAN to scan; this + is purely an API/UI server for the reviewer. + +Everything below runs inside the Nix dev shell, which now ships `flyctl` and +`skopeo` (no Docker daemon required): + +```sh +nix develop # or: direnv / your usual dev shell entry +``` + +## 1. One-time setup + +```sh +# Log in (opens a browser). +fly auth login + +# Create the app. Use this name everywhere below (and in fly.toml's `app`). +fly apps create oott-test + +# Persistent storage for the SQLite DB. Match the region in fly.toml. +fly volumes create oott_data --region ams --size 1 --app oott-test + +# The API key the iOS app will authenticate with (keep it; you'll hand the same +# value to the reviewer / configure it in the build under test). +fly secrets set OOTT_API_KEY="" --app oott-test +``` + +## 2. Build the image and push it to the Fly registry + +```sh +# Build the Fly image (backend + bundled web UI + startup config wrapper). +nix build .#flyImage -o result-fly + +# Push the image straight from the Nix store archive to Fly's registry. +# No Docker daemon involved; skopeo authenticates with a short-lived Fly token. +skopeo copy --dest-creds "x:$(fly auth token)" \ + docker-archive:result-fly \ + docker://registry.fly.io/oott-test:latest +``` + +## 3. Deploy + +```sh +fly deploy --app oott-test \ + --config deploy/fly/fly.toml \ + --image registry.fly.io/oott-test:latest +``` + +Your server is now at `https://oott-test.fly.dev`. Quick check: + +```sh +curl -H "Authorization: Bearer " \ + https://oott-test.fly.dev/api/test +# -> "OOTT_API_OK" +``` + +Open `https://oott-test.fly.dev/web` for the UI, `https://oott-test.fly.dev/api/docs` +for the API explorer. + +## 4. Tear down (stop paying) + +```sh +fly apps destroy oott-test # removes the app, machine, and volume +``` + +## Notes + +- **Redeploying after a change:** repeat steps 2 and 3. +- **Cost:** one `shared-cpu-1x`/512MB machine + a 1GB volume is on the order of a + few US dollars a month, billed by usage — well under a dollar for a few days. + To trim it further between reviewer sessions, flip `fly.toml` to scale-to-zero + (`auto_stop_machines = "stop"`, `min_machines_running = 0`) and redeploy. +- **Empty UI:** the reviewer sees a working but device-less app, since there is + no LAN to discover. If the review benefits from a populated screen, seed a few + sample devices via the API (`PUT /api/devices`) after deploying. +- **Config knobs** are env vars in `fly.toml` (`OOTT_LOG_LEVEL`, + `OOTT_NOTIFICATIONS_METHOD`, `OOTT_PORT`, `OOTT_DATA_DIR`); the API key is the + `OOTT_API_KEY` secret. diff --git a/deploy/fly/fly.toml b/deploy/fly/fly.toml new file mode 100644 index 0000000..898c584 --- /dev/null +++ b/deploy/fly/fly.toml @@ -0,0 +1,40 @@ +# Fly.io config for the OOTT App Store review test server. +# +# This deploys the backend (REST API + bundled Flutter web UI) as a single +# always-on machine with a persistent volume for the SQLite database. It is a +# throwaway, pay-as-you-go server: tear it down once review is done +# (`fly apps destroy oott-test`). +# +# See deploy/fly/README.md for the full deploy procedure. + +app = "oott-test" # change to your chosen Fly app name +primary_region = "ams" # change to a region near you / the reviewers + +# The image is built with Nix and pushed to the Fly registry (see README), +# so there is no [build] section: deploy with `fly deploy --image ...`. + +[env] + OOTT_LOG_LEVEL = "info" + OOTT_NOTIFICATIONS_METHOD = "none" # no push/pushover on the test server + OOTT_PORT = "8080" + OOTT_DATA_DIR = "/data" + +# Persistent SQLite storage. Create the volume once with: +# fly volumes create oott_data --region ams --size 1 +[[mounts]] + source = "oott_data" + destination = "/data" + +[http_service] + internal_port = 8080 + force_https = true # Apple ATS requires HTTPS for the app's API calls + # Keep the machine always on during review so a reviewer never hits a cold + # start. To save money once review is over, set auto_stop_machines = "stop" + # and min_machines_running = 0 to scale to zero. + auto_stop_machines = "off" + auto_start_machines = true + min_machines_running = 1 + +[[vm]] + size = "shared-cpu-1x" + memory = "512mb" diff --git a/flake.nix b/flake.nix index 09aea53..33f8d12 100644 --- a/flake.nix +++ b/flake.nix @@ -75,6 +75,8 @@ clippy # Rust linter pythonEnv gh # GitHub CLI tool (for release process) + flyctl # Fly.io CLI (deploy/fly/: test server for App Store review) + skopeo # push the Nix-built image to the Fly registry without a Docker daemon ]; # fish > all @@ -150,13 +152,60 @@ }; # Package definition - packages = forEachSystem (system: { - oott = pkgsBySystem.${system}.oott; - default = pkgsBySystem.${system}.oott; + packages = forEachSystem (system: let + pkgs = pkgsBySystem.${system}; + + # Startup wrapper for the Fly.io image. The backend only reads its config + # from a TOML file, but on Fly we want the (secret) API key to come from a + # Fly secret and the rest of the deployment knobs from plain env vars, with + # nothing sensitive baked into the image. So we render /data/oott.toml from + # the environment on every boot and then exec the backend against it. The + # scanners are forced off: a cloud host has no LAN to scan, this is purely + # an API/UI server for App Store review. + flyEntrypoint = pkgs.writeShellApplication { + name = "oott-fly-entrypoint"; + runtimeInputs = [pkgs.coreutils pkgs.oott]; + text = '' + : "''${OOTT_API_KEY:?OOTT_API_KEY must be set (fly secrets set OOTT_API_KEY=...)}" + data_dir="''${OOTT_DATA_DIR:-/data}" + mkdir -p "$data_dir" + cat > "$data_dir/oott.toml" <