/ astro-integrations / How to Integrate Fly.io with Astro: Complete Guide
astro-integrations 10 min read

How to Integrate Fly.io with Astro: Complete Guide

Step-by-step guide to integrating Fly.io with your Astro website.

How to Integrate Fly.io with Astro: Complete Guide

Fly.io runs your applications on servers close to your users. Instead of picking one region and hoping for the best, Fly.io deploys your app to multiple locations worldwide and routes requests to the nearest one. For Astro sites that render on demand (server-side rendering), this means faster response times for visitors regardless of where they are.

Unlike traditional platform-as-a-service providers that abstract away the infrastructure, Fly.io gives you lightweight VMs called Machines. You get the simplicity of a managed platform with the control of real servers. Astro's rendering model pairs well with this because you can serve prerendered static pages from cache while running on-demand routes on Fly Machines.

Versions checked on 2026-05-29. This guide targets Astro 6.4.2 and the @astrojs/node adapter 10.1.2. Note that Astro is now on version 6, and several older patterns changed. The biggest one: output: 'hybrid' was removed in Astro 5. Static is the default, and you opt individual routes into on-demand rendering by adding an adapter. Details below.

What Changed in Astro 5 and 6 (Read This First)

If you are following an older Fly.io plus Astro tutorial, watch for these breaking changes:

  • output: 'hybrid' no longer exists. In Astro 5 the 'hybrid' and 'static' build outputs were merged into a single 'static' mode (the default). You no longer set output: 'hybrid'. Remove it if you have it. See the Astro v5 upgrade guide.
  • Static is the default, adapters unlock on-demand rendering. By default every page, route, and endpoint is prerendered at build time. To render a route at request time on the server, add an adapter and set export const prerender = false on that route (or set output: 'server' to flip the default for the whole project). See On-demand rendering.
  • Node requirement moved up. Astro 6 requires Node >=22.12.0 (per the astro package engines field on npm). The old "Node 18+" advice no longer applies, and your Dockerfile base image needs to match.

Prerequisites

  • Node.js 22.12.0 or newer (required by Astro 6)
  • An Astro project (npm create astro@latest)
  • A Fly.io account
  • The Fly CLI (flyctl) installed

Install the Fly CLI. These are the commands from the official Install flyctl page:

# macOS (Homebrew tap recommended by Fly; the core formula also works)
brew install superfly/tap/flyctl

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

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

Confirm it installed, then log in:

flyctl version
flyctl auth login

Setting Up Astro for On-Demand Rendering

Fly.io runs your Astro app as a Node server, so install the Node.js adapter. The official approach is the astro add command, which installs @astrojs/node and edits your astro.config.* for you in one step:

npx astro add node

If you prefer to wire it up by hand, install the package and edit the config yourself:

npm install @astrojs/node

Update your astro.config.mjs:

import { defineConfig } from "astro/config";
import node from "@astrojs/node";

export default defineConfig({
  // Static is the default. Add the adapter to enable on-demand rendering.
  // Set output: "server" only if you want EVERY route rendered on demand.
  // Otherwise leave output unset and mark individual routes with
  // `export const prerender = false`.
  output: "server",
  adapter: node({
    mode: "standalone",
  }),
});

The standalone mode produces a self-contained server that starts itself when the entry module runs, which is exactly what Fly.io needs. The other mode, middleware, is for embedding Astro inside an existing Express or Fastify server. See the @astrojs/node docs.

In standalone mode the server reads two environment variables at runtime, HOST and PORT. This matters for Docker (more on that next).

Letting Fly Generate the Dockerfile (Easiest Path)

If you already added the Node adapter, the simplest path is to let Fly do the work. When you run flyctl launch, Fly detects the @astrojs/node adapter and, if no Dockerfile exists, generates one with the correct start command and environment variables, along with a fly.toml. The official Deploy your Astro Site to Fly.io guide and Fly's Astro framework docs both describe this auto-detection.

flyctl launch

If you want full control instead, write your own Dockerfile as shown below. A hand-written Dockerfile wins over the generated one.

Writing Your Own Dockerfile (Full Control)

Create a Dockerfile in your project root. Note the Node 22 base image to match Astro 6's engine requirement:

FROM node:22-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-slim AS runtime
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/package.json ./

# HOST=0.0.0.0 is REQUIRED inside Docker. Without it the standalone
# server binds to 127.0.0.1 and Fly's proxy cannot reach it.
ENV HOST=0.0.0.0
ENV PORT=8080
EXPOSE 8080

CMD ["node", "./dist/server/entry.mjs"]

Add a .dockerignore to keep the image small:

node_modules
.git
.env
dist

Launching on Fly.io

Initialize your Fly app:

flyctl launch

This creates a fly.toml configuration file. Edit it to match your setup:

app = "your-astro-app"
primary_region = "iad"

[build]

[http_service]
  internal_port = 8080
  force_https = true
  auto_stop_machines = "stop"
  auto_start_machines = true
  min_machines_running = 0

[env]
  NODE_ENV = "production"

