howto

NextJS with Prisma on Kubernetes

deploy as a knative service

tags
nextjs
javascript
prisma
knative
kubernetes

Now that we have our cluster up and running, lets look at how to build and deploy a NextJS app on it, including the database.

Create a NextJS app

We'll scaffold out a TypeScript app.

1
2
3
4
5
  npx create-next-app@latest --typescript  myapp 

  cd myapp

  npm run dev

Fireup a local data

1
  docker run -e POSTGRES_PASSWORD=awesome_password -p 5432:5432 postgres

Install prisma

We'll add the npm packages to our project.

1
2
3
  npm install prisma

  npx prisma init

Edit .env and change the url to be

1
  DATABASE_URL="postgresql://postgres:awesome_password@localhost:5432/mydb?schema=public"

Data Models

In primsa/schema.prisma add a model:

1
2
3
4
5
6
model Post {
  id        Int      @id @default(autoincrement())
  createdAt DateTime @default(now())
  title     String   @db.VarChar(255)
  content   String?
}

Run the migration

1
  npx prisma migrate dev --name init

Run prisma studio to check it out

1
  npx prisma studio

Write the post page and api

This is a basic demostration, so nothing fancy.

tsconfig.json

Add

"baseUrl" : "." and "strict":"false" to compilerOptions.

lib/prisma.ts

1
2
3
4
5
6
7
8
9
  import { PrismaClient } from '@prisma/client';

  declare global {
    var prisma: PrismaClient | undefined;
  }

  export const prisma = global.prisma || new PrismaClient();

  if (process.env.NODE_ENV !== 'production') global.prisma = prisma;

pages/posts.ts

We'll use react-hook-form to make things simplier.

1
  npm install react-hook-form

A couple points.

  1. We query the database in getServiceSideProps
  2. We are fetching our API from window.location.origin
  3. We are totally hacking the window reload, I'm just lazy
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
  import Head from 'next/head';
  import { prisma } from 'lib/prisma';
  import { Post } from '.prisma/client';
  import Link from 'next/link';
  import { useForm } from 'react-hook-form';

  export default function PostsPage({ posts }) {
    const {
      register,
      handleSubmit,
      formState: { errors, isSubmitted },
    } = useForm();

    const onSubmit = (data) => {
      try {
        fetch(`${window.location.origin}/api/create`, {
          body: JSON.stringify(data),
          headers: {
            'Content-Type': 'application/json',
          },
          method: 'POST',
        }).then( () => {
          window.location.href = window.location.href
         } )
      } catch (error) {
        throw new Error(error);
      }
    };

      return (
        <>
          <ul>
              {posts.map((item: Post) => (
                  <li key={item.id}>{item.title} - {item.content}</li>
              ))}
          </ul>
          <form onSubmit={handleSubmit(onSubmit)}>
            <label>Title</label>
            <input 
              id="title"
              type="text" 
              {...register('title', {required: true})}/>
              <br/>
            <label>Content</label>
            <input 
              type="text" 
              id="content"
              {...register('content', {required: true})}/>
            <input type="submit"/>
          </form>
          </>
      );
  }

  export const getServerSideProps = async () => {
    const posts = await prisma.post.findMany({
      select: {
        id: true,
        title: true,
        content: true
      },
    });

    console.log(posts);
    return {
      props: {
        posts,
      },
    };
  };

api/create.ts

This is the api request to handle the post. It runs on the server.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import { prisma } from 'lib/prisma';

export default async function handler(req, res) {
  const { content, title } = req.body;

  try {
    const feedback = await prisma.post.create({
      data: {
        content,
        title,
      },
    });
    res.status(200).json(feedback);
  } catch (error) {
    res.status(400).json({
      message: `Something went wrong :/ ${error}`,
    });
  }
}

Setup postgres on your cluster

1
2
  helm repo add bitnami https://charts.bitnami.com/bitnami
  helm upgrade --install postgres bitnami/postgresql

Eventually the database will be available on postgres-postgresql.default.svc.cluster.local

We can get the password using

1
  kubectl get secret --namespace default postgres-postgresql -o jsonpath="{.data.postgresql-password}" | base64 --decode

When we deploy the function we'll set the DATABASE_URL to look something like:

1
  DATABASE_URL="postgresql://postgres:1oOFIcOvB1@postgres-postgresql.default.svc.cluster.local:5432/database?schema=public"

You'll need to update that with your actual password of course.

Migrate the database

We are going to setup the database from our local enviornment. We'll use kubectl to forward the local postgres port to the remote install. Remember to stop your local postgres container

1
  kubectl port-forward svc/postgres-postgresql 5432:5432

Then, using the password that we found above, deploy the current migrations to the postgres that's being forwared on localhost. This is a different url then the previous one shown!:

1
2
  DATABASE_URL="postgresql://postgres:1oOFIcOvB1@localhost:5432/database?schema=public" \
              npx prisma migrate deploy

You can also run prisma studio that way, by the way.

Deploy the service

Build image

We need to tweaks the dockerfile from the official docs to add prisma generate to get the client code. So, copy over that Dockerfile and then, right after it copies the node_modules over added

RUN npx prisma generate

For me it's on like 14, right before yarn build.

Since I'm working off an M1 mac, I need to set the --platform to make sure that the image is built using the correct binaries. An example of this is:

1
  docker build . --platform linux/amd64 -t wschenk/prismatest && docker push wschenk/prismatest

Create the service

1
2
3
    kn service create prisma \
       --image wschenk/prismatest \
       --env DATABASE_URL="postgresql://postgres:1oOFIcOvB1@postgres-postgresql.default.svc.cluster.local:5432/database?schema=public"

Once this is up and running, you should be able to interact with your service and add messages.

Previously

howto

Serving a knative function on the root

root to services

tags
kubernetes
knative
kourier

Next

howto

Setting up knative eventing

lets send some messages

tags
knative
kubernetes
eventing