Date: 2026-05-11
Time: 15:35
This is a production deployment script for catbeez-arcade, a web application hosting browser games at catbeez.com. It uses FTL2's automation API to provision a Linode VPS, harden the server (firewall, SSH, SELinux, fail2ban), configure Caddy as a reverse proxy, and deploy the application with game assets. It demonstrates a complete real-world FTL2 workflow: cloud provisioning → bootstrap → hardening → application deployment → verification.
The script is a standalone uv run script with inline dependency declarations. Run it directly:
./deploy-catbeez.py
# or
uv run deploy-catbeez.py
The core pattern is async with automation(...) as ftl: which creates an FTL2 session with state tracking and secret bindings. All operations flow through the ftl object:
# Cloud provisioning via community module
result = await ftl.community.general.linode_v4(label='catbeez-prod', ...)
linode_ip = result["instance"]["ipv4"][0]
# Host registration and targeting
ftl.add_host(hostname='catbeez-prod', ansible_host=linode_ip, ansible_user='admin', ansible_become=True)
# Module calls on specific hosts
await ftl["catbeez-prod"].copy(content=CADDYFILE, dest='/etc/caddy/Caddyfile', mode='0644')
await ftl["catbeez-prod"].service(name='caddy', state='started', enabled=True)
Key pattern: host re-registration — the script first connects as root to bootstrap an admin user, then re-registers the same hostname with ansibleuser='admin' and ansiblebecome=True for subsequent privileged operations.
State file: state-prod.json — tracks idempotent state across runs.
Secret bindings: Maps module names to environment variables for automatic secret injection:
secret_bindings={"community.general.linode_v4": {"access_token": "LINODE_TOKEN"}}
Requires LINODE_TOKEN environment variable to be set.
Constants:
GAMES — list of game names to deploy (each needs .html, .js, .wasm)GAMES_SRC — local path to game build artifactsWHEEL — local path to the application's Python wheelBEEHIVE_KEY — SSH public key for the CI/build serverroot, then switches to admin with sudo. This is necessary because the admin user doesn't exist on a fresh Linode image.136.56.0.0/16 can SSH in; HTTP/HTTPS are open. Uses firewalld's drop zone to silently discard all other traffic.becomeuser for non-root tasks: Application installation runs as the catbeez service user via becomeuser='catbeez', not as root.copy calls for 9 games), not as an archive.ftl2.automation) — the orchestration framework; provides addhost, waitfor, host indexing (ftl["hostname"]), and module dispatch.community.general.linode_v4) — FQCN-style module for Linode provisioning, uses secret bindings for API token.user, shell, copy, file, service, wait_for.catbeez.com to localhost:8000.uv pip install into a venv.