.claude/skills/fusionaly-deploy/SKILL.md
Use ONLY when explicitly invoked. Deploys Fusionaly to a new Hetzner server or an existing server, with optional hardening and Cloudflare DNS.
npx skillsauth add karloscodes/fusionaly-oss fusionaly-deployInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Deploy Fusionaly to a new Hetzner server or an existing server you already have. Optionally harden the server and configure Cloudflare DNS.
Principle: Scan first, report status, ask before every invasive action.
Run all checks, collect results:
# hcloud CLI
which hcloud && hcloud version
hcloud context active 2>/dev/null
# flarectl
which flarectl
# Local SSH keys
ls ~/.ssh/*.pub 2>/dev/null
# Hetzner SSH keys (only if hcloud configured)
hcloud ssh-key list 2>/dev/null
Show everything at once so the user sees the full picture:
Environment check:
hcloud CLI: ✓ installed (v1.x.x) / ✗ not installed
Hetzner token: ✓ configured (context: xxx) / ✗ not configured
flarectl: ✓ installed / ✗ not installed
Local SSH keys: ✓ N keys found / ✗ none found
Hetzner SSH keys: ✓ N keys / — (can't check without token)
For each missing item, ask whether to install/configure. One at a time.
hcloud CLI (only needed for new server creation):
brew install hcloud
Hetzner API token (only needed for new server creation). Walk the user through:
You need a Hetzner Cloud API token with read/write permissions.
1. Go to https://console.hetzner.cloud
2. Select your project (or create one)
3. Go to Security → API Tokens
4. Click "Generate API Token"
5. Name it (e.g. "fusionaly-deploy"), select Read & Write
6. Copy the token (you won't see it again)
Then store it:
hcloud context create fusionaly --token <token-user-provided>
# Verify it works
hcloud server-type list > /dev/null && echo "Hetzner token works"
SSH keys — if none exist:
ssh-keygen -t ed25519
flarectl — handled later in Cloudflare section if user opts in.
Do you want to:
1. Create a new server on Hetzner
2. Use an existing server (any provider)
Ask one question at a time. Wait for answer before asking next.
If user declined hcloud install in step 3: explain it's required for this path and stop gracefully.
What domain will Fusionaly run on? (e.g. analytics.example.com)
Fetch from hcloud location list and present the list. Let user pick.
Fetch from hcloud server-type list, filter to shared CPU types (cx/cax), and present with specs and prices. Let user pick.
Fusionaly minimum: 1 vCPU, 512MB RAM, 10GB SSD.
hcloud ssh-key list
If keys exist in Hetzner: let user pick from list. If no Hetzner keys but local keys exist: offer to upload one.
Upload with: hcloud ssh-key create --name <name> --public-key-from-file <path>
Ask:
Verify SSH connectivity:
ssh -o ConnectTimeout=5 -p <port> <user>@<ip> echo ok
If it fails, help troubleshoot (wrong key, port, user).
Do you use Cloudflare for DNS on this domain? (y/n)
If yes:
1. Install flarectl (if not already installed):
brew install cloudflare/cloudflare/flarectl
2. Get a Cloudflare API token. Walk the user through this:
You need a Cloudflare API token with DNS edit permissions.
1. Go to https://dash.cloudflare.com/profile/api-tokens
2. Click "Create Token"
3. Use the "Edit zone DNS" template
4. Under Zone Resources: select the zone for your domain
5. Click "Continue to summary" → "Create Token"
6. Copy the token (you won't see it again)
3. Store and verify the token. Ask the user to paste it, then:
# Export for this session
export CF_API_TOKEN="<token-user-provided>"
# Verify it works — this MUST succeed before proceeding
flarectl zone list
If flarectl zone list fails (401, no zones, etc.) — stop and troubleshoot. Do NOT continue to DNS setup with a broken token.
Show full plan. Adapt based on path:
New Hetzner server:
Ready to provision:
Server: <type> (<specs>)
Location: <location-name> (<location-id>)
Image: Ubuntu 24.04
SSH key: <key-name>
Domain: <domain>
Cloudflare: yes / no
Proceed? This will create a billable server.
Existing server:
Ready to deploy:
Server: <ip> (via <user>@<ip>:<port>)
Domain: <domain>
Cloudflare: yes / no
Proceed?
Only proceed on explicit yes.
Sanitize domain for server name: replace dots with dashes, lowercase (e.g. analytics.example.com → fusionaly-analytics-example-com).
hcloud server create \
--name fusionaly-<sanitized-domain> \
--type <type> \
--location <location> \
--image ubuntu-24.04 \
--ssh-key <key-name>
Capture the public IP from output. Wait for SSH to become available:
# Poll until SSH is ready (max ~60 seconds)
ssh -o ConnectTimeout=5 -o StrictHostKeyChecking=no root@<ip> echo ok
Tell the user: "Server created at <ip>. SSH is ready."
Before telling the user to SSH in, check the URLs work:
curl --head --silent --fail https://raw.githubusercontent.com/karloscodes/server-hardener/main/harden.sh > /dev/null && echo "server-hardener: reachable" || echo "server-hardener: UNREACHABLE"
curl --head --silent --fail https://fusionaly.com/install > /dev/null && echo "fusionaly installer: reachable" || echo "fusionaly installer: UNREACHABLE"
If either URL is unreachable, tell the user before proceeding.
Should I run the server-hardener? This will:
- Create admin user with SSH key
- Disable root login and password auth
- Configure firewall (80/443 open)
- Option for Tailscale (locks SSH to Tailscale only)
- Enable unattended security upgrades
If yes, the hardener is interactive — the user must run it themselves:
The server-hardener is interactive (4 questions). Run this in your terminal:
ssh root@<ip> 'curl -fsSL https://raw.githubusercontent.com/karloscodes/server-hardener/main/harden.sh -o /tmp/harden.sh && sudo bash /tmp/harden.sh'
Wait for user to confirm hardening is complete before proceeding.
After hardening, ask what SSH access method they now have:
ssh admin@<tailscale-ip>ssh -p 2222 admin@<ip>Should I install Fusionaly now? This will:
- Install Docker
- Set up Caddy reverse proxy with SSL
- Configure automatic backups and updates
If yes, the installer is interactive — the user must run it themselves:
The Fusionaly installer is interactive (asks for domain). Run this in your terminal:
ssh <user>@<host> 'curl -fsSL https://fusionaly.com/install | sudo bash'
Use the SSH access from the hardening step (admin user, correct port).
Wait for user to confirm installation is complete.
After the user confirms installation is complete, verify it's actually running:
# Wait a moment for SSL to provision, then check
curl --silent --max-time 10 -o /dev/null -w "%{http_code}" https://<domain>
If you get a 200 or 302: install confirmed.
If connection refused or timeout: SSL may still be provisioning (Let's Encrypt needs DNS to resolve first). Tell the user to wait a few minutes and check https://<domain> in their browser.
Should I create the A record now?
<domain> → <server-ip>
If yes — CF_API_TOKEN should already be exported from the Cloudflare setup step earlier:
# CF_API_TOKEN was exported during Cloudflare setup in Phase 2
flarectl dns create \
--zone <base-domain> \
--name <subdomain> \
--type A \
--content <server-ip>
Where <base-domain> is extracted from the domain (e.g. example.com from analytics.example.com) and <subdomain> is the prefix (e.g. analytics).
If CF_API_TOKEN is not set (e.g. new shell session), re-export it:
export CF_API_TOKEN="<token-from-earlier>"
Adapt based on what was actually done. Only show completed steps.
New Hetzner server:
Done! Here's what was set up:
Server: fusionaly-<domain> (<ip>)
Location: <location-name> (<location-id>)
Type: <type> (<specs>)
OS: Ubuntu 24.04
Hardened: ✓ / ✗ skipped
Tailscale: ✓ / ✗ / — (skipped hardening)
Fusionaly: ✓ installed at <domain> / ✗ skipped
DNS: ✓ A record via Cloudflare / manual setup needed
SSH access: ssh admin@<tailscale-ip> / ssh -p 2222 admin@<ip>
Dashboard: https://<domain>/admin
Existing server:
Done! Here's what was set up:
Server: <ip> (via <user>@<ip>:<port>)
Hardened: ✓ / ✗ skipped
Fusionaly: ✓ installed at <domain> / ✗ skipped
DNS: ✓ A record via Cloudflare / manual setup needed
SSH access: <the access method from hardening>
Dashboard: https://<domain>/admin
Only show what's relevant:
<domain> to <ip>. SSL via Let's Encrypt won't activate until DNS propagates."/opt/fusionaly/storage/backups/."~/.zshrc: export CF_API_TOKEN=\"your-token\""development
Use when user asks about website analytics, traffic, visitors, page views, referrers, or mentions "fusionaly". Queries Fusionaly analytics via SQL API.
development
Use after code changes, before releases, or when testing features - runs the right level of QA based on what changed
content-media
Use when styling Fusionaly UI components, pages, or charts - applies the Fusionaly design system with black/white palette and brand accents
tools
Use when working on Fusionaly OSS or Pro - enforces clean separation between repos, prevents Pro features in OSS, forbids modifying OSS from Pro