Back to Blog
Tutorial9 min read2026-07-01

How to Deploy a Koa.js API with PostgreSQL

Koa is a minimal, middleware-driven Node framework that leaves architecture up to you. This guide covers structuring a Koa API with a PostgreSQL pool, graceful shutdown, and a clean container deploy.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Koa is the spiritual successor to Express from the same team — smaller, built around async middleware, and deliberately unopinionated. That minimalism means *you* own the production concerns Koa doesn't ship: structured error handling, connection pooling, and graceful shutdown. Here's how to deploy a Koa + PostgreSQL API properly.

A minimal but production-shaped Koa app

npm install koa @koa/router pg
// src/app.js
import Koa from 'koa';
import Router from '@koa/router';
import pg from 'pg';

const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  idleTimeoutMillis: 30000
});

const app = new Koa();
const router = new Router();

router.get('/health', (ctx) => { ctx.body = { status: 'ok' }; });

router.get('/api/posts', async (ctx) => {
  const { rows } = await pool.query('SELECT * FROM posts LIMIT 20');
  ctx.body = rows;
});

app.use(router.routes()).use(router.allowedMethods());

export { app, pool };

Koa has no built-in error handler, so add one as the first middleware. This is the single most important production addition:

app.use(async (ctx, next) => {
  try {
    await next();
  } catch (err) {
    ctx.status = err.status || 500;
    ctx.body = { error: ctx.status === 500 ? 'Internal Server Error' : err.message };
    ctx.app.emit('error', err, ctx);
  }
});

Put this *before* your router so it catches everything downstream. Without it, an unhandled rejection in a route can crash the process.

The server entry point

Separate app definition from server startup so you can test the app and handle shutdown cleanly:

// src/server.js
import { app, pool } from './app.js';

const port = Number(process.env.PORT ?? 3000);
const server = app.listen(port, '0.0.0.0', () => {
  console.log(`koa listening on :${port}`);
});

// Graceful shutdown: drain the server, then close the pool
process.on('SIGTERM', () => {
  server.close(async () => {
    await pool.end();
    process.exit(0);
  });
});

Graceful shutdown matters on any platform that restarts containers (deploys, autoscaling). Handling SIGTERM lets in-flight requests finish and closes DB connections so you don't leak them.

Containerize

FROM node:20-slim
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
ENV NODE_ENV=production
EXPOSE 3000
CMD ["node", "src/server.js"]

Koa has no build step for plain JS, so the Dockerfile is dead simple. Bind to 0.0.0.0 (done above) so the platform proxy can reach the app.

Connection pooling done right

The pg.Pool is the part people misconfigure. Two rules:

  • One pool per process, shared across requests. Never create a pool per request.
  • Size the pool to the database, not the app. If your managed Postgres allows 50 connections and you run 5 replicas, each pool's max should be well under 50 / 5.

Managed databases publish a connection limit — respect it. PandaStack's plans, for example, define a max connection count per tier; size your pools to stay comfortably below it across all replicas.

Deploying on PandaStack

  1. 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects DATABASE_URL — your pg.Pool reads it directly.
  2. 2Connect the Git repo as a container app. The Dockerfile is auto-detected; otherwise the Node buildpack runs it.
  3. 3Set NODE_ENV=production. PORT is provided by the platform; the code already reads it.
  4. 4Add a custom domain; SSL is automatic.
  5. 5Scale replicas as needed, keeping total pool connections under the DB limit.

Migrations

Koa is unopinionated about migrations. A common, dependency-light choice is node-pg-migrate:

npm install -D node-pg-migrate
npx node-pg-migrate up   # run as a deploy step

Run migrations once per deploy before the new version takes traffic.

Verifying

curl -s https://api.example.com/health
# {"status":"ok"}

curl -s https://api.example.com/api/posts | head

A healthy /health plus real rows from /api/posts confirms the pool is connected.

References

  • [Koa documentation](https://koajs.com/)
  • [node-postgres connection pooling](https://node-postgres.com/features/pooling)
  • [@koa/router](https://github.com/koajs/router)
  • [node-pg-migrate](https://salsita.github.io/node-pg-migrate/)

---

Koa's tiny container plus a managed PostgreSQL is a clean PandaStack deploy, with DATABASE_URL injected and connection limits documented per plan. Start free 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