The Best Way to Deploy Next.js to Fly.io

By Manthan Mallikarjun, Published 3 months ago

Fly.io is an amazing service which allows you to deploy container all across the globe with ease. The have abstracted away a lot of the complexity, to the point where you give them a Dockerfile and they do the rest.

The problem

While Fly works really well for traditional API services, Next.js has a couple of quirks that makes it harder to deploy. Typically your Next.js app will need some public environment variables (prefixed with NEXT_PUBLIC) as well as some private environment variables during the build command.

Fly (rightfully) prefers the official way to provide secrets into a Dockerfile which is more secure than blindly injecting secrets into the build environment. Typically that will look something like this:

Mount the secrets:

RUN --mount=type=secret,id=MY_SUPER_SECRET \
    MY_SUPER_SECRET="$(cat /run/secrets/MY_SUPER_SECRET)" some_command \
    && more_commands_maybe

and then pass it during deployment:

fly deploy \
    --build-secret MY_SUPER_SECRET=some_value

Docs

In an application where you have many secrets, this process becomes cumbersome and error-prone. We found a way to make this process easier and more hands-off.

NOTE: Fly.io does have this CLI solution to make things easier, but it still feels like a cumbersome process

The solution

We have many applications with many secrets. Trying to remember to update the Dockerfile, Fly.io's runtime secrets, and GitHub actions was a nightmare. We found a way to make this process easier and more hands-off using a couple of tools.

Overview

Our solution involves the following steps:

  1. Store the env vars in a central location. Sync it to Fly.io and GitHub actions
  2. Build the Next.js application inside of GitHub actions (outside of Fly.io)
  3. Copy the built files and deploy that container to Fly.io

Infisical

Infisical is the key to our solution. It allows us to manage our secrets in a single place and then inject them into our Dockerfile, Fly, and GitHub actions. It is open source and free to self host or you can use their hosted solution.

To get started, we set up Infisical, created our project, and added our production secrets. Then we set up the following syncs to Fly.io and GitHub actions:

Infisical Integrations

Note that for the GitHub integration we specifically use the Repository Environment scope.

GitHub actions

The next step is to build the Next.js application inside of GitHub actions. We use the following GitHub action to build the application:

deploy:
  runs-on: ubuntu-latest
  environment: production # IMPORTANT, you need this to absorb the synced variables
 
  steps:
    - uses: actions/checkout@v4
 
    - uses: actions/setup-node@v4
      with:
        node-version: lts/*
        cache: 'npm'
 
    - uses: actions/cache@v4
      with:
        path: ${{ github.workspace }}/.next/cache
        key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**/*.js', '**/*.jsx', '**/*.ts', '**/*.tsx') }}
        restore-keys: |
          ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-
 
    - run: npm ci
 
    - uses: koyashiro/[email protected]
      with:
        secrets: ${{ toJSON(secrets) }}
 
    - run: npm run build
 
    - uses: docker/setup-buildx-action@v3
    - uses: superfly/flyctl-actions/setup-flyctl@master
    - run: flyctl deploy --local-only

Dockerfile

Finally, you'll need the Dockerfile to collect the built files and deploy it to Fly.io. First you'll need to make a change in the next.config.js file.

module.exports = {
  output: 'standalone',
};

This tells Next.js that you'll be running the app in a server as opposed to a serverless environment.

Then you can use the following Dockerfile:

FROM node:lts-alpine as base
 
WORKDIR /app
 
ENV NODE_ENV production
ENV NEXT_TELEMETRY_DISABLED 1
 
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
 
COPY public* ./public
 
RUN mkdir .next
RUN chown nextjs:nodejs .next
 
COPY --chown=nextjs:nodejs .next/standalone ./
COPY --chown=nextjs:nodejs .next/static ./.next/static
 
USER nextjs
 
ENV PORT 3000
 
EXPOSE 3000
CMD HOSTNAME="0.0.0.0" node server.js

Deploy

Commit and push your changes and watch as GitHub actions build your Next.js application and deploy it to Fly.io. You can now manage your secrets in a single place and have them automatically synced to Fly.io and GitHub actions.