DEV Community

Cover image for Deploying Payload CMS to Fly.io
Cândido Sales Gomes
Cândido Sales Gomes

Posted on

Deploying Payload CMS to Fly.io

Infra

Service Memory Disk CPU
PostgreSQL 256 MB RAM 1 GB 1 (shared)
PayloadCMS (Node) 1 GB RAM 1 GB 1 (shared)
Meilisearch 1 GB RAM 1 (shared)

Install Fly CLI

brew install flyctl
Enter fullscreen mode Exit fullscreen mode

Login to your Fly account.

Follow the instructions and return to the command line.

fly auth login
Enter fullscreen mode Exit fullscreen mode

Create the fly.toml and Dockerfile in your current project.

# fly.toml app configuration file generated for payload-app on 2024-11-19T22:14:01-05:00
#
# See https://fly.io/docs/reference/configuration/ for information about how to use this file.
#

app = 'payload-app'
primary_region = 'gru'
kill_timeout = '5m0s'

[build]

[[mounts]]
  source = 'media'
  destination = '/opt/app/media'
  initial_size = '1gb'

[http_service]
  internal_port = 3000
  force_https = true
  auto_stop_machines = 'stop'
  auto_start_machines = true
  min_machines_running = 1
  processes = ['app']

[[services]]
  protocol = 'tcp'
  internal_port = 1337
  processes = ['app']

  [[services.ports]]
    port = 80
    handlers = ['http']

[[vm]]
  cpu_kind = 'shared'
  cpus = 1
  memory_mb = 1024
Enter fullscreen mode Exit fullscreen mode

Create the Dockerfile. The example below is a multi-stage docker build of Payload for production.

# syntax=docker.io/docker/dockerfile:1

# From https://github.com/vercel/next.js/blob/canary/examples/with-docker/Dockerfile

FROM node:18-alpine AS base

# Install dependencies only when needed
FROM base AS deps
# Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed.
RUN apk add --no-cache libc6-compat
WORKDIR /app

# Install dependencies based on the preferred package manager
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
    if [ -f yarn.lock ]; then yarn --frozen-lockfile; \
    elif [ -f package-lock.json ]; then npm ci; \
    elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
    else echo "Lockfile not found." && exit 1; \
    fi


# Rebuild the source code only when needed
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .

# Next.js collects completely anonymous telemetry data about general usage.
# Learn more here: https://nextjs.org/telemetry
# Uncomment the following line in case you want to disable telemetry during the build.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN \
    if [ -f yarn.lock ]; then yarn run build; \
    elif [ -f package-lock.json ]; then npm run build; \
    elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
    else echo "Lockfile not found." && exit 1; \
    fi

# Production image, copy all the files and run next
FROM base AS runner
WORKDIR /app

ENV NODE_ENV=production
# Uncomment the following line in case you want to disable telemetry during runtime.
# ENV NEXT_TELEMETRY_DISABLED 1

RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs

# COPY --from=builder /app/public ./public

# Set the correct permission for prerender cache
RUN mkdir .next
RUN chown nextjs:nodejs .next

# Automatically leverage output traces to reduce image size
# https://nextjs.org/docs/advanced-features/output-file-tracing
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static

# Media folder
COPY --from=builder --chown=nextjs:nodejs /app/media ./media


USER nextjs

EXPOSE 3000

ENV PORT=3000

# server.js is created by next build from the standalone output
# https://nextjs.org/docs/pages/api-reference/next-config-js/output
ENV HOSTNAME="0.0.0.0"
CMD ["node", "server.js"]
Enter fullscreen mode Exit fullscreen mode

You must add standalone output for the NextJS to enable run in the Docker container.

next.config.js

import { withPayload } from '@payloadcms/next/withPayload'

/** @type {import('next').NextConfig} */
const nextConfig = {
    // Your Next.js config here
    output: "standalone",
}

export default withPayload(nextConfig)

Enter fullscreen mode Exit fullscreen mode

Create migrate folder

Create a new migration file in the migrations directory.

First, set up the migrationDir in payload.config.ts

import { buildConfig } from 'payload'

export default buildConfig({
  // your config here
  db: postgresAdapter({
    pool: {
      connectionString: process.env.DATABASE_URI || '',
    },
    //  Add the path to migrations folder
    migrationDir: './src/migrations', // <<<<<<<
  })
})
Enter fullscreen mode Exit fullscreen mode

Later, run the command below to generate the migrations in the folder.

npm run payload migrate:create
Enter fullscreen mode Exit fullscreen mode

In order to run migrations at runtime, on initialization, you can pass your migrations to your database adapter under the prodMigrations key as follows:

// Import your migrations from the `index.ts` file
// that Payload generates for you
import { migrations } from './migrations'
import { buildConfig } from 'payload'

export default buildConfig({
  // your config here
  db: postgresAdapter({
    //  your adapter config here
    pool: {
      connectionString: process.env.DATABASE_URI || '',
    },
    migrationDir: './src/migrations',
    prodMigrations: migrations // <<<<<<<<<<
  })
})
Enter fullscreen mode Exit fullscreen mode

Passing your migrations, as shown above, will tell Payload, in production only, to execute any migrations that need to be run before completing the initialization of Payload. This is ideal for long-running services where Payload is only initialized at startup.

Deploy

Before deploying, you have to create the database. So run the command below:

fly postgres create
Enter fullscreen mode Exit fullscreen mode

Image description

After creating the database, copy the Connection string in the safe place. You will use it as a secret DATABASE_URI.

Generate your PAYLOAD_SECRET at https://randomkeygen.com/ and store it in a safe place.

Now, you will deploy the app.

fly launch
Enter fullscreen mode Exit fullscreen mode

And later ...

fly deploy
Enter fullscreen mode Exit fullscreen mode

The last step is to add the secrets in your app in Fly.io

Image description

And deploy again to update the secrets; now, it will work on the migrations in runtime.

fly deploy
Enter fullscreen mode Exit fullscreen mode

Conclusion

The flow is simple, but I took too long to understand how to set up the migrations. I hope it will help you :)

Source

Top comments (0)