Skip to content

Latest commit

 

History

History
146 lines (108 loc) · 6.97 KB

File metadata and controls

146 lines (108 loc) · 6.97 KB

Deploying the Open Green Button server

The reference deployment is Fly.io with scale-to-zero.

One-time setup

# 1. Pick a domain. Anywhere with a DNS panel works.
#    Suggested host: greenbutton.<your-domain>
#
# 2. Sign up at fly.io, install flyctl.

cd server/
fly launch --no-deploy --copy-config --name open-green-button --region yyz

# 3. Generate and set secrets. Both are 32-byte random base64 strings.
fly secrets set \
  OPENGB_CRYPTO_AESKEYBASE64=$(openssl rand -base64 32) \
  OPENGB_CRYPTO_HMACPEPPERBASE64=$(openssl rand -base64 32) \
  OPENGB_PUBLIC_BASE_URL=https://greenbutton.<your-domain>

# 4. Attach your custom hostname.
fly certs add greenbutton.<your-domain>
# Add the AAAA + A records that fly prints, then:
fly certs check greenbutton.<your-domain>

# 5. Build and deploy. See "Building the image" below — the default is a GraalVM
#    native image built from the repo-root Dockerfile.

Building the image

GraalVM native image (recommended)

The server compiles to a self-contained GraalVM native binary: sub-second cold start and a small memory footprint, which pairs well with Fly's scale-to-zero. The Dockerfile at the repo root is a multi-stage build — it runs :app:nativeCompile inside the GraalVM container (no local GraalVM needed) and packages the binary onto a slim distroless/base-debian12.

The Ktor CIO engine is the only one that works under native-image; that commitment is documented in CioKtorService.kt. The native build is wired up by the org.graalvm.buildtools.native Gradle plugin in server/app/build.gradle.kts.

# Run from the REPO ROOT (the build context needs ../branding). Fly builds the Dockerfile on
# its remote builder, pushes to registry.fly.io, and releases — one command.
fly deploy --config server/fly.toml --dockerfile Dockerfile

To build the native binary locally (mise supplies GraalVM for JDK 24 via GRAALVM_HOME — see mise.toml — so no manual setup beyond mise install):

cd server
./gradlew :app:nativeCompile           # → app/build/native/nativeCompile/opengb-server
./gradlew :app:nativeRun               # build + run it directly

Reachability metadata

Native-image needs to know about reflection, resources, and dynamic proxies ahead of time. Most of it comes from the GraalVM reachability-metadata repository (enabled in the Gradle config) for our dependencies; the app-specific remainder is committed under server/app/src/main/resources/META-INF/native-image/org.opengb/ in three subdirectories (native-image unions them at build time):

  • from-tests/ — generated by -Pagent test + metadataCopy. Covers what the in-memory test engine exercises: crypto, kotlinx.serialization, the OAuth flow, route handlers. (No real sockets — tracing socket teardown is non-deterministic and would make the CI guard flaky.)
  • from-app/ — generated by tracing a real boot() under the agent. Covers what the test engine can't: the CIO network selector (readHandlerReference), log4j2's JSON-config plugin reflection, Hoplite's config decode, Bootable startup.
  • manual/ — hand-maintained. Holds Bootable's StopSignalHandler, whose reflection happens on the OS-signal shutdown path that the agent can't observe. Edit by hand if shutdown-path reflection regresses.

Regenerate from-tests/ + from-app/ with the helper script. It needs a GraalVM for JDK 24 (for the native-image-agent), which mise provides via GRAALVM_HOME — so with mise active and mise install done, just run:

./scripts/generate-native-metadata.sh

Re-run after dependency or code changes that touch reflection-heavy paths, rebuild the native image, and confirm the binary still boots and serves before committing the regenerated files — a missing entry only fails at runtime, not at build time. CI enforces this: the native-image-metadata job in server-ci.yml regenerates from-tests/ + from-app/ and fails if they differ from what's committed. The reliable correctness check is still the container actually booting:

podman build -t opengb-native -f Dockerfile .          # from the repo root
podman run --rm -p 8080:8080 opengb-native             # then: curl localhost:8080/health

(cd server && ./gradlew :app:nativeRun also works, but only in a shell whose environment has no variables containing ${...} — e.g. exported shell functions from Lmod/environment-modules — which Hoplite's config loader will try to resolve and fail on. The Fly machine's environment is clean.)

Verifying scale-to-zero

After 5 minutes of idle:

fly status   # machine state should be "stopped"
curl -i https://greenbutton.<your-domain>/health
#  first request wakes the machine: ~sub-second with the native image (a JVM image
#  would take ~5-15s on shared-cpu-1x 256MB). Subsequent calls within the keep-alive
#  window respond in ms.

Per-utility secrets

When you register the app with a new utility (Burlington Hydro, etc.), the utility issues a client_id and client_secret. Set them as Fly secrets named per the utility:

fly secrets set \
  OPENGB_UTILITY_BURLINGTON_HYDRO_CLIENTID="..." \
  OPENGB_UTILITY_BURLINGTON_HYDRO_CLIENTSECRET="..."

The server's utilities.conf reads these via Hoplite env substitution.

Continuous deploy from GitHub Actions

.github/workflows/deploy.yml runs on every push to master that touches server/**, branding/**, or the Dockerfile. It runs ./gradlew build (compile + tests) as a gate, then flyctl deploy --config server/fly.toml --dockerfile Dockerfile --remote-only — compiling the GraalVM native image on Fly's remote builder and releasing it, with OPENGB_VERSION stamped to the commit SHA. (Native-image compilation is slower and more memory-hungry than a plain JVM build, so expect longer CI runs — the payoff is the sub-second startup above.)

One-time setup — generate a deploy token and store it as a repo secret:

fly tokens create deploy -x 8760h   # 1 year, scoped to deploy only
# copy the token (starts with "FlyV1 ...")

Then in GitHub: Settings → Secrets and variables → Actions → New repository secret

  • Name: FLY_API_TOKEN
  • Value: paste the token from fly tokens create

The token can be rotated at any time with fly tokens revoke + a fresh fly tokens create. Avoid using your personal fly auth token for CI — deploy tokens are scoped narrower and easier to revoke.

To deploy manually without a commit: Actions tab → "deploy" workflow → "Run workflow".

To roll back: flyctl releases --app open-green-button to find a prior release and its image ref, then flyctl deploy --app open-green-button --image <image-ref> (Fly tags native builds itself — e.g. registry.fly.io/open-green-button:deployment-… — rather than by commit SHA, so copy the ref from flyctl releases output).