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 <noreply@anthropic.com>
This commit is contained in:
rzuasti
2026-06-18 09:00:07 -04:00
co-authored by Claude Opus 4.8
parent ab8cdcc275
commit 386e459c12
3 changed files with 199 additions and 4 deletions
+89
View File
@@ -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="<pick-a-strong-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 <your-OOTT_API_KEY>" \
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.
+40
View File
@@ -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"
+70 -4
View File
@@ -75,6 +75,8 @@
clippy # Rust linter clippy # Rust linter
pythonEnv pythonEnv
gh # GitHub CLI tool (for release process) 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 # fish > all
@@ -150,13 +152,60 @@
}; };
# Package definition # Package definition
packages = forEachSystem (system: { packages = forEachSystem (system: let
oott = pkgsBySystem.${system}.oott; pkgs = pkgsBySystem.${system};
default = pkgsBySystem.${system}.oott;
# 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" <<EOF
[database]
path = "$data_dir/oott.db"
[log]
level = "''${OOTT_LOG_LEVEL:-info}"
[notifications]
method = "''${OOTT_NOTIFICATIONS_METHOD:-none}"
[web_server]
ip_address = "0.0.0.0"
port = ''${OOTT_PORT:-8080}
api_key = "$OOTT_API_KEY"
[arp_scanner]
enabled = false
[mdns_scanner]
enabled = false
[ssdp_scanner]
enabled = false
[dhcp_scanner]
enabled = false
EOF
exec oott --config "$data_dir/oott.toml"
'';
};
in {
oott = pkgs.oott;
default = pkgs.oott;
# Docker image generation # Docker image generation
# use via 'nix build .#dockerImage' # use via 'nix build .#dockerImage'
dockerImage = with pkgsBySystem.${system}; dockerImage = with pkgs;
dockerTools.buildLayeredImage { dockerTools.buildLayeredImage {
name = "oott"; name = "oott";
tag = "latest"; tag = "latest";
@@ -171,6 +220,23 @@
Env = ["SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt"]; Env = ["SSL_CERT_FILE=${cacert}/etc/ssl/certs/ca-bundle.crt"];
}; };
}; };
# Fly.io deployment image (see deploy/fly/). Same backend + bundled
# front-end as dockerImage, but its config is generated at startup from the
# environment (flyEntrypoint) so the DB can live on a Fly volume and the
# API key can come from a Fly secret. Build with 'nix build .#flyImage'.
flyImage = pkgs.dockerTools.buildLayeredImage {
name = "oott-fly";
tag = "latest";
contents = [flyEntrypoint pkgs.oott pkgs.cacert];
# /tmp for the process, /data as the mountpoint for the Fly volume.
extraCommands = "mkdir -p tmp data";
config = {
Cmd = ["${flyEntrypoint}/bin/oott-fly-entrypoint"];
Env = ["SSL_CERT_FILE=${pkgs.cacert}/etc/ssl/certs/ca-bundle.crt"];
ExposedPorts = {"8080/tcp" = {};};
};
};
}); });
# Modules definition # Modules definition