Fly.io

Deploy your Strapi v5 headless CMS to Fly.io's global edge infrastructure. This guide covers Docker configuration, PostgreSQL setup, persistent storage for media uploads, and production best practices for full-stack developers.

Fly.io logo

What Is Fly.io?

Fly.io is a platform-as-a-service that runs Docker containers as Firecracker micro-VMs distributed globally across edge locations. According to Fly.io's official architecture documentation, the platform converts Docker containers into Firecracker micro-VMs—lightweight virtualized containers that provide full hardware virtualization rather than just container isolation.

This approach delivers strong VM-level security isolation with millisecond startup times enabling rapid scaling, along with efficient resource usage where many micro-VMs run on single physical hosts with minimal overhead.

Why Integrate Fly.io with Strapi

Building with Strapi's headless CMS gives you API-first content management. Deploying to Fly.io solves the infrastructure complexity that typically comes with self-hosting. Here's why this combination works well for full-stack developers:

  • Global edge performance: API responses benefit from anycast routing that serves content from the nearest region—critical when your Strapi REST API powers applications with international users.
  • Git-integrated deployments: The flyctl CLI enables Docker-based deployment workflows that integrate directly with Git repositories. Developers maintain deployment configurations through fly.toml files stored in version control alongside application code.
  • Automatic HTTPS: SSL certificates provision and renew automatically with zero configuration, eliminating certificate management from your operational checklist.
  • Managed PostgreSQL: Production databases with automatic failover and encrypted backups, properly configured for Strapi's requirements.
  • Cost-effective scaling: Usage-based pricing starts at approximately $5/month, with auto-stop features that reduce costs during low-traffic periods.
  • Persistent storage options: Native volume support for media uploads, plus straightforward integration with Cloudinary or S3 when you need CDN-backed asset delivery.

How to Integrate Fly.io with Strapi

Follow these steps to deploy your Strapi v5 application to Fly.io with PostgreSQL, persistent storage, and production-ready configuration.

Prerequisites

Before starting, ensure you have:

  • A Fly.io account (create one at fly.io)
  • Docker installed locally for container builds
  • Node.js 20, 22, or 24 (Strapi v5 requires Active LTS or Maintenance LTS versions only)
  • An initialized Strapi v5 project
  • Git for version control

Step 1: Install and Authenticate the Fly.io CLI

The flyctl command-line tool manages your entire deployment workflow. Install it using the appropriate command for your operating system:

# macOS and Linux
curl -L https://fly.io/install.sh | sh

# Windows (PowerShell)
iwr https://fly.io/install.ps1 -useb | iex

After installation, authenticate with your Fly.io account:

flyctl auth login

This opens a browser window for authentication. Once you complete the authentication process, your terminal session has access to deploy and manage applications.

Step 2: Create the Dockerfile

Strapi v5 deployments require a multi-stage Docker build that separates the admin panel compilation from the production runtime. Create a Dockerfile in your project root:

# Build stage
FROM node:18-alpine AS builder
WORKDIR /app

# Install dependencies
COPY package*.json ./
RUN npm install

# Copy application files and build admin panel
COPY . .
RUN npm run build

# Production stage
FROM node:18-alpine AS production
WORKDIR /app

# Copy built application from builder
COPY --from=builder /app .

# Remove dev dependencies for smaller image
RUN npm prune --production

# Create uploads directory with correct permissions
RUN mkdir -p /app/public/uploads && chown -R node:node /app/public/uploads

# Expose Strapi port
EXPOSE 1337

# Start Strapi in production mode
CMD ["npm", "run", "start"]

The multi-stage approach keeps your production image lean by excluding build-time dependencies. In the production stage, the npm prune --production command removes development packages that aren't needed at runtime, reducing the final Docker image size and attack surface.

Step 3: Configure fly.toml

The fly.toml file defines your application's deployment configuration. Create this file in your project root:

app = "your-strapi-app-name"
primary_region = "iad"

[build]
  dockerfile = "Dockerfile"

[mounts]
  source = "strapi_uploads"
  destination = "/app/public/uploads"

[env]
  NODE_ENV = "production"
  HOST = "0.0.0.0"
  PORT = "1337"

