Production deployment to Fly.io with optional CDN.
Note: See
CLAUDE.mdfor local development setup.
- Fly.io account (signup at fly.io)
- Fly CLI installed (
brew install flyctl) - Git repository
- 1Password CLI (for local secrets)
brew install flyctl
fly auth login# From project root
fly launch
# Follow prompts:
# - App name: droodotfoo (or your choice)
# - Region: Choose closest to your users
# - PostgreSQL: Yes (required for wiki, pgvector)
# - Redis: NoThis creates fly.toml configuration.
# Required secrets
fly secrets set SECRET_KEY_BASE=$(mix phx.gen.secret)
fly secrets set PHX_HOST="your-app.fly.dev"
# Optional: GitHub token (for higher API rate limits)
fly secrets set GITHUB_TOKEN="ghp_your_token_here"
# Optional: Spotify integration
fly secrets set SPOTIFY_CLIENT_ID="your_client_id"
fly secrets set SPOTIFY_CLIENT_SECRET="your_client_secret"
# Optional: CDN
fly secrets set CDN_HOST="your-project.pages.dev"
# Required: Blog API token (for /api/posts endpoint)
fly secrets set BLOG_API_TOKEN=$(mix phx.gen.secret)Security Note: The
BLOG_API_TOKENis required for the/api/postsendpoint used by Obsidian/external publishing tools. This endpoint features:
- Bearer token authentication (constant-time comparison)
- Rate limiting: 10 posts/hour, 50 posts/day per IP
- Content validation: max 1MB, slug sanitization, path traversal prevention
- Returns 401 if token not configured (no bypass)
fly deployapp = "droodotfoo"
primary_region = "sea"
[build]
[env]
PHX_SERVER = "true"
[http_service]
internal_port = 4000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 0
processes = ["app"]
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256# Add custom domain
fly certs add droo.foo
# Check certificate status
fly certs show droo.foo
# DNS configuration (add these records):
# A @ <fly-ip-address>
# AAAA @ <fly-ipv6-address>1. Build Static Assets Locally
mix assets.deploy2. Deploy to Cloudflare Pages
# Install Wrangler
npm install -g wrangler
# Deploy
cd priv/static
wrangler pages publish . --project-name=droodotfoo3. Configure Fly.io
fly secrets set CDN_HOST="droodotfoo.pages.dev"Benefits:
- Edge caching worldwide
- Automatic asset optimization
- DDoS protection
- No bandwidth charges from Fly.io
Fly.io automatically monitors your app:
# In fly.toml
[[services.http_checks]]
interval = 10000
grace_period = "5s"
method = "get"
path = "/"
protocol = "http"
timeout = 2000# Stream logs
fly logs
# Recent logs
fly logs --tail=100# View app status
fly status
# View metrics
fly dashboard# Increase memory
fly scale memory 512
# Increase CPUs
fly scale count 2# Add machines in multiple regions
fly scale count 2 --region sea,ord# In fly.toml
[http_service]
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
max_machines_running = 31. App Won't Start
# Check logs
fly logs
# SSH into machine
fly ssh console
# Check secrets
fly secrets list2. 502 Bad Gateway
- Check if app is running:
fly status - Verify
PHX_SERVER=truein environment - Check internal port matches (4000)
3. Slow Deployment
# Clear build cache
fly deploy --no-cache4. Certificate Issues
# Renew certificate
fly certs renew droo.foo
# Check DNS propagation
dig droo.foo# List releases
fly releases
# Rollback to previous
fly releases rollbackCreate .github/workflows/deploy.yml:
name: Deploy to Fly.io
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: superfly/flyctl-actions/setup-flyctl@master
- run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}Setup:
# Get API token
fly auth token
# Add to GitHub secrets:
# Settings → Secrets → Actions → New secret
# Name: FLY_API_TOKEN
# Value: <your-token>Free Tier:
- 3 shared-cpu-1x VMs (256MB)
- 160GB bandwidth/month
- Auto-stop machines when idle
Tips:
- Use
auto_stop_machines = true(stops when idle) - Minimize memory (256MB sufficient)
- Use CDN for static assets
- Cache aggressively
Best Practices:
- Use secrets for all sensitive data (never hardcode)
- Enable force_https
- Keep dependencies updated
- Monitor security advisories
- Rotate secrets periodically (especially BLOG_API_TOKEN)
- Use strong tokens:
mix phx.gen.secretgenerates secure 64-byte values
Headers:
# In content_security_policy.ex
plug :put_secure_browser_headers, %{
"content-security-policy" => "...",
"x-frame-options" => "DENY",
"x-content-type-options" => "nosniff"
}Git-tracked content:
- Blog posts in Git (
priv/posts/*.md) - Resume data in Git (
priv/resume.json) - Configuration in Git
- Secrets in 1Password + Fly.io Secrets
Database:
- PostgreSQL with pgvector (wiki articles, OSRS data, parts catalog)
- Daily backups to MinIO via
Droodotfoo.Wiki.Backup.PostgresWorker(3am) - Run
mix ecto.setupto recreate schema,mix wiki sync --fullto repopulate
Disaster Recovery:
# Redeploy from scratch
fly launch
fly secrets set ... # Restore secrets
fly deploy # Deploy from Git
mix ecto.setup # Create database schema
mix wiki sync --full # Repopulate wiki data