Fly.io Security Guide for Vibe Coders
Published on January 23, 2026 - 11 min read
TL;DR
Fly.io runs your apps at the edge globally, which creates unique security considerations. Always use fly secrets set for sensitive data (never commit to fly.toml), enable private networking between machines, use firewall rules to restrict access, and configure health checks properly. The distributed nature means you need to think about security at every edge location.
Why Fly.io Security Matters for Vibe Coding
Fly.io deploys your applications to edge locations worldwide, running them in Firecracker microVMs. This architecture is fantastic for performance but introduces security considerations that AI coding tools often overlook. When you ask an AI to "deploy my app to Fly.io," it might generate a working configuration but miss critical security hardening.
The edge deployment model means your app runs in multiple locations simultaneously. A security misconfiguration affects every deployment region, potentially exposing your app across the globe.
Secrets Management
Fly.io provides a built-in secrets manager that injects environment variables at runtime. This is the only safe way to handle sensitive data.
Setting Secrets Correctly
# Set secrets via CLI (the secure way)
fly secrets set DATABASE_URL="postgres://user:pass@host:5432/db"
fly secrets set API_KEY="sk-your-secret-key"
fly secrets set JWT_SECRET="your-256-bit-secret"
# Set multiple secrets at once
fly secrets set \
STRIPE_SECRET_KEY="sk_live_..." \
SENDGRID_API_KEY="SG...."
# List secrets (values are hidden)
fly secrets list
# Unset a secret
fly secrets unset OLD_API_KEY
Common AI-Generated Mistake
AI tools sometimes put secrets directly in fly.toml under [env]. This exposes your secrets in version control. Always use fly secrets set instead of adding sensitive values to fly.toml.
What Goes Where
# fly.toml - Safe for non-sensitive config
[env]
NODE_ENV = "production"
LOG_LEVEL = "info"
PUBLIC_API_URL = "https://api.example.com"
# fly secrets - Required for sensitive data
# DATABASE_URL, API_KEY, JWT_SECRET, etc.
Private Networking
Fly.io provides a private IPv6 network (6PN) between your apps. This allows internal services to communicate without exposing endpoints to the internet.
# fly.toml - Configure internal services
[[services]]
internal_port = 8080
protocol = "tcp"
# This exposes the app publicly
[[services.ports]]
port = 443
handlers = ["tls", "http"]
# For internal-only services (databases, workers)
[[services]]
internal_port = 5432
protocol = "tcp"
# No [services.ports] = not publicly accessible
Connecting Between Apps
# Access internal services via .flycast or .internal domains
# From within your Fly app:
const dbUrl = "postgres://user:pass@my-db-app.flycast:5432/db"
// Or using the internal DNS
const redisUrl = "redis://my-redis.internal:6379"
Firewall and Network Security
Configure which connections are allowed using fly.toml and machine-level settings.
# fly.toml - Restrict to specific ports and protocols
[[services]]
internal_port = 8080
protocol = "tcp"
auto_stop_machines = true
auto_start_machines = true
[[services.ports]]
port = 80
handlers = ["http"]
force_https = true # Redirect HTTP to HTTPS
[[services.ports]]
port = 443
handlers = ["tls", "http"]
[[services.http_checks]]
interval = "10s"
timeout = "2s"
path = "/health"
IP Allowlisting
For admin endpoints or internal tools, restrict access by IP:
// Middleware to check allowed IPs
const ALLOWED_IPS = process.env.ALLOWED_IPS?.split(',') || [];
function ipAllowlist(req, res, next) {
// Fly.io sets the client IP in this header
const clientIP = req.headers['fly-client-ip'] ||
req.headers['x-forwarded-for']?.split(',')[0];
if (ALLOWED_IPS.length > 0 && !ALLOWED_IPS.includes(clientIP)) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
}
// Protect admin routes
app.use('/admin', ipAllowlist, adminRouter);
Machine and Volume Security
Fly Machines are the compute units that run your app. Secure their configuration properly.
# fly.toml - Machine configuration
[build]
dockerfile = "Dockerfile"
[deploy]
release_command = "npm run migrate"
[mounts]
source = "data"
destination = "/data"
# Set memory and CPU limits
[[vm]]
cpu_kind = "shared"
cpus = 1
memory_mb = 256
Dockerfile Security
# Use specific versions, not 'latest'
FROM node:20-alpine
# Create non-root user
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
WORKDIR /app
# Copy package files first (better caching)
COPY package*.json ./
RUN npm ci --only=production
# Copy application code
COPY --chown=appuser:appgroup . .
# Switch to non-root user
USER appuser
EXPOSE 8080
CMD ["node", "server.js"]
Health Checks and Monitoring
Proper health checks ensure only healthy machines receive traffic, which is also a security measure against compromised instances.
# fly.toml - Comprehensive health checks
[[services.http_checks]]
interval = "15s"
timeout = "5s"
grace_period = "10s"
method = "GET"
path = "/health"
protocol = "http"
[services.http_checks.headers]
X-Health-Check = "true"
[[services.tcp_checks]]
interval = "15s"
timeout = "2s"
grace_period = "5s"
// Health check endpoint
app.get('/health', (req, res) => {
// Verify the request is a genuine health check
if (req.headers['x-health-check'] !== 'true') {
// Log potential probe attempts
console.warn('Health check without header from:', req.ip);
}
// Check critical dependencies
const checks = {
database: checkDatabaseConnection(),
redis: checkRedisConnection(),
};
const allHealthy = Object.values(checks).every(v => v);
res.status(allHealthy ? 200 : 503).json({ status: allHealthy ? 'healthy' : 'unhealthy', checks });
});
Deployment Security
Secure your deployment pipeline to prevent unauthorized releases.
# .github/workflows/deploy.yml
name: Deploy to Fly.io
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Fly CLI
uses: superfly/flyctl-actions/setup-flyctl@master
- name: Deploy to Fly.io
run: flyctl deploy --remote-only
env:
FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}
Generate a deploy token with minimal permissions:
# Create a deploy-only token
fly tokens create deploy -x 720h
# Store this token in GitHub Secrets as FLY_API_TOKEN
Fly.io Security Checklist
- All secrets stored via
fly secrets set, not in fly.toml - Internal services use private networking (.internal or .flycast)
- HTTPS forced with
force_https = true - Dockerfile uses non-root user
- Health checks configured and working
- Deploy tokens have minimal permissions
- Fly API token stored securely in CI/CD secrets
- Auto-stop enabled for cost and security (idle machines)
- Volumes encrypted at rest (Fly.io default)
- IP allowlisting for admin endpoints
Scaling Security Considerations
When scaling across regions, each instance needs the same security configuration:
# Scale to multiple regions securely
fly scale count 2 --region sea,iad
# Each region gets the same secrets automatically
# But verify they're set correctly
fly secrets list
Logging and Audit
Fly.io provides logging that you should configure for security monitoring:
# View live logs
fly logs
# Ship logs to external service (recommended for production)
fly logs --instance all | your-log-shipper
# Or use the Fly log shipping integration
fly secrets set LOGTAIL_TOKEN="your-token"
Can I use .env files with Fly.io?
You should not commit .env files or include them in your Docker image. Use fly secrets set to inject environment variables at runtime. For local development, use a .env file but add it to .gitignore.
::
How do I connect to a Fly Postgres database securely?
Fly Postgres runs on the private network by default. Connect using the .internal hostname from your app: postgres://user:pass@your-db.internal:5432/db . Never expose Postgres publicly unless you have a specific need and proper firewall rules.
Are Fly.io volumes encrypted?
Yes, Fly.io volumes are encrypted at rest by default. However, you should still implement application-level encryption for highly sensitive data and ensure your app handles data securely in memory.
How do I restrict which users can deploy?
Use Fly.io organizations and teams to manage access. Create deploy tokens with limited permissions for CI/CD, and use personal tokens only for development. Audit token usage in the Fly.io dashboard.
::
What CheckYourVibe Detects
When you scan your Fly.io project with CheckYourVibe, we automatically detect:
- Secrets hardcoded in fly.toml or Dockerfiles
- Missing HTTPS enforcement
- Services exposed publicly that should be internal
- Dockerfiles running as root
- Missing health check configurations
- Overly permissive network configurations
Run npx checkyourvibe scan before deploying to catch these issues automatically.