[[services]]
  internal_port = 1337
  protocol = "tcp"
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 1

  [[services.ports]]
    handlers = ["http"]
    port = 80
    force_https = true

  [[services.ports]]
    handlers = ["tls"]
    port = 443

  [[services.http_checks]]
    interval = 10000
    timeout = 5000
    grace_period = "30s"
    method = "GET"
    path = "/_health"

[[vm]]
  cpu_kind = "shared"
  cpus = 1
  memory_mb = 2048

A few configuration points that matter:

  • HOST = "0.0.0.0" is required—Strapi must bind to all interfaces for container networking to work correctly.
  • The 30-second grace_period gives Strapi time to initialize database connections and compile content-type schemas before health checks begin.
  • memory_mb = 2048 is the minimum recommended for production Strapi instances; insufficient memory causes most deployment failures.

Step 4: Initialize the Fly.io Application

With your configuration files ready, initialize the application without deploying immediately:

flyctl launch --no-deploy

The --no-deploy flag prevents immediate deployment, allowing configuration of database and secrets before the first deployment.

Step 5: Create and Attach PostgreSQL

Fly.io provides managed PostgreSQL clusters. Create one for your Strapi application:

flyctl postgres create --name your-strapi-db

The CLI prompts you for configuration options. For development, a single-node cluster works fine. For production, select the high-availability option with a leader-replica configuration, as documented in Fly.io's Postgres cluster creation process.

Save the credentials immediately—Fly.io displays the PostgreSQL password once during cluster creation and cannot retrieve it later.

Attach the database to your application:

flyctl postgres attach your-strapi-db --app your-strapi-app-name

Attaching the database automatically creates a DATABASE_URL secret in your application's environment.

Step 6: Create the Persistent Volume

Without persistent storage, uploaded media files disappear when your application redeploys. Create a volume in the same region as your application:

flyctl volumes create strapi_uploads --region iad --size 10

The volume name must match the source field in your fly.toml mounts configuration exactly. Start with 10GB—you can extend volumes later but cannot shrink them.

Step 7: Configure Environment Secrets

Strapi's security configuration requires several cryptographic secrets for production deployments. According to the official documentation, these mandatory environment variables include APP_KEYS (comma-separated list of signing keys), API_TOKEN_SALT (for API token generation), ADMIN_JWT_SECRET (for admin panel authentication), JWT_SECRET (for general JWT operations), and TRANSFER_TOKEN_SALT (for content transfer tokens).

Generate secure random values using a cryptographically secure method (such as openssl rand -base64 32) and set them using fly secrets set to ensure they are injected as environment variables without being stored in version control.

# Generate secrets (run each separately)
openssl rand -base64 32

# Set all required secrets
flyctl secrets set \
  APP_KEYS="base64-key-1,base64-key-2,base64-key-3,base64-key-4" \
  API_TOKEN_SALT="your-random-salt" \
  ADMIN_JWT_SECRET="your-admin-jwt-secret" \
  JWT_SECRET="your-jwt-secret" \
  TRANSFER_TOKEN_SALT="your-transfer-salt"

Each secret serves a specific purpose in Strapi's security model:

  • APP_KEYS: Session encryption (comma-separated list of keys).
  • API_TOKEN_SALT: Generates API tokens for programmatic access.
  • ADMIN_JWT_SECRET: Authenticates Admin Panel sessions.
  • JWT_SECRET: Signs tokens for user authentication.
  • TRANSFER_TOKEN_SALT: Required for Strapi v5's content transfer features.

Step 8: Update Strapi Database Configuration

Modify your config/database.js to work with Fly.io's PostgreSQL:

module.exports = ({ env }) => ({
  connection: {
    client: 'postgres',
    connection: {
      connectionString: env('DATABASE_URL'),
      ssl: env.bool('DATABASE_SSL', false) && {
        rejectUnauthorized: env.bool('DATABASE_SSL_REJECT_UNAUTHORIZED', true),
      },
    },
    pool: {
      min: 0,
      max: 10,
      acquireTimeoutMillis: 30000,
      createTimeoutMillis: 30000,
      destroyTimeoutMillis: 5000,
      idleTimeoutMillis: 30000,
      reapIntervalMillis: 1000,
      createRetryIntervalMillis: 100
    },
    debug: false,
  },
});

