Fly.io Deployment Example¶
Complete example of deploying a Rails application with Navigator on Fly.io, including machine suspension, multi-region setup, and production optimizations.
Project Structure¶
myapp/
├── Dockerfile # Fly.io container
├── fly.toml # Fly.io configuration
├── config/
│ ├── navigator.yml # Navigator configuration
│ └── database.yml # Rails database config
├── app/ # Rails application
├── Gemfile
└── .github/
└── workflows/
└── deploy.yml # CI/CD workflow
Complete Example Files¶
Dockerfile¶
FROM ruby:3.2-slim
# Install system dependencies
RUN apt-get update && apt-get install -y \
build-essential \
libpq-dev \
nodejs \
npm \
&& rm -rf /var/lib/apt/lists/*
# Copy Navigator binary from Docker Hub
COPY --from=samruby/navigator:latest /navigator /usr/local/bin/navigator
RUN chmod +x /usr/local/bin/navigator
# Set up Rails app
WORKDIR /app
COPY Gemfile Gemfile.lock ./
RUN bundle install
# Copy application code
COPY . .
# Precompile assets
RUN bundle exec rails assets:precompile
# Create required directories
RUN mkdir -p tmp/pids log storage
# Expose port
EXPOSE 3000
# Start Navigator
CMD ["navigator", "/app/config/navigator.yml"]
fly.toml¶
app = "myapp-production"
primary_region = "ord"
kill_signal = "SIGTERM"
kill_timeout = "60s"
[build]
[deploy]
release_command = "bundle exec rails db:migrate"
[env]
RAILS_ENV = "production"
LOG_LEVEL = "info"
[http_service]
internal_port = 3000
force_https = true
auto_stop_machines = true
auto_start_machines = true
min_machines_running = 1
processes = ["app"]
[[http_service.checks]]
grace_period = "10s"
interval = "30s"
method = "GET"
path = "/up"
port = 3000
timeout = "5s"
type = "http"
# VM configuration
[[vm]]
cpu_kind = "shared"
cpus = 2
memory_mb = 1024
# Persistent volumes
[[mounts]]
source = "myapp_storage"
destination = "/app/storage"
initial_size = "10GB"
# Secrets (set via flyctl secrets)
# DATABASE_URL
# SECRET_KEY_BASE
# REDIS_URL
Navigator Configuration¶
config/navigator.yml
server:
listen: 3000
public_dir: /app/public
pools:
max_size: 15 # Optimized for 1GB machine
idle_timeout: 600 # 10 minutes
start_port: 4000
# Machine suspension for cost optimization
suspend:
enabled: true
idle_timeout: 900 # 15 minutes - conservative for production
check_interval: 60 # Check every minute
grace_period: 120 # 2 minute grace period
# Serve static files directly
static:
directories:
- path: /assets/
root: /app/public/assets/
cache: 31536000 # 1 year for fingerprinted assets
- path: /images/
root: /app/public/images/
cache: 86400 # 1 day for images
- path: /favicon.ico
root: /app/public/favicon.ico
cache: 86400
extensions: [css, js, png, jpg, gif, ico, svg, woff, woff2, map]
try_files:
enabled: true
suffixes: [".html", ".htm"]
fallback: rails
# Fly-Replay for optimal regional routing
routes:
fly_replay:
# Route PDF generation to primary region (more CPU)
- path: "^/api/pdf/"
region: ord
status: 307
# Route image processing to primary region
- path: "^/api/images/"
region: ord
status: 307
# Route European traffic to Frankfurt
- path: "^/eu/"
region: fra
status: 307
# Route Asian traffic to Tokyo
- path: "^/asia/"
region: nrt
status: 307
applications:
global_env:
RAILS_ENV: production
RAILS_LOG_TO_STDOUT: "1"
RAILS_SERVE_STATIC_FILES: "false"
DATABASE_URL: "${DATABASE_URL}"
SECRET_KEY_BASE: "${SECRET_KEY_BASE}"
REDIS_URL: "${REDIS_URL}"
# Performance settings
RAILS_MAX_THREADS: "15"
WEB_CONCURRENCY: "1"
# Feature flags
RAILS_FORCE_SSL: "true"
RAILS_LOG_LEVEL: "info"
tenants:
- name: production
path: /
working_dir: /app
# Managed processes for background jobs
managed_processes:
- name: sidekiq
command: bundle
args: [exec, sidekiq]
working_dir: /app
env:
RAILS_ENV: production
SIDEKIQ_CONCURRENCY: "10"
auto_restart: true
start_delay: 5
Step-by-Step Deployment¶
1. Prerequisites¶
# Install flyctl
curl -L https://fly.io/install.sh | sh
# Login to Fly.io
flyctl auth login
# Verify installation
flyctl version
2. Create Rails Application¶
# Create new Rails app (or use existing)
rails new myapp --database=postgresql
cd myapp
# Add Navigator configuration
mkdir -p config
# Copy navigator.yml from above
3. Set Up Fly.io App¶
# Create Fly app
flyctl apps create myapp-production
# Create PostgreSQL database
flyctl postgres create myapp-production-db --region ord
# Attach database
flyctl postgres attach myapp-production-db --app myapp-production
# Create Redis (optional)
flyctl redis create myapp-production-redis --region ord
flyctl redis connect myapp-production-redis --app myapp-production
# Create volume for storage
flyctl volumes create myapp_storage --region ord --size 10
4. Configure Secrets¶
# Generate and set Rails secret
SECRET_KEY=$(rails secret)
flyctl secrets set SECRET_KEY_BASE="$SECRET_KEY"
# Set other secrets
flyctl secrets set RAILS_MASTER_KEY="$(cat config/master.key)"
# Optional monitoring
flyctl secrets set NEW_RELIC_LICENSE_KEY="your-license-key"
flyctl secrets set HONEYBADGER_API_KEY="your-api-key"
5. Deploy Application¶
# Create Dockerfile and fly.toml (from examples above)
# Initial deployment
flyctl deploy
# Monitor deployment
flyctl logs
# Check status
flyctl status
6. Verify Deployment¶
# Check app is running
curl https://myapp-production.fly.dev/up
# Check Navigator status
flyctl ssh console -C "ps aux | grep navigator"
# Check Rails processes
flyctl ssh console -C "ps aux | grep rails"
# View logs
flyctl logs --app myapp-production
Multi-Region Deployment¶
Deploy to Multiple Regions¶
# Add regions
flyctl regions add ord fra nrt
# Scale per region
flyctl scale count 2 --region ord # Primary region - 2 machines
flyctl scale count 1 --region fra # Secondary regions - 1 machine each
flyctl scale count 1 --region nrt
# Check distribution
flyctl status
flyctl machine list
Regional Configuration¶
# Update navigator.yml for regional routing
routes:
fly_replay:
# Geographic routing examples
- path: "^/americas/"
region: ord
status: 307
- path: "^/europe/"
region: fra
status: 307
- path: "^/apac/"
region: nrt
status: 307
# Load-balancing heavy operations
- path: "^/api/heavy/"
region: ord # Route to primary region with more resources
status: 307
Production Optimizations¶
Resource Optimization¶
fly.toml - Production VM
[[vm]]
cpu_kind = "performance" # Better performance than shared
cpus = 4
memory_mb = 2048
navigator.yml - Production pools
pools:
max_size: 25 # More processes for higher traffic
idle_timeout: 1800 # 30 minutes - keep processes alive longer
start_port: 4000
Monitoring Setup¶
navigator.yml - Add monitoring
applications:
global_env:
# Performance monitoring
NEW_RELIC_LICENSE_KEY: "${NEW_RELIC_LICENSE_KEY}"
HONEYBADGER_API_KEY: "${HONEYBADGER_API_KEY}"
# Logging
RAILS_LOG_LEVEL: "info"
LOG_LEVEL: "info"
Database Optimizations¶
navigator.yml - Database config
applications:
global_env:
# Connection pool settings
DATABASE_POOL: "25"
DATABASE_TIMEOUT: "5000"
# Performance settings
RAILS_CACHE_STORE: "redis_cache_store"
SESSION_STORE: "redis_session_store"
CI/CD Integration¶
GitHub Actions Workflow¶
.github/workflows/deploy.yml
name: Deploy to Fly.io
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_PASSWORD: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: ruby/setup-ruby@v1
with:
bundler-cache: true
- name: Setup test database
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
RAILS_ENV: test
run: |
bundle exec rails db:create
bundle exec rails db:migrate
- name: Run tests
env:
DATABASE_URL: postgres://postgres:postgres@localhost:5432/test
RAILS_ENV: test
run: bundle exec rails test
- name: Validate Navigator config
run: |
docker run --rm -v $(pwd):/app samruby/navigator:latest /navigator --validate /app/config/navigator.yml
deploy:
if: github.ref == 'refs/heads/main'
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy to production
run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
- name: Verify deployment
run: |
sleep 30 # Wait for deployment
curl -f https://myapp-production.fly.dev/up
Monitoring and Alerting¶
Health Monitoring Script¶
scripts/health-check.sh
#!/bin/bash
# Health monitoring script
APP_NAME="myapp-production"
HEALTH_URL="https://${APP_NAME}.fly.dev/up"
SLACK_WEBHOOK="${SLACK_WEBHOOK_URL}"
check_health() {
if ! curl -f --max-time 10 "$HEALTH_URL" > /dev/null 2>&1; then
echo "Health check failed for $APP_NAME"
# Send alert
curl -X POST -H 'Content-type: application/json' \
--data "{\"text\":\"🚨 Health check failed for $APP_NAME\"}" \
"$SLACK_WEBHOOK"
exit 1
fi
echo "Health check passed for $APP_NAME"
}
check_machines() {
running_machines=$(flyctl machine list --app "$APP_NAME" --json | jq -r '.[] | select(.state == "started") | .id' | wc -l)
if [ "$running_machines" -eq 0 ]; then
echo "No running machines for $APP_NAME"
exit 1
fi
echo "$running_machines machines running for $APP_NAME"
}
# Run checks
check_health
check_machines
echo "All checks passed for $APP_NAME"
Automated Monitoring¶
# Set up cron job for monitoring
crontab -e
# Add line:
*/5 * * * * /path/to/scripts/health-check.sh
Troubleshooting¶
Common Deployment Issues¶
# Check deployment logs
flyctl logs --app myapp-production
# Check specific machine
flyctl machine list
flyctl logs --app myapp-production --instance machine-id
# SSH into machine for debugging
flyctl ssh console --app myapp-production
# Check Navigator status inside machine
ps aux | grep navigator
journalctl -u navigator -n 50
Performance Issues¶
# Monitor resource usage
flyctl metrics --app myapp-production
# Check machine specs
flyctl machine show machine-id
# Scale up if needed
flyctl scale vm performance-2x --memory 2048
flyctl scale count 3
Database Issues¶
# Check database connection
flyctl postgres connect myapp-production-db
# Monitor database performance
flyctl postgres list
flyctl postgres show myapp-production-db
Cost Management¶
Optimize for Cost¶
# Aggressive suspension for cost savings
suspend:
enabled: true
idle_timeout: 300 # 5 minutes - aggressive
check_interval: 30 # Frequent checks
# Use shared CPU for cost savings
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 512
[machines]
min_machines_running = 0 # Allow full suspension
Monitor Costs¶
# Check usage and billing
flyctl dashboard
# Monitor per-app costs
flyctl usage --app myapp-production
# Optimize regions (some are more expensive)
flyctl regions list
flyctl regions remove expensive-region
Best Practices¶
1. Environment Separation¶
# Separate apps for different environments
flyctl apps create myapp-production # Production
flyctl apps create myapp-staging # Staging
flyctl apps create myapp-review-pr123 # Review apps
2. Database Management¶
# Use separate databases
flyctl postgres create myapp-prod-db # Production
flyctl postgres create myapp-staging-db # Staging
3. Secrets Management¶
# Environment-specific secrets
flyctl secrets set --app myapp-production SECRET_KEY_BASE="prod-secret"
flyctl secrets set --app myapp-staging SECRET_KEY_BASE="staging-secret"
4. Monitoring¶
# Set up comprehensive monitoring
flyctl secrets set --app myapp-production NEW_RELIC_LICENSE_KEY="..."
flyctl secrets set --app myapp-production HONEYBADGER_API_KEY="..."
5. Scaling Strategy¶
# Start small, scale as needed
flyctl scale count 1 # Start with 1 machine
flyctl scale count 3 # Scale up for traffic
flyctl scale vm shared-cpu-2x # Upgrade machine size