Back to Blog
Tutorial10 min read2026-07-02

How to Deploy a GraphQL Yoga Server with a Database

A practical walkthrough for deploying a GraphQL Yoga server backed by PostgreSQL: containerizing the app, wiring up the database connection, and shipping it to production with a git push.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

GraphQL Yoga is a lightweight, spec-compliant GraphQL server built on top of the [graphql](https://graphql.org) reference implementation and the WHATWG Fetch API. It runs anywhere a standard Request/Response runtime exists — Node.js, Bun, Deno, Cloudflare Workers — which makes it one of the easiest GraphQL servers to containerize. In this tutorial we'll build a small Yoga server, connect it to a managed PostgreSQL database, and deploy it to production.

Why GraphQL Yoga

Yoga sits in a sweet spot between bare graphql-http and the heavier Apollo Server. You get subscriptions, file uploads, a built-in GraphiQL playground, and plugin support out of the box, with very little ceremony. If you're starting fresh in 2026, it's a sensible default.

Project setup

Start with a minimal Node.js project. We'll use [Prisma](https://www.prisma.io) as the database client because its generated types pair nicely with GraphQL resolvers.

npm init -y
npm install graphql-yoga graphql @prisma/client
npm install -D prisma typescript tsx @types/node
npx prisma init --datasource-provider postgresql

Define a tiny schema in prisma/schema.prisma:

model Post {
  id        Int      @id @default(autoincrement())
  title     String
  body      String
  createdAt DateTime @default(now())
}

Now the server in src/index.ts:

import { createServer } from 'node:http'
import { createSchema, createYoga } from 'graphql-yoga'
import { PrismaClient } from '@prisma/client'

const prisma = new PrismaClient()

const schema = createSchema({
  typeDefs: /* GraphQL */ `
    type Post { id: Int!, title: String!, body: String! }
    type Query { posts: [Post!]! }
    type Mutation { addPost(title: String!, body: String!): Post! }
  `,
  resolvers: {
    Query: { posts: () => prisma.post.findMany() },
    Mutation: {
      addPost: (_, { title, body }) => prisma.post.create({ data: { title, body } }),
    },
  },
})

const yoga = createYoga({ schema })
createServer(yoga).listen(process.env.PORT || 4000)

Note process.env.PORT — your platform injects the port, so never hardcode it.

The database connection

Prisma reads DATABASE_URL from the environment. Locally that lives in .env. In production you want a managed database so you're not babysitting Postgres yourself. On PandaStack, when you provision a managed PostgreSQL instance and attach it to your service, the DATABASE_URL is injected automatically — Prisma picks it up with no extra config. That auto-wiring is the single biggest time-saver here; getting connection strings, SSL modes, and credentials right by hand is where most first deploys stall.

Containerizing

You can let buildpacks auto-detect a Node app, but for a Prisma project an explicit Dockerfile is cleaner because you must run prisma generate at build time:

FROM node:20-slim
RUN apt-get update && apt-get install -y openssl && rm -rf /var/lib/apt/lists/*
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npx prisma generate
EXPOSE 4000
CMD ["npx", "tsx", "src/index.ts"]

The openssl package matters — Prisma's query engine needs it, and slim images often omit it, producing a confusing runtime error.

Running migrations

Migrations should run as a release step, not on every container start (otherwise concurrent replicas race each other). A clean pattern is a one-off cronjob or a pre-deploy command:

npx prisma migrate deploy

Run this once against the production database before your first real traffic. On subsequent deploys, only run it when the schema changed.

Deploying

Push your repo and connect it. PandaStack detects the Dockerfile, builds the image in an ephemeral rootless BuildKit job, pushes to the registry, and rolls out via Helm. Steps:

  1. 1Create a managed PostgreSQL database (14.x or 16.x).
  2. 2Create a container service from your Git repo.
  3. 3The DATABASE_URL is wired in automatically.
  4. 4Run prisma migrate deploy once.
  5. 5Open the live URL and hit /graphql for GraphiQL.
ConcernLocalProduction
Porthardcoded okprocess.env.PORT
DB URL.envinjected DATABASE_URL
Migrationsmigrate devmigrate deploy (release step)
GraphiQLonconsider disabling for public APIs

Production hardening

  • Disable introspection/GraphiQL on public-facing APIs, or gate them, to reduce schema enumeration. Pass graphiql: false to createYoga.
  • Add depth/cost limiting with @escape.tech/graphql-armor to prevent malicious deeply-nested queries from exhausting the database.
  • Connection pooling: Prisma opens a pool per instance. If you scale to many replicas, put a pooler (PgBouncer or Prisma Accelerate) in front of Postgres so you don't exhaust the connection limit.

References

  • [GraphQL Yoga docs](https://the-guild.dev/graphql/yoga-server/docs)
  • [Prisma with PostgreSQL](https://www.prisma.io/docs/orm/overview/databases/postgresql)
  • [GraphQL Armor (security)](https://github.com/Escape-Technologies/graphql-armor)
  • [GraphQL spec](https://spec.graphql.org/)

Want to skip the connection-string fiddling? PandaStack's free tier includes a managed database and container services, and the DATABASE_URL is auto-wired the moment you attach it. Spin one up at [dashboard.pandastack.io](https://dashboard.pandastack.io).

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also