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
maxshould be well under50 / 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
- 1Provision a managed PostgreSQL (14.x or 16.x). PandaStack injects
DATABASE_URL— yourpg.Poolreads it directly. - 2Connect the Git repo as a container app. The Dockerfile is auto-detected; otherwise the Node buildpack runs it.
- 3Set
NODE_ENV=production.PORTis provided by the platform; the code already reads it. - 4Add a custom domain; SSL is automatic.
- 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 stepRun 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 | headA 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).