Back to Blog
Tutorial9 min read2026-06-30

How to Deploy a Koa.js App to Production

Koa is the minimalist, middleware-first Node framework from the Express team. This tutorial covers its async middleware model, production error handling, and a clean container deploy with a managed database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

# How to Deploy a Koa.js App to Production

Koa was built by the original Express team as a leaner, more modern foundation. It ships almost nothing by default — no router, no body parser — and leans entirely on an elegant async middleware model built around async/await. That minimalism is great for control but means you assemble your production stack deliberately. This tutorial walks through a clean Koa deployment.

Koa's middleware model

Koa's defining feature is its onion model: middleware wraps the request both on the way in and the way out via await next().

import Koa from 'koa';
const app = new Koa();

// Timing middleware: runs before AND after downstream
app.use(async (ctx, next) => {
  const start = Date.now();
  await next();              // hand off to downstream middleware
  ctx.set('X-Response-Time', `${Date.now() - start}ms`);
});

Everything — routing, parsing, auth — is middleware you add explicitly.

Step 1: assemble the stack

A realistic Koa app pulls in a router and a body parser:

import Koa from 'koa';
import Router from '@koa/router';
import bodyParser from 'koa-bodyparser';

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

router.get('/health', (ctx) => { ctx.body = { ok: true }; });
router.get('/users/:id', async (ctx) => {
  ctx.body = await getUser(ctx.params.id);
});

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

Step 2: production-grade error handling

Because Koa is minimal, you own error handling. Put a try/catch middleware at the very top of the chain so it catches everything downstream:

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);   // for centralized logging
  }
});

app.on('error', (err) => console.error('Server error', err));

Never leak raw stack traces to clients in production.

Step 3: read config from the environment

const port = Number(process.env.PORT) || 8080;
app.listen(port, () => console.log(`Listening on ${port}`));

Step 4: wire a database

Koa does not care about your data layer; use a standard pool driven by an env var:

import pg from 'pg';
export const db = new pg.Pool({ connectionString: process.env.DATABASE_URL });

router.get('/todos', async (ctx) => {
  const { rows } = await db.query('SELECT * FROM todos ORDER BY id');
  ctx.body = rows;
});

Run migrations as a release step so the schema is ready before traffic.

Step 5: graceful shutdown

Koa apps should drain in-flight requests when the platform sends SIGTERM (e.g. during a deploy or a spot-node reclaim). Otherwise you drop live requests:

const server = app.listen(port);
process.on('SIGTERM', () => {
  server.close(() => {
    db.end().then(() => process.exit(0));
  });
});

Step 6: containerize

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

Koa vs Express, briefly

KoaExpress
Core sizeTiny, no built-insBatteries-ish (built-in router)
Async modelNative async/await onionCallback-based (async works but less native)
Error handlingYou build itBuilt-in error middleware
PhilosophyCompose what you needMore out of the box

Neither is "better" — Express is more convenient out of the box; Koa gives a cleaner async core for teams that prefer to assemble their own stack. Both deploy identically as Node containers.

Common pitfalls

  • No top-level error middleware. Unhandled errors crash the process or hang requests.
  • Forgetting allowedMethods(). You lose proper 405/501 responses.
  • Hardcoded port. Always read process.env.PORT.
  • No graceful shutdown. Deploys and node reclaims drop in-flight requests.
  • Body parser missing. ctx.request.body is undefined and POSTs silently fail.

Deploying on PandaStack

PandaStack auto-detects Node apps or builds from your Dockerfile, so the Koa container above ships from a git push. Provision a managed PostgreSQL and DATABASE_URL is injected automatically into your pg.Pool — no manual wiring. The platform sends SIGTERM for rolling deploys, so the graceful-shutdown handler above works as intended. You get custom domains with automatic SSL, live app logs to watch your error emitter, plus rollbacks and deploy history. The free tier (5 web services + 1 database) is ample for a Koa API with its database.

References

  • [Koa.js documentation](https://koajs.com/)
  • [@koa/router](https://github.com/koajs/router)
  • [koa-bodyparser](https://github.com/koajs/bodyparser)
  • [Node.js graceful shutdown patterns](https://nodejs.org/api/process.html#signal-events)

---

Koa rewards teams that like assembling a clean, minimal stack. PandaStack builds your Koa app from one git push and auto-wires the database so you can focus on middleware — 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