Back to Blog
Tutorial11 min read2026-06-28

How to Deploy an Express API with PostgreSQL

Deploy a production Express API backed by PostgreSQL: connection pooling, migrations, graceful shutdown, health checks, and a Git deploy with an auto-wired managed database.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Express remains the most common way to build a Node.js API, and pairing it with PostgreSQL is a default for good reason. The gap between a working local app and a robust production deployment is mostly about how you manage the database connection. This guide focuses on that, plus the deployment mechanics.

Pool, don't connect per request

The single most important production rule: use a connection pool. Opening a new PostgreSQL connection per request is slow and quickly exhausts the server's connection limit. With pg:

import pg from 'pg'

export const pool = new pg.Pool({
  connectionString: process.env.DATABASE_URL,
  max: 10,
  idleTimeoutMillis: 30_000,
  connectionTimeoutMillis: 5_000,
  ssl: process.env.PGSSL ? { rejectUnauthorized: false } : undefined
})

Size max carefully. If you run 4 instances with max: 10, that's 40 connections. Stay under your database's limit (the PandaStack Free tier allows 50 connections; Pro allows 300).

A clean query layer

import { pool } from './db.js'

export async function getUser(id) {
  const { rows } = await pool.query(
    'SELECT id, email FROM users WHERE id = $1',
    [id]
  )
  return rows[0]
}

Always use parameterized queries ($1, $2) — never string interpolation — to prevent SQL injection.

Migrations

Keep schema changes in versioned migration files. A lightweight tool like node-pg-migrate works well:

npx node-pg-migrate up

Run migrations as a separate deploy step before the new app version takes traffic. Make changes backward-compatible across one release (add nullable columns first, enforce constraints later) so rolling deploys don't break in-flight instances.

Health checks and graceful shutdown

app.get('/healthz', (req, res) => res.json({ status: 'ok' }))

app.get('/readyz', async (req, res) => {
  try {
    await pool.query('SELECT 1')
    res.json({ ready: true })
  } catch {
    res.status(503).json({ ready: false })
  }
})

const server = app.listen(process.env.PORT || 3000, '0.0.0.0')

process.on('SIGTERM', async () => {
  server.close(async () => {
    await pool.end()
    process.exit(0)
  })
})

Closing the pool on SIGTERM lets in-flight queries finish and prevents "connection terminated unexpectedly" errors in your logs during deploys.

Dockerfile

FROM node:20-slim AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .

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

Deploying on PandaStack

This is where the managed database pays off:

  1. 1Provision a managed PostgreSQL instance (14.x or 16.x) from the [dashboard](https://dashboard.pandastack.io).
  2. 2Connect your Express repo as a container app. PandaStack auto-detects Node — you may not even need the Dockerfile above.
  3. 3Attaching the database injects DATABASE_URL automatically, so the pg.Pool config above works with zero changes.
  4. 4Add a release step to run your migrations.
  5. 5Point the readiness probe at /readyz.

Images build with rootless BuildKit in ephemeral Kubernetes Jobs and deploy via Helm. You get automatic SSL, live logs (self-hosted Elasticsearch), and server-side metrics without instrumenting your code.

Connection budgeting

PlanMax DB connectionsSuggested pool max × instances
Free50e.g. 10 × 4 = 40
Pro300e.g. 20 × 10 = 200
Premium1000scale accordingly

If you scale horizontally, lower per-instance max or add a pooler. Leave headroom for migrations and admin tools that also consume connections.

Backups

Managed PostgreSQL includes scheduled and manual backups, with 7-day retention on Free and longer on paid plans. Take a manual backup before running a risky migration so you have a clean restore point.

References

  • [node-postgres pooling guide](https://node-postgres.com/features/pooling)
  • [PostgreSQL connection settings](https://www.postgresql.org/docs/current/runtime-config-connection.html)
  • [Express production best practices](https://expressjs.com/en/advanced/best-practice-performance.html)
  • [OWASP SQL Injection Prevention Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/SQL_Injection_Prevention_Cheat_Sheet.html)

Want your Express API and PostgreSQL auto-wired together? PandaStack's free tier includes a managed database with automatic DATABASE_URL injection — start 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