Fly.io Security Guide for Vibe Coders

Share

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.

Tool & Platform Guides

Fly.io Security Guide for Vibe Coders