Overview

Date: 2026-05-11

Time: 15:39

Overview

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.

Usage Patterns

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.*().

API and Configuration

Required environment variables (read at runtime via os.environ):

Hardcoded constants:

FTL2 automation() parameters used:

Key Behaviors

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.

Relationships

FTL2 modules used:

External systems: Linode (VM hosting), Cloudflare (DNS), Google OAuth, Caddy (reverse proxy + TLS), systemd, firewalld, SELinux, fail2ban.

Related projects: