Mobile, Docker, Nix and hosted services
These distributions build from the same brand. None of them run in the shipped release workflow except the Android APK and the Docker image.
Android APK
Section titled “Android APK”The release workflow builds an arm64 APK with scripts/release/build-android-apk.mjs
(see Android signing). To build one
locally you need JDK 21 and the Android SDK:
export FROGG_BRAND_DIR=brands/acmenode scripts/release/build-android-apk.mjs --abi arm64-v8a --out-dir release-assetsThe package ID is your applicationId. Development builds use <applicationId>.debug
and the name <name> Debug, so both install side by side:
FROGG_BRAND_DIR=brands/acme npm run android:development --workspace=@frogg/appEAS (Android and iOS)
Section titled “EAS (Android and iOS)”EAS needs a project-local eas.json. brand:eas exports a disposable copy of the
tracked source, writes the branded eas.json and staged brand into it, and runs
eas-cli from there. Your checkout stays untouched.
-
Create an Expo project and put its ID in the brand:
brands/acme/brand.json "distribution": {"repository": "acme-corp/frogg","expoProjectId": "1b2c3d4e-5f60-4a1b-8c2d-3e4f5a6b7c8d","iosStoreId": "1234567890"} -
Inspect the export without building:
Terminal window FROGG_BRAND_DIR=brands/acme npm run brand:eas -- --prepare-onlyMobile source and EAS configuration exported to /tmp/acme-mobile-XXXXXX -
Build:
Terminal window export EXPO_TOKEN=... # or run `npx eas-cli login` firstFROGG_BRAND_DIR=brands/acme npm run brand:eas -- build --platform android --profile production-apkFROGG_BRAND_DIR=brands/acme npm run brand:eas -- build --platform ios --profile production
Profiles come from apps/ui/eas.json: development, production and production-apk.
iosStoreId is written into the submit.production.ios.ascAppId field of the generated
eas.json. Without it, the iOS submit profile is removed.
Docker image
Section titled “Docker image”Set distribution.dockerImage, then:
export FROGG_BRAND_DIR=brands/acmescripts/release/build-docker.sh # host platform, loads locallyscripts/release/build-docker.sh --push # linux/amd64 + linux/arm64, pushes all tagsThe script stages the brand into .branding-input/acme and passes it as the
FROGG_BRAND_DIR build argument to deploy/docker/base/Dockerfile. Override the target
with FROGG_IMAGE_REPO or FROGG_PLATFORMS.
brand:prepare also writes a Compose file for your product:
FROGG_BRAND_DIR=brands/acme npm run brand:preparedocker compose -f .generated/branding/deploy/compose.json configFor Acme it runs container acme-daemon on port 10099, sets ACME_HOME, mounts
./acme-state, and labels the container with app.brand.id=acme. The generated
install-docker.sh on your release checks that label before replacing a container, so
it never touches another product’s container.
deploy/nix/package.nix takes a pinned, self-contained brandSource. Stage the brand
first, then point your flake at it:
{ inputs.frogg.url = "github:acme-corp/frogg?dir=deploy/nix";
outputs = { self, nixpkgs, frogg, ... }: { nixosConfigurations.build-box = nixpkgs.lib.nixosSystem { system = "x86_64-linux"; modules = [ frogg.nixosModules.default # includes branded-module.nix ({ pkgs, ... }: { services.frogg.instances.acme = { enable = true; package = pkgs.callPackage "${frogg}/deploy/nix/package.nix" { brandSource = ./acme-brand; # brand.json + artwork }; openFirewall = true; environmentFile = "/run/secrets/acme-daemon.env"; }; }) ]; }; };}services.frogg.instances.<name> options: enable, package, user and dataDir
(default from the brand id), port (default daemonPort), listenAddress
(0.0.0.0), openFirewall, environment, environmentFile. Two instances must have
different brand identities.
When the lockfile changes, the package’s npmDepsHash changes too. Override it with
.override { npmDepsHash = "sha256-..."; } if your nixpkgs differs from the pinned one.
Hosted pairing page
Section titled “Hosted pairing page”Pairing links look like <pairingUrl>/code/<offer>. The page is stateless: the offer is
in the URL, and it only hands the code to your app. Without services.pairingUrl,
links use your URL scheme (acme://pair#offer=...) and still work on devices with the
app installed.
Two ways to host it:
-
Container: set
distribution.pairingImage, thenFROGG_BRAND_DIR=brands/acme scripts/release/build-pair-page-docker.sh --push. It listens on port 8787 (FROGG_PAIR_PORT) and serves/healthz. -
Cloudflare Worker:
brand:preparewrites.generated/branding/deploy/pair-worker.jsonwith your hostname as a custom domain.Terminal window export FROGG_BRAND_DIR=brands/acmenode packages/server/scripts/build-pair-worker.mjsnpx wrangler deploy --config .generated/branding/deploy/pair-worker.json
Keep the pairing service off the machines that run your daemons; it has no reason to reach them.
Install scripts on your domain
Section titled “Install scripts on your domain”Every release carries install.sh, uninstall.sh, install-docker.sh and
uninstall-docker.sh generated for your brand. They install your CLI, your service
names and your home directory, and download from your repository’s releases.
To serve them from https://studio.acme.example/install.sh, set links.installer and
deploy the generated install worker:
export FROGG_BRAND_DIR=brands/acmenpm run brand:preparenpx wrangler deploy --config .generated/branding/deploy/install-worker.jsonThe generated config has no routes. Add the exact paths (/install.sh,
/uninstall.sh, /install-docker.sh) for your site in Cloudflare so the worker does
not take over the whole domain.
Web app
Section titled “Web app”The daemon serves the web UI itself when started with --web-ui (installed services
enable it). To host the web client separately on Cloudflare Pages:
export FROGG_BRAND_DIR=brands/acmeexport FROGG_WEB_DEPLOY_PROJECT=acme-studionpm run deploy:web --workspace=@frogg/appA custom brand must set FROGG_WEB_DEPLOY_PROJECT; only stock frogg defaults to a project
(frogg-app). Add the hosted origin to services.allowedOrigins so new daemons accept it.
A brand cannot ship a relay endpoint, and stock frogg has none. To offer connections from
outside the LAN without a VPN, deploy packages/relay yourself and have each daemon owner
set the endpoint: in the app under host settings, Pair devices, or with
daemon.relay.endpoint or FROGG_RELAY_ENDPOINT. Operating a relay is outside the scope of
these pages.