The pool.min: 0 setting is critical for Strapi on Fly.io. Fly.io uses PgBouncer for connection pooling, and according to the official Strapi database configuration documentation, setting the minimum pool size to 0 prevents issues with idle connections being killed in containerized environments. Setting a minimum above zero can cause connection exhaustion in these environments, as documented in Strapi deployment best practices.

Install the PostgreSQL client if you haven't already:

npm install pg --save

Step 9: Deploy Your Application

With everything configured, deploy your Strapi application:

flyctl deploy

The deployment process builds your Docker image, pushes it to Fly.io's registry, creates VM instances in your specified region, mounts the persistent volume, injects secrets as environment variables, executes health checks, and routes traffic only to healthy instances.

Monitor the deployment:

flyctl logs

Once health checks pass, open your application:

flyctl open

Navigate to https://your-app-name.fly.dev/admin to create your first administrator account and start building your content types.

Project Example: Travel Blog with Next.js Frontend

Let's walk through a practical implementation: a travel blog where Strapi manages content and a Next.js frontend consumes the API. This pattern is common for headless CMS architectures where content teams and developers work independently.

Content Type Structure

In your Strapi Admin Panel, create three content types:

Article Collection Type:

  • title (Text, required) - The article headline displayed in content lists and detail pages.
  • slug (UID, based on title) - URL-friendly identifier generated from the title for SEO-optimized URLs.
  • content (Rich Text with Blocks editor) - Main article body supporting formatted text, images, and dynamic content blocks.
  • excerpt (Text, max 200 characters) - Summary text for preview cards and metadata.
  • coverImage (Media, single image) - Featured image displayed in article listings and detail pages.
  • publishedAt (DateTime) - Publication timestamp controlling content visibility and sorting.
  • author (Relation to Author) - Link to the author collection establishing content ownership.
  • destinations (Relation to Destination, many) - Multi-reference field linking articles to related travel destinations.

Author Collection Type:

  • name (Text, required).
  • bio (Text).
  • avatar (Media, single image).
  • articles (Relation to Article, one-to-many).

Destination Collection Type:

  • name (Text, required).
  • slug (UID).
  • country (Text).
  • description (Rich Text).
  • featuredImage (Media).
  • articles (Relation to Article, many-to-many).

API Permissions Configuration

In Settings > Users & Permissions > Roles, configure the Public role to allow find and findOne operations on your content types. This enables unauthenticated API access for your frontend.

Querying Content from Next.js

Create a utility function to fetch content from your deployed Strapi instance:

// lib/strapi.ts
const STRAPI_URL = process.env.NEXT_PUBLIC_STRAPI_URL || 'https://your-strapi-app.fly.dev';

interface StrapiResponse<T> {
  data: T;
  meta: {
    pagination?: {
      page: number;
      pageSize: number;
      pageCount: number;
      total: number;
    };
  };
}

export async function fetchAPI<T>(
  path: string,
  options: RequestInit = {}
): Promise<StrapiResponse<T>> {
  const response = await fetch(`${STRAPI_URL}/api${path}`, {
    headers: {
      'Content-Type': 'application/json',
      ...options.headers,
    },
    ...options,
  });

  if (!response.ok) {
    throw new Error(`Strapi API error: ${response.status}`);
  }

  return response.json();
}

export async function getArticles() {
  return fetchAPI<Article[]>('/articles?populate=coverImage,author');
}

export async function getArticleBySlug(slug: string) {
  return fetchAPI<Article[]>(
    `/articles?filters[slug][$eq]=${slug}&populate=coverImage,author,destinations`
  );
}

export async function getDestinations() {
  return fetchAPI<Destination[]>('/destinations?populate=featuredImage');
}

Dynamic Article Pages

Implement dynamic routing with static generation for individual articles:

// app/articles/[slug]/page.tsx
import { getArticleBySlug, getArticles } from '@/lib/strapi';
import { notFound } from 'next/navigation';

