Building NextJS with Server Components, Prisma and LiteFS

Hey!

My stack: NextJS with App Router. This means that Server Components are fetching data during build step. I also have Prisma that connects to SQLite through LiteFS. So I’d like to be able to query database with Prisma inside Server Components.

The problem I have is that NextJS throws errors during build, since Prisma calls can’t connect to SQLite before LiteFS is initialised, which is the last step just before NextJS app start.

Does anyone has idea how to solve this?

In snippets below you see my last attempt with disabled next build during image build and trying to run it during exec phase of LiteFS mount, but this feels like a hack and does not work

Dockerfile

# syntax = docker/dockerfile:1

# Adjust NODE_VERSION as desired
ARG NODE_VERSION=16.14.0
FROM node:${NODE_VERSION}-slim as base

LABEL fly_launch_runtime="Next.js"

# Next.js app lives here
WORKDIR /app

# Set production environment
ENV NODE_ENV=production
ENV DATABASE_URL=file:/litefs/db

# Install SQLite & LiteFS dependencies
RUN apt-get update -qq && \
    apt-get install -y ca-certificates fuse3 sqlite3


# Throw-away build stage to reduce size of final image
FROM base as build

# Install packages needed to build node modules; SQLite & LiteFS dependencies
RUN apt-get update -qq && \
    apt-get install -y python pkg-config build-essential

# Install node modules
COPY --link package-lock.json package.json ./
RUN npm ci --include=dev

# Copy application code
COPY --link . .

# Build prisma client
RUN npx prisma generate

# Build application; Disabled since build is run as part of litefs mount;
#RUN npm run build

## Remove development dependencies; Disabled since build is run as part of litefs mount;
#RUN npm prune --omit=dev


# Final stage for app image
FROM base

# Copy built application
COPY --from=build /app /app

# Install LiteFS binary
COPY --from=flyio/litefs:0.5 /usr/local/bin/litefs /usr/local/bin/litefs
ADD litefs.yml /etc/litefs.yml

# Start the server by default, this can be overwritten at runtime
EXPOSE 3000

ENTRYPOINT litefs mount

litefs.yml

fuse:
  dir: "/litefs"

data:
  dir: "/var/lib/litefs"

exit-on-error: false

proxy:
  addr: ":8080"
  target: "localhost:3000"
  db: "db"

exec:
# Run Next.js build only after litefs is mounted to enable prisma calls for Server Components
  - cmd: "npx prisma migrate deploy"
    if-candidate: true
  - cmd: "npm run build"
  - cmd: "npm run start"

lease:
  type: "consul"
  advertise-url: "http://${HOSTNAME}.vm.${FLY_APP_NAME}.internal:20202"
  candidate: ${FLY_REGION == PRIMARY_REGION}
  promote: true

  consul:
    url: "${FLY_CONSUL_URL}"
    key: "litefs/${FLY_APP_NAME}"

How does it fail? What you are attempting should work, presuming that you are just querying and not writing to the database. And if your build step is writing files, this may be the right place for this logic. The downside would be the amount of time any one machine is unavailable during deployment, but that can be mitigated with multiple machines.

I was playing with that today and made it work by scaling memory up to allow next build complete!
Thanks for confirming that I did steps in right direction.

One more questions regarding this setup though:

I have multiple machines running across several regions. Only one machine is primary with write permissions. Now I want to have NextJS Server Component that performs database write (I added that to Server Component render method just for testing, see sample below). It fails on replicas because it can’t directly write to database. So basically with LiteFS all events triggering database write should come from outside through LiteFS proxy call and I can’t write from the inside the running app replica?

So in the end the question is if this at all possible somehow or I have to expose database write logic via some route so that it will be proxied?

import Image from "next/image";
import { prisma } from "@/prisma/client";

export default async function Home() {
  const users = await prisma.user.findMany({})

  if (users.length < 5) {
    await prisma.user.create({
      data: {
        name: 'John',
        email: `john-${Math.random()}@example.com`
      }
    })
  }

  return (
    <main>
      <ul>
        {users.map(user => (
          <li key={user.id}>{user.email}</li>
        ))}
      </ul>
    </main>
  );
}

That would work. Some thoughts:

  • litefs works best if there is a very high read/write ratio. If that isn’t the case for your application, perhaps consider a different database.
  • If you are making internal http requests between machines, you can bypass the proxy. See Private Networking · Fly Docs, but essentially you can make http (not https) requests directly to the server which is running on a different port (perhaps 3001) using <region>.<appname>.internal.
  • you probably don’t want to make those routes available to the internet. Perhaps have these routes check to see if request.env.HTTP_FLY_REGION is set and, if so, return a 403 forbidden response.