The ease of deploying to Fly.io

I've been working on a podcasting platform for a client lately — a Rails app, nothing exotic — and last week it was finally time to put it somewhere the client could click on it. I'd blocked off the afternoon. I made coffee. I braced myself for the usual dance: writing a Dockerfile, fighting a cloud console, figuring out where DNS points, learning that the health check is wrong, learning that the health check is right but the port is wrong, and so on.

I had a public URL in under ten minutes.

The tool was Fly.io, and the gap between what I was prepared for and what actually happened is what prompted this post. Deploying a real app to a real VM used to be a project. Now it can be a coffee break.

What "easy" actually looks like

I ran fly launch from the project root. It noticed the app was Rails. It picked a region, asked whether I wanted a Postgres database, generated a sensible fly.toml and a Dockerfile, showed me the proposed machine size, and asked if I wanted to tweak anything. I didn't. A fly deploy later, the app was on the internet at a *.fly.dev URL.

That was it. No console clicking, no IAM dance, no separately provisioned load balancer, no certificate ceremony. The defaults were sensible enough that I could have stopped there and gone back to writing code.

What struck me wasn't that any one of those steps was particularly clever. It was that someone had clearly sat down and thought about the whole arc — from "I have a Rails app on my laptop" to "it is on the internet" — and made a sane decision at every fork. That's rarer than it should be.

The two things that tripped me up

I'd be lying if I said it was completely smooth. Two small things bit me, and both are worth flagging if you're about to try this yourself.

Secrets. I keep app secrets in Doppler — it's the source of truth across my local environment, CI, and every other deploy target I touch. Fly has its own secret store (fly secrets set), which is fine, but it means Doppler is no longer the single source of truth unless I make it one. The fix was a small deploy script that pulls from Doppler and pipes the values into Fly right before the deploy:

doppler secrets download --no-file --format=env | fly secrets import --stage
fly deploy

fly secrets import reads KEY=VALUE pairs from stdin and handles quoting and multiline values correctly. --stage queues the secrets without restarting machines, so they all land together when the deploy goes out. Five minutes of friction, then never again.

Memory. Whatever default machine size I ended up with was undersized for this app. It deployed cleanly, ran for a bit, then started getting OOM-killed and restarted under perfectly normal load. The first time you see it, it looks like an app problem. It isn't. It's a VM-sizing problem.

The fix is in fly.toml:

[[vm]]
  memory = '1gb'
  cpu_kind = 'shared'
  cpus = 1

You can also bump it ad-hoc with fly scale memory 1024, but fly.toml is the value of record — the next deploy will reset whatever you scaled to match the file. Do it up front. Chasing OOM restarts is a far worse use of an afternoon than nudging a number once.

Why this matters

For a long time, the answer to "where should I run this?" trended toward more abstraction: managed everything, serverless, container orchestration, multi- region this, autoscaling that. There were good reasons for it, but the cost was that deploying a small app — the kind of thing one developer might ship for one client — started to require a small team's worth of expertise.

Fly is a reminder that a VM with a sensible wrapper around it is still a fantastic unit of deployment for most things most of the time. You get a real machine. You can SSH into it. You can scale it up by typing a number. You can add a region by typing a region. The abstraction stops where it stops being useful, which is exactly where you want it to stop.

I'm not going to pretend this is the right answer for every workload. If you're running something that genuinely needs the full Kubernetes apparatus, go run Kubernetes. But if you've been carrying around the assumption that deploying a real app to real infrastructure is inherently a heavy lift — because that's what it was the last time you tried — it's worth checking that assumption again.

Block off ten minutes. Run fly launch. See what happens.

The ease of deploying to Fly.io - Will Mitchell