These integration guides are not official documentation and the Strapi Support Team will not provide assistance with them.
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.
Sign up for the Logbook, Strapi's Monthly newsletter
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
flyctlCLI enables Docker-based deployment workflows that integrate directly with Git repositories. Developers maintain deployment configurations throughfly.tomlfiles 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 | iexAfter installation, authenticate with your Fly.io account:
flyctl auth loginThis 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 = 2048A 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_periodgives Strapi time to initialize database connections and compile content-type schemas before health checks begin. memory_mb = 2048is 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-deployThe --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-dbThe 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-nameAttaching 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 10The 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 --saveStep 9: Deploy Your Application
With everything configured, deploy your Strapi application:
flyctl deployThe 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 logsOnce health checks pass, open your application:
flyctl openNavigate 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-adminAccording 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.
Get Started in Minutes
npx create-strapi-app@latest in your terminal and follow our Quick Start Guide to build your first Strapi project.