Date: 2026-05-11
Time: 15:39
This is an FTL2 deployment script for ftl2-servercraft-web, a web application with a Python API backend (served via systemd) and a PWA frontend (served via Caddy). It provides two modes: deploy for full initial provisioning of a Linode VM (including DNS, firewall, SSH hardening, SELinux, fail2ban, and application setup), and redeploy for updating the application on an already-provisioned server. The script demonstrates FTL2 used as infrastructure-as-code for a complete production deployment pipeline.
Invoked as a CLI script with one of two subcommands:
./deploy.py deploy # Full initial provision (Linode VM + DNS + hardening + app)
./deploy.py redeploy # Update app on existing server (wheel, env, caddy, PWA)
The script uses uv run as its shebang, with inline PEP 723 script dependencies — no virtualenv setup needed. FTL2 and linode-api4 are declared as inline deps.
Core FTL2 usage pattern — the automation() async context manager:
async with automation(state_file="state.json") as ftl:
ftl.add_host(hostname='servercraft-web', ansible_host=LINODE_IP,
ansible_user='admin', ansible_become=True)
await ftl["servercraft-web"].copy(src=WHEEL, dest='/home/servercraft/...')
await ftl["servercraft-web"].shell(cmd='...')
await ftl["servercraft-web"].service(name='caddy', state='reloaded', enabled=True)
Hosts are addressed by name via ftl["hostname"], and modules are called as async methods: .copy(), .shell(), .file(), .service(), .user(), .wait_for(). Community modules are called via ftl.community.general.*().
Required environment variables (read at runtime via os.environ):
SECRET_KEY — app secret keyGOOGLECLIENTID / GOOGLECLIENTSECRET — OAuth credentialsALLOWED_EMAILS — optional, defaults to empty stringLINODETOKEN — bound via secretbindings to the Linode moduleCLOUDFLAREAPITOKEN — bound via secret_bindings to the Cloudflare DNS moduleHardcoded constants:
WHEEL — local path to the Python wheel to deployPWA_DIST — local path to the built PWA static filesAPI_KEY — servercraft API key (embedded in script)LINODE_IP — static IP for redeploy mode (45.33.64.95)BEEHIVE_KEY — SSH public key for CI/build server accessFTL2 automation() parameters used:
state_file="state.json" — persists run state for crash recoverysecretbindings — maps module FQCNs to environment variable names for secrets (only in fulldeploy)Two-tier deployment model: deployapp() is a shared function called by both fulldeploy() and redeploy(). This separates infrastructure provisioning (Linode, DNS, firewall, SSH) from application deployment (wheel, systemd, Caddy, PWA).
Host re-registration: During fulldeploy(), the host is first registered as root for initial user creation, then re-registered as admin with ansiblebecome=True for all subsequent tasks. This bootstraps sudo access.
Secret bindings vs. environment variables: Linode and Cloudflare tokens use FTL2's secret_bindings mechanism (auto-injected from env vars into specific modules). App secrets use a manually-constructed .env file with mode='0600'.
PWA deployment is flat: The script globs PWA_DIST/* and copies each file individually — no recursive directory sync. Only top-level files in the dist directory are deployed.
Hardening sequence: Firewall (firewalld with drop zone), SSH hardening (no root login, no passwords), crypto policy (NO-SHA1), SELinux enforcing, disabled unnecessary services (cups, avahi, bluetooth), fail2ban. SSH access restricted to 136.56.0.0/16.
Verification steps: After app deployment, the script runs ss -tlnp and systemctl status to verify the service is listening and running.
uv as package manager on remote: The script installs uv on the remote host if not present, creates a venv, and installs the wheel using uv pip install.
FTL2 modules used:
copy — file/content transfershell — arbitrary commandsservice — systemd service managementfile — directory/permission managementuser — user creationwait_for — port readiness checkcommunity.general.linode_v4 — Linode VM provisioningcommunity.general.cloudflare_dns — DNS record managementExternal systems: Linode (VM hosting), Cloudflare (DNS), Google OAuth, Caddy (reverse proxy + TLS), systemd, firewalld, SELinux, fail2ban.
Related projects:
ftl2-servercraft-web — the application being deployed (provides the wheel)servercraft-pwa — the frontend PWA (provides static dist files)faster-than-light2 — FTL2 itself (the automation framework)jail.local file expected in the working directory for fail2ban config