This guide covers deploying your React Router Gospel Stack application to Fly.io with Docker. Choose the section that matches your database setup.
⚠️ Important: All commands should be run from the monorepo root directory unless specified otherwise.
- Fly CLI installed
- A Fly.io account
- Git repository set up
- GitHub account (for CI/CD)
These steps are common to both PostgreSQL and Turso deployments.
fly auth signup
# or if you already have an account
fly auth loginCreate two apps: one for production and one for staging.
fly apps create react-router-gospel-stack
fly apps create react-router-gospel-stack-stagingNote: Replace
react-router-gospel-stackwith your desired app name. These names must be globally unique on Fly.io.
Check the apps/webapp/fly.toml file and ensure the app name matches your production app:
app = "react-router-gospel-stack" # Must match your production app name
primary_region = "cdg" # Change to your preferred regionImportant: The stack automatically appends a unique suffix during init, which may not match the apps you created. Update the
appvalue if needed.
git initCreate a new GitHub Repository, then add it as the remote:
git remote add origin <ORIGIN_URL>Do not push yet! Complete the setup first.
- Go to Fly.io Personal Access Tokens
- Create a new token
- Add it to your GitHub repo secrets with the name
FLY_API_TOKEN
For PostgreSQL setup details, see the Database Guide.
Create databases for both environments:
# Production database
fly postgres create --name react-router-gospel-stack-db --region cdg
# Staging database
fly postgres create --name react-router-gospel-stack-staging-db --region cdgChoose the appropriate configuration when prompted (Development, Production, or High Availability).
# Attach production database
fly postgres attach react-router-gospel-stack-db --app react-router-gospel-stack
# Attach staging database
fly postgres attach react-router-gospel-stack-staging-db --app react-router-gospel-stack-stagingThis automatically sets the DATABASE_URL secret in your apps.
Before your first deployment, apply the migrations:
# Get a connection string
fly postgres connect --app react-router-gospel-stack-db
# In another terminal, run migrations
DATABASE_URL="<connection-string>" pnpm run db:migrate:applyCommit and push your changes:
git add .
git commit -m "Initial commit"
git push -u origin mainPushing to main triggers the GitHub Action that deploys to production. Pushing to dev deploys to staging.
To scale your PostgreSQL database across regions:
- Read Fly's Multi-region PostgreSQL guide
- Set the
PRIMARY_REGIONenvironment variable:fly secrets set PRIMARY_REGION=cdg --app react-router-gospel-stack
For Turso setup details and concepts, see the Database Guide.
Turso uses embedded replicas for optimal performance—a local SQLite file that syncs with the remote Turso database.
curl -sSfL https://get.tur.so/install.sh | bash
turso auth loginturso org list
turso org switch <organization-name># Create groups
turso group create production-group
turso group create staging-group
# Create databases
turso db create react-router-gospel-stack-db --group production-group
turso db create react-router-gospel-stack-staging-db --group staging-group# Production
turso db show --url react-router-gospel-stack-db
turso db tokens create react-router-gospel-stack-db
# Staging
turso db show --url react-router-gospel-stack-staging-db
turso db tokens create react-router-gospel-stack-staging-dbSave these values for the next step.
With Drizzle:
# Apply directly to remote Turso database
pnpm db:migrate:apply:productionWith Prisma:
Apply migrations manually (Prisma cannot apply automatically to Turso):
turso db shell react-router-gospel-stack-db < packages/infrastructure/prisma/migrations/<migration-folder>/migration.sql
turso db shell react-router-gospel-stack-staging-db < packages/infrastructure/prisma/migrations/<migration-folder>/migration.sqlEmbedded replicas need persistent storage on Fly.io:
# Production volume
fly volumes create libsql_data --region cdg --size 1 --app react-router-gospel-stack
# Staging volume
fly volumes create libsql_data --region cdg --size 1 --app react-router-gospel-stack-stagingNote: Change the
--regionto match yourprimary_regioninfly.toml. Adjust--size(in GB) based on your needs.
Set the environment variables for embedded replicas:
Production:
fly secrets set \
DATABASE_URL="file:/data/libsql/local.db" \
DATABASE_SYNC_URL="<production-database-sync-url>" \
DATABASE_AUTH_TOKEN="<production-auth-token>" \
--app react-router-gospel-stackStaging:
fly secrets set \
DATABASE_URL="file:/data/libsql/local.db" \
DATABASE_SYNC_URL="<staging-database-sync-url>" \
DATABASE_AUTH_TOKEN="<staging-auth-token>" \
--app react-router-gospel-stack-stagingImportant:
DATABASE_URLis the local file path for the embedded replicaDATABASE_SYNC_URLis the remote Turso database URLDATABASE_AUTH_TOKENis the Turso authentication token
Commit and push your changes:
git add .
git commit -m "Initial commit"
git push -u origin mainThe GitHub Action will build and deploy your app to production. Push to dev branch to deploy to staging.
With the above configuration, your app uses Turso's embedded replicas for optimal performance. See the Database Guide for details on how this works.
Learn more: Turso Embedded Replicas on Fly.io
The stack includes GitHub Actions workflows for automated deployments.
- Push to
main→ Deploy to production - Push to
dev→ Deploy to staging - Pull requests → Run tests and linting
Located in .github/workflows/:
deploy-production.yml- Production deploymentdeploy-staging.yml- Staging deployment
Ensure these secrets are set in your GitHub repository:
FLY_API_TOKEN- Your Fly.io API token
Edit the workflow files to customize:
- Test commands
- Build steps
- Deployment regions
- Environment variables
If you prefer to deploy manually without GitHub Actions:
# Build the webapp image
pnpm docker:build:webapp
# Run it locally to test
pnpm docker:run:webapp# Deploy to production
DOCKER_DEFAULT_PLATFORM=linux/amd64 flyctl deploy \
--config ./apps/webapp/fly.toml \
--dockerfile ./apps/webapp/Dockerfile
# Deploy to staging
DOCKER_DEFAULT_PLATFORM=linux/amd64 flyctl deploy \
--config ./apps/webapp/fly.toml \
--app react-router-gospel-stack-staging \
--dockerfile ./apps/webapp/DockerfileOnce your app is running in a single region, you can scale to multiple regions for better global performance.
# Scale your app to multiple regions
fly scale count 3 --region cdg,iad,syd --app react-router-gospel-stackSet the primary region environment variable:
fly secrets set PRIMARY_REGION=cdg --app react-router-gospel-stackThis should match the primary_region in your fly.toml:
primary_region = "cdg"PostgreSQL:
- Follow Fly's Multi-region PostgreSQL guide
- Set up read replicas in additional regions
Turso:
- Turso automatically distributes reads globally
- No additional setup needed—embedded replicas handle it
To test your app from different regions:
- Install ModHeader browser extension
- Add header:
fly-prefer-regionwith value likeiad,cdg,syd - Check the response header
x-fly-regionto confirm which region handled your request
Available regions: Fly.io Regions List
The stack includes a health check endpoint at /healthcheck.
In fly.toml:
[[services.http_checks]]
grace_period = "10s"
interval = "30s"
method = "GET"
timeout = "5s"
path = "/healthcheck"This enables:
- Automatic failover to healthy instances
- Zero-downtime deployments
- Region fallbacks
# Tail production logs
fly logs --app react-router-gospel-stack
# Tail staging logs
fly logs --app react-router-gospel-stack-staging# Check app status
fly status --app react-router-gospel-stack
# View app info
fly info --app react-router-gospel-stackVisit fly.io/dashboard for:
- Metrics and monitoring
- Log exploration
- App configuration
- Billing information
- Check GitHub Actions logs for errors
- Verify all secrets are set correctly
- Ensure
fly.tomlapp name matches your Fly app - Check Fly.io logs:
fly logs --app <app-name>
PostgreSQL:
# Test connection
fly postgres connect --app react-router-gospel-stack-dbTurso:
# Test connection
turso db shell <database-name>
# Verify tokens
turso db tokens validate <token> --db <database-name>This usually means the app name in fly.toml doesn't match the actual Fly app name. Update fly.toml:
app = "your-actual-app-name"If your embedded replica volume has issues:
# List volumes
fly volumes list --app react-router-gospel-stack
# Delete and recreate if needed
fly volumes delete <volume-id> --app react-router-gospel-stack
fly volumes create libsql_data --region cdg --size 1 --app react-router-gospel-stackFor migration workflows, see the Database Guide.
Quick reference:
- PostgreSQL: Migrations are automatically applied during deployment by the GitHub Action
- Turso with Drizzle:
pnpm db:migrate:apply:production - Turso with Prisma: Apply manually using
turso db shell
- Never commit secrets - Use Fly secrets and GitHub secrets
- Use staging environment - Test changes before production
- Enable 2FA - On both Fly.io and GitHub accounts
- Rotate tokens - Regularly rotate API tokens and database credentials
- Review logs - Monitor for suspicious activity
- Use autoscaling - Scale down during low traffic
- Choose appropriate regions - Fewer regions = lower cost
- Monitor usage - Use Fly.io dashboard to track spending
- Optimize images - Smaller Docker images = faster deploys and lower egress
- Set up monitoring and alerting
- Configure custom domains
- Implement automated backups
- Set up staging environment workflow
- Review the Architecture for scaling strategies