Make sure internal_port matches the PORT your server listens on (8080 in the Dockerfile above). The auto_stop_machines and auto_start_machines settings keep costs low. Machines spin down when idle and start back up when requests come in.

Deploying

Deploy your app:

flyctl deploy

Fly.io builds the Docker image, pushes it to their registry, and starts your Machines. The first deploy takes a few minutes. Subsequent deploys are faster because Docker layers are cached.

Check your app status:

flyctl status

Open it in the browser:

flyctl open

Environment Variables

Set secrets for your app (these are encrypted and available as environment variables):

flyctl secrets set ADMIN_EMAIL=admin@example.com
flyctl secrets set ADMIN_PASS=your-secure-password
flyctl secrets set DATABASE_URL=postgres://...

List current secrets:

flyctl secrets list

Multi-Region Deployment

Add more regions to reduce latency for global users:

flyctl scale count 2 --region iad,ams

This runs one Machine in Virginia (iad) and one in Amsterdam (ams). Fly.io's Anycast network routes users to the closest one.

For database-backed Astro apps, consider using Fly.io's managed Postgres or LiteFS for SQLite replication across regions.

Custom Domains

Add your domain:

flyctl certs add yourdomain.com
flyctl certs add www.yourdomain.com

Point your DNS records to Fly.io:

  • A record pointing to the IPv4 address shown by flyctl ips list
  • AAAA record pointing to the IPv6 address

Fly.io handles SSL certificates automatically through Let's Encrypt.

Production Tips

  1. Use health checks. Add a health check endpoint in your Astro app and configure it in fly.toml so Fly.io knows when your app is ready to receive traffic.

  2. Set memory limits. Scale memory with flyctl scale memory 512 if your on-demand routes need more headroom under load.

  3. Enable metrics. Fly.io provides built-in metrics through Grafana. Access them from the Fly.io dashboard to monitor response times and error rates.

  4. Use volumes for persistent data. If your Astro app writes files (like uploaded images), attach a Fly Volume since Machine filesystems are ephemeral.

  5. Set up CI/CD. Add flyctl deploy to your GitHub Actions workflow. This is the canonical workflow from Fly's Continuous Deployment with GitHub Actions guide.

# .github/workflows/fly-deploy.yml
name: Fly Deploy
on:
  push:
    branches:
      - main
jobs:
  deploy:
    name: Deploy app
    runs-on: ubuntu-latest
    concurrency: deploy-group
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: flyctl deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Generate the deploy token with fly tokens create deploy, then add it as a repository secret named FLY_API_TOKEN under Settings, Secrets and variables, Actions. Copy the entire token value including the FlyV1 prefix.

Common Errors and Fixes

  • App deploys but Fly reports it is unreachable or health checks fail. The standalone Node server defaults to binding 127.0.0.1 inside the container, so Fly's proxy cannot connect. Set ENV HOST=0.0.0.0 in your Dockerfile (and make internal_port in fly.toml match the PORT the server listens on). This is the single most common Astro-on-Fly mistake. See @astrojs/node docs for the HOST/PORT env vars.
  • Astro errors out saying output: 'hybrid' is not a valid option. That value was removed in Astro 5. Delete it. Static is the default, and adding an adapter is what enables on-demand rendering now. See the v5 upgrade guide.
  • Build fails with an unsupported Node version. Astro 6 requires Node >=22.12.0 (the engines field on the astro npm package). Bump your Dockerfile base image to node:22-slim and your CI Node version to match.
  • Routes that should be dynamic are returning stale prerendered HTML. With static as the default, a route is only rendered on demand if you set export const prerender = false on it (or set output: 'server' project-wide). Mixing the two without realizing which is the default is a frequent gotcha. See On-demand rendering.
  • flyctl launch did not generate a Dockerfile. Fly only auto-generates a Node Dockerfile when it detects the @astrojs/node adapter in your config. Add the adapter first (npx astro add node), or write your own Dockerfile as shown above. See Deploy your Astro Site to Fly.io.
  • GitHub Action fails to authenticate. Make sure the repository secret is named exactly FLY_API_TOKEN and contains the full token from fly tokens create deploy, including the leading FlyV1 prefix. See Continuous Deployment with GitHub Actions.

Official Docs and Examples

Alternatives to Consider

  • Vercel if you want zero-config deployment with edge functions and built-in Astro support.
  • Cloudflare Pages if you prefer edge-first deployment with the Cloudflare ecosystem.
  • Railway if you want a simpler PaaS experience without writing Dockerfiles.

Wrapping Up

Fly.io gives on-demand Astro apps the performance benefits of edge deployment without the constraints of serverless platforms. You get real VMs, multi-region routing, and fine-grained control over your infrastructure. The auto-stop feature keeps costs near zero for low-traffic sites, and scaling up is a single command. If your Astro site needs server-side rendering with global reach, Fly.io is one of the strongest options available. Just remember the Astro 6 essentials: static is the default, the Node adapter unlocks on-demand routes, and HOST=0.0.0.0 is mandatory inside Docker.

Sources