Moving a SvelteKit Site off Netlify and onto Fly.io

Published at Jun 29, 2026

All posts #svelte#devops

I’ve been running this site on Netlify for a while. It’s been fine. No complaints, really. But I wanted to consolidate — everything I self-host lives on Fly.io, and having one more thing scattered somewhere else was starting to bother me. So: let’s move it.

This is a SvelteKit site. It’s simple, statically prerendered, and has no server-side logic worth speaking of. Here’s what the migration actually looked like.

1. Switch the adapter

Netlify deployments use @sveltejs/adapter-netlify. Fly.io runs a Docker container, so you need something that produces a Node.js server instead. Swap to @sveltejs/adapter-node:

bun remove @sveltejs/adapter-netlify
bun add -D @sveltejs/adapter-node

Then update svelte.config.js:

import adapter from '@sveltejs/adapter-node';

const config = {
  kit: {
    adapter: adapter()
  }
};

export default config;

adapter-node compiles your app into a self-contained server at build/index.js. You run it with node build/index.js (or bun build/index.js). Port defaults to 3000.

2. Write the Dockerfile

Since the project already uses Bun, I keep it consistent in the image:

FROM oven/bun:1 AS builder
WORKDIR /app

COPY package.json bun.lock ./
RUN bun install --frozen-lockfile

COPY . .
RUN bun run build

FROM oven/bun:1-slim
WORKDIR /app

COPY --from=builder /app/build build/
COPY --from=builder /app/node_modules node_modules/
COPY package.json .

EXPOSE 8080
ENV NODE_ENV=production
ENV PORT=8080
CMD ["bun", "build/index.js"]

Two-stage build: the first stage installs and compiles, the second stage is lean and only carries what’s needed to run. This keeps the final image small.

3. Set up the Fly.io project

Install the Fly CLI if you haven’t already, then:

fly launch

Fly will detect the Dockerfile and walk you through naming the app, picking a region, and creating a fly.toml. For a personal site the defaults are fine. Here’s roughly what fly.toml ends up looking like:

app = "your-app-name"
primary_region = "fra"

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = true
  auto_start_machines = true
  min_machines_running = 0

auto_stop_machines and min_machines_running = 0 means the machine shuts down when there’s no traffic and cold-starts on the next request. For a personal site that’s fine — you trade a bit of latency for not burning compute 24/7 on a page nobody visits at 3am.

Then deploy:

fly deploy

Fly builds the image, pushes it, and gives you a *.fly.dev URL to verify everything works before touching DNS.

4. Point your domain at Fly.io

Once the *.fly.dev URL looks good, switch the DNS. I have my domain at a third-party registrar, so this is just updating records there.

Get Fly’s IP addresses:

fly ips list

Then in your DNS settings:

  • A record → the IPv4 address Fly gives you
  • AAAA record → the IPv6 address (if your registrar supports it)

For a www subdomain or if you’re proxying through Cloudflare, a CNAME to your *.fly.dev hostname works too.

Propagation takes anywhere from a few minutes to a few hours depending on your TTL. Set it low beforehand (300s) if you want the cutover to be fast.

Add the custom domain to Fly so it can provision a TLS certificate:

fly certs add yourdomain.com
fly certs add www.yourdomain.com

5. Clean up Netlify

Once DNS is resolving correctly and HTTPS is working, go to Netlify, find the site, and delete it. There’s a confirm-by-typing step. Do it and move on.

One thing worth knowing: GitHub deploys break

Netlify’s GitHub integration is what triggered a new build on every push to main. That’s gone now. Fly.io doesn’t have a built-in git integration — you fly deploy manually, or you wire it up yourself through a GitHub Action.

For a personal site I’m fine running fly deploy from the terminal when I push something. If you want it automated, a minimal GitHub Action looks like this:

name: Deploy
on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: superfly/flyctl-actions/setup-flyctl@master
      - run: fly deploy --remote-only
        env:
          FLY_API_TOKEN: ${{ secrets.FLY_API_TOKEN }}

Generate the token with fly tokens create deploy and add it as a repository secret. Then push-to-deploy is back.


That’s the full migration. Swap the adapter, write a Dockerfile, fly deploy, update DNS, delete Netlify. Roughly an afternoon of work, and most of that is waiting for DNS to propagate.

© 2026 Roman Kuba
No cookies. Privacy-friendly analytics. · Built with Svelte and Tailwind.