tl;dr for the post, in case you don’t care about anything except literally “why I stopped using Vercel:”

  1. My app needs certain executables, like git and ssh, to be available at runtime (not build time). The Vercel platform doesn’t allow any customization of the runtime environment.

It would have been possible to work around that, but there is a second limitation that bothered me:

  1. The Hobby plan only has one hour of log retention. Even the Pro plan — which costs $20/mo — only has 24 hours of log retention.

My app doesn’t produce that many logs, but I care about the ones it does produce.

If the Hobby plan had 24 hours log retention, and the Pro plan had 7 days, I’d probably still be using Vercel.

I recently deployed another one of my little apps, written with Next.js (NB. The app code is open-sourced on Github).

When I think of deploying a Next app, I immediately think of Vercel, a serverless cloud hosting platform designed by the creators of Next.js, specifically for Next.js apps. It’s called ”the native Next.js platform,” after all.

In fact, I did initially deploy the app on Vercel, but ended up switching to deploying with Fly partway through development.

This post is a summary of my notes about:

  1. Why I switched from Vercel to Fly. (Both are great services, this isn’t a knock on either!)
  2. What does Vercel have but Fly doesn’t, and vice-versa?
  3. A comparison of the deployment process.

We’ll view these topics through the lens of deploying my particular application.

The requirements

My little app is a typical side project, with a few major components:

  1. Client (complex, interactive UI)
  2. Server (authentication, data fetching, mutations, etc.)
  3. Database (Postgres)
  4. Batch jobs (Nightly export)

The client and server are implemented in Next.js. All data is persisted to Postgres (using the Prisma ORM to handle database communication). Finally, there is also a batch job that runs regularly which needs access to the Postgres instance as well as the ability to call into shared code from the server component.

As another note, this application has a Git export feature that requires it to be able to: (a) generate SSH public/private keypairs, (b) use the generated keys to push/pull to/from Git repo(s) with SSH auth.

Vercel — the pros and cons

Vercel pros:

  1. The vercel.com project dashboard has fantastic DX/UX overall. Some notable DX/UX features I appreciated:
    • Emphasis on GitOps.
    • Out-of-the-box 90% working for a Next.js app.
    • Deployments are very fast: ~2 min.
    • Deep integration with Next.js features: OpenTelemetry, Data Cache, etc.
  2. Vercel’s serverless Postgres offering uses Neon cloud hosting under the hood. This is a true serverless Postgres that is, in theory, fully scalable and production ready.
  3. Vercel has a clever Cron job implementation that allows my app to “scale to zero” (which happens magically in serverless land) and yet still run a nightly cron task.

In comparison, Fly does not emphasize GitOps and has slower deployments.

Fly’s not optimized for Next.js (but has an easy guide for Next apps) and it doesn’t have a fully managed Postgres solution that competes with Neon. (In fact — if I were doing a true production app, I might use Fly for application containers and Neon for the database instead of relying on Fly’s self-managed database tooling.)

Fly also has no direct Cron support, you have to manage it yourself with an always-running process which costs extra money.

Vercel cons:

  1. No Dockerfile compatibility or way to install extra packages to the runtime; the runtime environment is totally opaque.
    • Unclear exactly what packages are available to the app at runtime. There is documentation about the build image used, but nothing about the runtime image or execution context. This is probably deliberate.
    • For my app, this made it impossible to directly call things like git from application code. I would need another “microservice,” running in a totally separate cloud provider, to expose an API encapsulating the git-related operations.
  2. Essential observability features are gated behind a pretty expensive price hike from Hobby to Pro ($0/mo -> $20/mo):
    • Web Analytics require a Pro plan.
    • Logs are weirdly limited. Hobby plans can only view one hour of log data; Pro plans can still only view 24 hours of log data. On the plus side, Pro plans support log sinks, so you could hook up your own system to keep logs for a longer period. Still. I pay $20 per month and you delete my logs after 24 hours? My application doesn’t produce that much log data, but what it does produce, I care about. This seems like an unexpected limitation.
  3. No private networking. For example, the only thing protecting the database is a password. There are no network-level protections.
    • Perhaps a necessity, since the database is provided by a 3rd party provider. Still, this feels like a significant gap for defense-in-depth.
    • Relatedly, there is no database migration functionality. The Vercel workaround is to run database migrations during your build which seems non-ideal. For example, Pull Request deployment builds will accidentally migrate the production database unless you add protections. This feels like a significant risk vector in a variety of ways.

Fly — the pros and cons

Fly pros:

  1. I understand how the Fly runtime architecture works.
    • Compared to Vercel, Fly has extremely less magic and gives me a lot more control.
    • I also have escape hatches to fully control the runtime, prevent scale-to-zero if I don’t want it, and so on.
    • Compatible with any containerized app (anything with a Dockerfile).
  2. I pay per usage for my app. By itself, with minimal traffic, a lightweight app with (a) an application process, (b) a cron process, and (c) a Postgres database, will ultimately cost me about $5/month on Fly. (Assuming (a) and (c) are scale-to-zero, which they are.)

