Deploy Node.js Apps to Fly.io with Persistent Storage
Fly.io makes deploying Node.js apps incredibly simple. This guide shows you how to deploy apps with persistent storage for SQLite databases and file uploads.
Why Fly.io?
- Auto-generated Dockerfiles - No Docker knowledge needed
- Generous free tier - Up to 3 VMs and 3GB storage
- Built-in SSL - Free HTTPS certificates
- Simple pricing - Pay only for what you use
Prerequisites
# Install flyctl CLI
curl -L https://fly.io/install.sh | sh
# Login to Fly.io
flyctl auth login
Deploy Your App
1. Initialize
# Run in your project directory
flyctl launch
This will:
- Auto-generate an optimized Dockerfile
- Create a
fly.tomlconfiguration file - Auto-select closest region based on your location
- Ask if you want to deploy now (say no for now)
Note on regions: Fly.io automatically chooses the closest region to you (e.g., iad for Washington DC if you're on the US East Coast). This gives the best performance for your location. You can manually change it in fly.toml if needed:
primary_region = "iad" # Washington DC
# primary_region = "lax" # Los Angeles
# primary_region = "lhr" # London
# primary_region = "syd" # Sydney
2. Add Persistent Storage (Optional)
If your app uses SQLite or stores files:
# Create a volume for all persistent data
flyctl volumes create data --size 3 --region iad
Note: Use the same region as your primary_region in fly.toml.
Then add to your fly.toml:
[mounts]
source = "data"
destination = "/data"
Update your app to use /data:
// Use /data for everything persistent
const DB_PATH = process.env.DB_PATH || '/data/app.db';
const UPLOAD_DIR = process.env.UPLOAD_DIR || '/data/uploads';
3. Set Environment Variables
Create a .env file with your secrets:
SMTP_HOST=smtp.example.com
[email protected]
SMTP_PASSWORD=your-password
JWT_SECRET=your-secret-key
DB_PATH=/data/app.db
Pipe it directly to Fly.io secrets:
cat .env | flyctl secrets import
This makes editing easier - just update .env and re-import.
4. Deploy
flyctl deploy
That's it! Your app is live.
Example fly.toml Configuration
app = "your-app-name"
primary_region = "iad" # Auto-selected based on your location
[build]
[env]
NODE_ENV = "production"
PORT = "8080"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop" # Stop when idle (free tier)
auto_start_machines = true # Start on request
min_machines_running = 0
[[vm]]
memory = "512mb" # 256mb works fine for very small apps
cpu_kind = "shared"
cpus = 1
[mounts]
source = "data"
destination = "/data"
RAM sizing guide:
- 256MB - Very small apps (simple APIs, webhooks)
- 512MB - Most Node.js apps (recommended starting point)
- 1GB+ - Heavy workloads (large datasets, image processing)
Health Checks
Add a simple health endpoint:
fastify.get('/health', async (request, reply) => {
return { status: 'ok' };
});
Configure in fly.toml:
[[http_service.checks]]
interval = "30s"
timeout = "5s"
path = "/health"
Monitoring
# View live logs
flyctl logs
# SSH into machine
flyctl ssh console
# Check app status
flyctl status
# Restart app
flyctl apps restart
Increasing Resources
As your app grows, you can easily add more resources:
Add More Storage
# Increase volume size
flyctl volumes extend <volume-id> --size 10
Add More Memory
# Scale to 1GB RAM
flyctl scale memory 1024
Add More CPU
# Scale to 2 CPUs
flyctl scale count 2
Backups
# Create volume snapshot
flyctl volumes snapshots create <volume-id>
# List snapshots
flyctl volumes snapshots list <volume-id>
# Restore from snapshot
flyctl volumes create data --snapshot-id <snapshot-id>
Free Tier Tips
Your fly.toml is already optimized for the free tier:
auto_stop_machines = "stop"- Stops when not in useauto_start_machines = true- Starts on requestmin_machines_running = 0- Allows complete shutdownmemory = "512mb"- Fits within free tier (or use 256mb for smaller apps)
Free tier includes:
- 3 shared-cpu VMs (256MB RAM each)
- 3GB persistent storage
- 160GB outbound data transfer
Common Issues
Port Not Binding Correctly
// IMPORTANT: Bind to 0.0.0.0, not localhost
const PORT = parseInt(process.env.PORT || '3000', 10);
fastify.listen({ port: PORT, host: '0.0.0.0' });
Build Failures
If your build needs more memory:
npx @flydotio/dockerfile --swap=1024
flyctl deploy
Database Migrations
Run migrations via SSH:
flyctl ssh console -C "node dist/migrate.js"
Custom Domain
# Add your domain
flyctl certs create yourdomain.com
# Follow instructions to add DNS record
flyctl certs show yourdomain.com
Complete Example
Here's the full workflow for a support inbox app:
# 1. Initialize (auto-selects closest region)
cd services/support-inbox
flyctl launch --no-deploy
# 2. Create volume (use same region as fly.toml)
flyctl volumes create data --size 3 --region iad
# 3. Add volume mount to fly.toml
# [mounts]
# source = "data"
# destination = "/data"
# 4. Set environment variables
cat .env | flyctl secrets import
# 5. Deploy
flyctl deploy
# 6. Check logs
flyctl logs
Useful Commands
# View all apps
flyctl apps list
# View app info
flyctl info
# View secrets
flyctl secrets list
# Update a secret
echo "NEW_VALUE" | flyctl secrets set SECRET_NAME=-
# View metrics
flyctl dashboard
# Destroy app (when done)
flyctl apps destroy your-app-name
Resources
- Fly.io Documentation
- Fly.io Pricing
- Auto-generated Dockerfile
- Building a Production AI Slack Bot - Real-world example
That's it! Your Node.js app is deployed with:
✅ Auto-generated Docker configuration
✅ Auto-selected optimal region
✅ Persistent storage for data
✅ Free HTTPS certificates
✅ Easy environment variable management
✅ Simple resource scaling when needed
Deploy in under 5 minutes with flyctl launch and flyctl deploy.