Fly.io has gained significant popularity in the developer community recently, particularly after being endorsed by Kent C. Dodds for hosting his Epic Web project. It's a go-to choice for hobby projects, thanks to its starting plans that are effectively free, making it highly accessible for individual developers. While Next.js applications are often deployed to Vercel, Fly.io has emerged as a perfectly viable alternative, offering robust hosting solutions and global deployment capabilities. In this blog post, we'll give an overview of how to install a Next.js app to Fly.io, mentioning any gotchas you should be aware of along the way.
The Project
Our project is a simple Next.js project using the latest version of Next.js at the time of the writing (14). It uses the app
directory and integrates with Spotify to get a list of episodes for our podcast, Modern Web. The bulk of the logic is in the page.tsx
file, shown below, which represents the front page that is server-rendered after retrieving a list of episodes from Spotify.
// app/page.tsx
import { getEpisodes } from '@/spotify/spotify';
export default async function Home() {
const modernWebShowId = '5FGA58foRFkJ6IgJbCFYgm';
const show = await getEpisodes(modernWebShowId);
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
{show.episodes.items.map(
(episode) => <a
key={episode.id}
href={episode.external_urls.spotify}
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
{episode.name}{' '}
<span
className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
->
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
{episode.description}
</p>
</a>)}
</div>
</main>
)
}
The getEpisodes
is a custom function that is a wrapper around the Spotify API. It uses the Spotify client ID and secret (provided through the SPOTIFY_CLIENT_ID
and SPOTIFY_CLIENT_SECRET
environment variables, respectively) to get an access token from Spotify and invoke a REST endpoint to get a list of episodes for the given show ID. As can be seen from the above code, the Home
is an async, server-rendered component.
Scaffolding of Fly.io Configuration
To get started with Fly.io and deploy a new project using flyctl
, you need to go through a few simple steps: installing the flyctl
CLI, logging into Fly.io, and using the flyctl launch
command.
Installing the CLI
Installing flyctl
is different depending on the operating system you use:
- If you're on Windows, the easiest way to install
flyctl
is by using scoop, a command-line installer. First, install scoop if you haven’t already, then runscoop install flyctl
in your command prompt or PowerShell. - For macOS users, you can use Homebrew, a popular package manager. Simply open your terminal and run
brew install superfly/tap/flyctl
. - Linux users can install
flyctl
by running the following script in the terminal:curl -L https://fly.io/install.sh | sh
. This will download and install the latest version.
Logging In
After installing flyctl
, the next step is to log in to your Fly.io account. Open your terminal or command prompt and enter flyctl auth login
. This command will open a web browser prompting you to log in to Fly.io. If you don’t have an account, you can create one at this step. Once you're logged in through the browser, the CLI will automatically be authenticated.
Scaffolding the Fly.io Configuration
The next step is to use fly launch
to add all the necessary files for deployment, such as a Dockerfile
and a fly.toml
file, which is the main Fly.io configuration file. This command initiates a few actions:
- It detects your application type and proposes a configuration.
- It sets up your application on Fly.io, including provisioning a new app name if you don’t specify one.
- It allows you to select a region to deploy to. There are really many to choose from, so you can get really picky here.
Once the process completes, flyctl
will be ready for deploying the application.
In our case, the process went like this:
➜ flyctl launch
Creating app in /Users/dario/Projects/blog-demos/thisdot-nextjs
Scanning source code
Detected a Next.js app
? Choose an app name (leave blank to generate one): thisdot-nextjs-blog
automatically selected personal organization: This Dot
Some regions require a paid plan (bom, fra, maa).
See https://fly.io/plans to set up a plan.
? Choose a region for deployment: Atlanta, Georgia (US) (atl)
App will use 'atl' region as primary
Created app 'thisdot-nextjs-blog' in organization 'personal'
Admin URL: https://fly.io/apps/thisdot-nextjs-blog
Hostname: thisdot-nextjs-blog.fly.dev
installing: npm install @flydotio/dockerfile@latest --save-dev
added 20 packages, and audited 352 packages in 5s
119 packages are looking for funding
run `npm fund` for details
found 0 vulnerabilities
create Dockerfile
Wrote config file fly.toml
Validating /Users/dario/Projects/blog-demos/thisdot-nextjs/fly.toml
Platform: machines
✓ Configuration is valid
If you need custom packages installed, or have problems with your deployment
build, you may need to edit the Dockerfile for app-specific changes. If you
need help, please post on https://community.fly.io.
Now: run 'fly deploy' to deploy your Next.js app.
Deploying
Now, if this was a simpler Next.js app without any environment variables, running flyctl deploy
would build the Docker image in a specialized "builder" app container on Fly.io and deploy that image to the app container running the app. However, in our case, executing flyctl
deploy will fail:
#0 1.486 Creating an optimized production build ...
#0 9.227 ✓ Compiled successfully
#0 9.227 Linting and checking validity of types ...
#0 12.17 Collecting page data ...
#0 13.06 Generating static pages (0/5) ...
#0 13.26 Error: Missing client ID or secret
#0 13.26 at getEpisodes (/app/.next/server/app/page.js:1:2140)
#0 13.26 at Home (/app/.next/server/app/page.js:1:2884)
This is because our page is statically rendered, and the Next.js build process attempts to run Home
, our server-rendered component to cache its output. Before we can do this, we need to add our environment variables so that Fly.io is aware of them, but this is somewhat a tricky subject, so let's explain why in the following chapter.
Handling of Secrets
Most complex web apps will need some secrets injected into the app via environment variables. Environment variables are a good way to inject sensitive information, such as API secret keys, to your web app without storing them in the repository, the file system, or any other unprotected place.
Unlike other providers such as the previously mentioned Vercel, Fly.io distinguishes built-time and run-time secrets, which are then injected as environment variables. Build-time secrets are those secrets that your app requires to build itself, while run-time secrets are needed while the app is running.
In our case, due to the fact that Next.js will attempt to cache our static pages upfront, the Spotify client ID and client secret are needed during both build-time and run-time (after the cache expires).
Build-Time Secrets
Our Next.js app is built while building the Docker image, therefore build-time secrets should be passed to the Docker context. The recommended, Docker-way of doing this, is through Docker's build-time secrets, which are added through a special --mount=type=secret
flag to the RUN
command that builds the site. This is a relatively newer feature that allows you to securely pass secrets needed during the build process without including them in the final image or even as an intermediate layer.
This means that, instead of having the following build command in our Dockerfile
:
RUN npm run build
we will have:
RUN \
SPOTIFY_CLIENT_ID="$(cat /run/secrets/SPOTIFY_CLIENT_ID)" \
SPOTIFY_CLIENT_SECRET="$(cat /run/secrets/SPOTIFY_CLIENT_SECRET)" \
npm run build
Now, you can either modify the Dockerfile
manually and add this, or you can use a helpful little utility called dockerfile:
npx dockerfile --mount-secret=SPOTIFY_CLIENT_ID --mount-secret=SPOTIFY_CLIENT_SECRET
If we were using docker build
to build our image, we would pass the secret values like so:
docker build --secret id=SPOTIFY_CLIENT_ID,src=someClientId --secret id=SPOTIFY_CLIENT_SECRET,src=someClientSecret
However, in our case we use fly deploy
, which is just a wrapper around docker build
. To pass the secrets, we would use the following command:
fly deploy --build-secret SPOTIFY_CLIENT_ID=someClientId --build-secret SPOTIFY_CLIENT_SECRET=someClientSecret
And now, the app builds and deploys successfully in a few minutes.
To summarize, if you have any secrets which are necessary at build-time, they will need to be provided to the fly deploy
command. This means that if you have a CI/CD pipeline, you will need to make sure that these secrets are available to your CI/CD platform. In the case of GitHub actions, they would need to be stored in your repository's secrets.
Run-Time Secrets
Run-time secrets are handled in a different way - you need to provide them to Fly.io via the fly secrets set
command:
fly secrets set SPOTIFY_CLIENT_ID=someClientId SPOTIFY_CLIENT_SECRET=someClientSecret
Now, you might be wondering why fly deploy
cannot use these secrets if they are already stored in Fly.io. The architecture of Fly.io is set up in such a way that reading these secrets through the API, once they are set, is not possible. Secrets are stored in an encrypted vault. When you set a secret using fly secrets set
, it sends the secret value through the Fly.io API, which writes it to the vault for your specific Fly.io app. The API servers can only encrypt; they cannot decrypt secret values. Therefore, the fly deploy
process, which is, if you remember, just a wrapper around docker build
, cannot access the decrypted secret values.
Other Things to Consider
Beware of .env Files
In Next.js, you can use .env
as well as .env.local
for storing environment variable values for local development. However, keep in mind that only .env.local files are ignored by the Docker build process via the .dockerignore
file generated by Fly.io. This means that if you happen to be using an .env
file, this file could be bundled into your Docker image, which is potentially a security risk if it contains sensitive information. To prevent this from happening, be sure to add .env
to your .dockerignore
file as well.
Not Enough Memory?
For larger Next.js sites, you might run into situations where the memory of your instance is simply not enough to run the app, especially if you are on the hobby plan.
If that happens, you have two options.
The first one does not incur any additional costs, and it involves increasing the swap size. This is not ideal, as more disk operation is involved, making the entire process slower, but it is good enough if you don't have any other options. To set swap size to something like 512 MB, you need to add the following line to the fly.toml
file near the top:
swap_size_mb = 512
The second one is increasing memory size of your instance. This does incur additional cost, however. If you decide to use this option, the command to use is:
fly scale memory [memoryInMb]
For example, to increase RAM memory to 1024 MB, you would use the command:
fly scale memory 1024
After making the changes, you can try redeploying and seeing if the process is still crashing due to lack of memory.
Conclusion
In conclusion, deploying Next.js applications to Fly.io offers a flexible and robust solution for developers looking for alternatives to more commonly used platforms like Vercel. We hope this blog post has provided you with some useful insights on the things to consider when doing so. Be sure to also check out our Next starter templates on starter.dev if you'd like to integrate a few other frameworks into your Next.js project.
The entire source code for this project is available on Stackblitz.