In comparison, Vercel has no pay-as-you-go plan — or, they do, but there’s a $20/month minimum. Depends on your perspective. Either way, paying for the Pro plan feels bad if you’re just deploying a single app. Maybe if I had deployed all my side projects on Vercel, it would feel better.

Also, Vercel has no runtime escape hatches. If you want to do something unusual on the server, you literally need a whole ‘nother cloud provider for that piece, because Vercel won’t handle it.

Fly cons:

  1. No managed Cron, just a guide to do it yourself.
  2. No managed Postgres, just a self-managed cluster with automatic deployment.
  3. Deployments are considerably slower (from ~2 min to >5 min). Of course, this isn’t an apples-to-apples comparison, but it’s a noticeable DX impact.

Conclusion

Are you in the happy land where you only need exactly the features Vercel offers, and you don’t particularly care about your runtime environment, and you maybe have some other kind of monitoring so you don’t need extended log retention? If so… use Vercel, it’s got great UX.

Are you hoping to understand what’s going on under the hood, change your runtime environment at all, or have traditional server features like log retention? If so… use Fly, and you’ll have the control you need.

But also, these are two fantastic providers that, between them, have changed my expectations about what’s possible for cloud hosting. I call out Vercel for having great UX. Fly also has great UX, just by comparison, not quite as much. Still, the UX was never the sticking point; both services are above the threshold of usability I’d expect. The main problem with both is to do with the features they’re missing.

It would be awesome to see them converge: Vercel to allow longer log retention and runtime modification, Fly to support serverless Postgres and Cron.

Appendix — Deployment Process Comparison

Vercel

Vercel setup mostly happens in the Web dashboard:

  1. Create a new Vercel project and attach your Git repo to it.
  2. Change build command to prisma generate && prisma migrate deploy && next build.
  3. Add all required environment variables in the UI:
    • Non-secrets: NEXTAUTH_URL, NEXT_TELEMETRY_DISABLED
    • Secrets: NEXTAUTH_SECRET, AUTH_GITHUB_ID, AUTH_GITHUB_SECRET, DUST_SSH_KEY_PASSPHRASE

After filling out the initial form and creating your project, there are some follow up steps:

  1. Open project -> Storage -> Connect to a database
  2. Open project -> Settings -> Domains -> Add custom domain

Also, because Neon/Vercel exposes both pooled and non-pooled DB connections, we can update our prisma/schema.prisma datasource to use both:

url       = env("DATABASE_PRISMA_URL")
directUrl = env("DATABASE_URL_NON_POOLING")

Finally, we need to add a vercel.json with crons:

{
  "crons": [
    {
      "path": "/api/cron/daily-export",
      "schedule": "0 5 * * *"
    }
  ],
}

Fly.io

Fly setup mostly happens via flyctl.

The deployment process is summarized below (remember to change URLs and things to match your setup, if you’re copying):

# Create fly.toml
flyctl launch
# "yes" to postgres instance
#    Choose Development config to minimize cost
# "no" to redis

# Add missing dependencies
# Note, this step is impossible in Vercel.
npx dockerfile --add-deploy openssh-client git

# Set env vars
npx dockerfile --env-deploy=NEXTAUTH_URL:https://dust.luketurner.org
npx dockerfile --env-base=NEXT_TELEMETRY_DISABLED:1

# Set secrets
flyctl secrets set \
  "NEXTAUTH_SECRET=$(openssl rand -base64 48)" \
  "AUTH_GITHUB_ID=foo" \
  "AUTH_GITHUB_SECRET=bar" \
  "DUST_SSH_KEY_PASSPHRASE=$(openssl rand -base64 48)"

# Add migrate command to fly.toml
cat >> fly.toml << _EOF_
[deploy]
  release_command = "npm run db:migrate:prod"
_EOF_

# Add cron process to fly.toml
cat >> fly.toml << _EOF_
[processes]
  app = "npm run start"
  cron = "supercronic /app/crontab"
_EOF_
# Remember to also add processes = ["app"] under the [http_service] section

# Supercronic dependencies 
npx dockerfile --add-deploy ca-certificates curl openssl

# Supercronic installation -- Put the following in your Dockerfile
# From https://fly.io/docs/app-guides/supercronic/

#ENV SUPERCRONIC_URL=https://github.com/aptible/supercronic/releases/download/v0.2.26/supercronic-linux-amd64 \
#    SUPERCRONIC=supercronic-linux-amd64 \
#    SUPERCRONIC_SHA1SUM=7a79496cf8ad899b99a719355d4db27422396735
#
#RUN curl -fsSLO "$SUPERCRONIC_URL" \
# && echo "${SUPERCRONIC_SHA1SUM}  ${SUPERCRONIC}" | sha1sum -c - \
# && chmod +x "$SUPERCRONIC" \
# && mv "$SUPERCRONIC" "/usr/local/bin/${SUPERCRONIC}" \
# && ln -s "/usr/local/bin/${SUPERCRONIC}" /usr/local/bin/supercronic
#
# COPY crontab crontab

# Initial deployment
flyctl deploy

# update CNAME in your DNS provider then provision cert:
flyctl certs create dust.luketurner.org

# Set max scale to 1 (defaults to 2 for HA)
flyctl scale count app=1 cron=1