export async function generateStaticParams() {
  const { data: articles } = await getArticles();
  
  return articles.map((article) => ({
    slug: article.attributes.slug,
  }));
}

export default async function ArticlePage({
  params,
}: {
  params: { slug: string };
}) {
  const { data: articles } = await getArticleBySlug(params.slug);
  
  if (!articles || articles.length === 0) {
    notFound();
  }

  const article = articles[0];

  return (
    <article>
      <h1>{article.attributes.title}</h1>
      <p>{article.attributes.content}</p>
    </article>
  );
}

This pattern implements Static Site Generation (SSG) where Next.js pre-builds pages at deployment time by querying your Strapi API (deployed on Fly.io) and generating HTML for each article slug. Benefits include:

  • SEO optimization: Pre-rendered HTML available to search engines immediately.
  • Performance: Fast page loads from static content with no runtime database queries.
  • Scalability: Decouples frontend performance from backend load.

Deployment Configuration

For production deployments, add these environment variables to your Next.js project:

NEXT_PUBLIC_STRAPI_URL=https://your-strapi-app.fly.dev
STRAPI_API_TOKEN=your-api-token-from-strapi-admin

According to the Strapi deployment documentation, your API token is generated in the Strapi admin panel at /admin/settings/api-tokens after deploying to Fly.io.

The Strapi integrations page documents additional frontend framework combinations if you prefer React, Vue, or other options.

Strapi Open Office Hours

If you have questions about deploying Strapi v5 to Fly.io or want to discuss your specific architecture, join us at Strapi's Discord Open Office Hours, Monday through Friday, from 12:30 pm to 1:30 pm CST: Strapi Discord.

For more details, visit the Strapi documentation and Fly.io documentation.

Frequently Answered Questions

Production Strapi deployments on Fly.io typically cost between $48-68 per month under standard configurations. This breaks down to approximately $10-30 for compute (depending on machine size and CPU/RAM allocation), $38 for the entry-level managed PostgreSQL cluster, and $0.15 per GB per month for persistent volume storage. Additional costs may apply for bandwidth at $0.02 per GB. Fly.io's auto-stop feature can reduce costs during low-traffic periods by stopping machines when not in use, and annual reservations provide up to 40% discount on compute charges for committed workloads.

While technically possible with persistent volumes, SQLite is strongly discouraged for production Strapi deployments. According to the official Strapi v5 deployment documentation, SQLite is recommended mainly for development or prototyping due to its concurrency limitations, lack of horizontal scaling capabilities, and risks of data corruption. PostgreSQL handles concurrent requests properly and provides the high availability options that production applications require. Fly.io's managed PostgreSQL service starts at $38/month and provides the robust database infrastructure that production Strapi deployments need.

Uploaded images disappear after redeployment because Fly.io uses ephemeral filesystems by default. Without persistent storage configuration, all files stored on the instance's local filesystem are deleted when the application is redeployed or the machine restarts. To persist uploaded media files across redeployments, you must mount a Fly.io volume to your application.

Configure persistent volumes by adding a [mounts] section to your fly.toml pointing to /app/public/uploads, then create the volume with flyctl volumes create. Alternatively, use cloud storage providers like AWS S3 or Cloudinary, which eliminates volume management entirely.

Fly.io integrates with GitHub Actions for automated deployments through the official flyctl GitHub Action. First, generate a deploy token with fly tokens create deploy and store it as a GitHub secret named FLY_API_TOKEN. Then create a workflow file that uses the official action to run flyctl deploy --remote-only on pushes to your main branch. The action handles authentication automatically using your FLY_API_TOKEN secret. Ensure your fly.toml configuration file is committed to the repository, as it's required for the deployment process.

Strapi requires a minimum of 2GB VM memory for stable production operation on Fly.io. Configure this in your fly.toml under [[vm]] with memory_mb = 2048. Additionally, set the Node.js heap size to match by adding NODE_OPTIONS = "--max-old-space-size=2048" to your environment variables. According to the research, insufficient memory is the primary cause of Strapi deployment failures on Fly.io, manifesting as random crashes, failed health checks, or deployments that reset unexpectedly. The research identifies memory as the most critical resource constraint across deployment, scaling, and troubleshooting scenarios.