Overview

Date: 2026-05-11

Time: 15:38

Overview

This is a production deployment script for expert-service, a web application hosted on Linode behind Caddy as a reverse proxy. It uses FTL2's automation context manager to provision infrastructure (Linode VM, Cloudflare DNS), harden the server (firewall, SSH, SELinux, fail2ban), and deploy the application (Python wheel via uv, systemd service, SQLite database with daily backups). It serves as a real-world example of using FTL2 for both initial provisioning and ongoing redeployment.

Usage Patterns

Two entry points via CLI:


./deploy.py deploy     # Full initial provision: Linode + DNS + hardening + app
./deploy.py redeploy   # Update app on existing server: wheel + env + restart

The script uses uv run (PEP 723 inline script metadata) to self-bootstrap dependencies — no virtual environment setup needed. FTL2 and linode-api4 are declared inline.

Core FTL2 pattern — the async with automation(...) context manager:


async with automation(state_file="state.json", secret_bindings={...}) as ftl:
    ftl.add_host(hostname=HOST, ansible_host=ip, ansible_user='admin', ansible_become=True)
    await ftl[HOST].copy(src=..., dest=..., owner=..., mode=...)
    await ftl[HOST].shell(cmd='...')
    await ftl[HOST].service(name='...', state='restarted', enabled=True)

Hosts are addressed by name via ftl[HOST], and modules are called as async methods: .copy(), .shell(), .file(), .service(), .user().

Community modules are called on the ftl object directly:


result = await ftl.community.general.linode_v4(label=..., type=..., region=..., image=..., state='present')
await ftl.community.general.cloudflare_dns(zone=..., record=..., type='A', value=ip, state='present')

API and Configuration

Environment variables required:

Secret bindings (FTL2 feature — maps module parameters to env vars):

State file: state.json — persists host IP and connection info between deploy and redeploy runs.

Hardcoded paths:

Key Behaviors

Relationships