Back to Blog
Tutorial12 min read2026-08-13

From Local Dev to Live API — Fastify and PostgreSQL via the CLI

Deploy a Fastify REST API with a managed PostgreSQL database using the panda CLI — database provisioning, connection pooling, environment injection, and log tailing.

Ajay Kumar
Ajay Kumar
Founder & DevOps, PandaStack

Fastify is one of the fastest Node.js frameworks for building APIs, but production deployments require more than npm start: you need a real database, environment variables that differ from your .env.local, health checks that prove the app is reachable, and a way to stream logs when things break. Running all of that on your laptop works until you want someone else to use the API.

The gap between local and production is smaller than it used to be. Modern platforms provision a managed database, wire it to your app automatically via DATABASE_URL, and handle TLS, backups, and connection limits so you never touch a psql shell unless you want to. The CLI is the fastest path from a working repo to a live endpoint when you already have the code and just need infrastructure.

Install the CLI and authenticate

The panda binary handles project creation, database provisioning, log tailing, and deployments without leaving the terminal. Install it globally and log in with your PandaStack credentials:

npm install -g @pandastack/cli
panda login

The login flow opens a browser, redirects back to the CLI, and stores a token in ~/.pandastack/config.json. From that point forward, all commands run against your organization without needing to pass credentials.

Check that authentication worked:

panda status

You should see your organization name, active projects, and resource usage. If the command errors with a 401, the token is invalid or expired — run panda logout && panda login to refresh it.

Create a managed PostgreSQL database

Fastify connects to Postgres via a connection string in the DATABASE_URL environment variable. Local development usually points at a Docker container or a SQLite file; production needs a real database that survives container restarts, has backups, and can scale beyond a single-core limit.

Provision a managed Postgres instance:

panda databases create \
  --name fastify-db \
  --engine postgres \
  --version 16

The platform spins up a PostgreSQL 16 instance via KubeBlocks on Kubernetes, allocates 1 CPU, 2 GB RAM, and 10 GB disk (the standard database tier), and configures daily backups with 7-day retention on the free plan. The command returns a database ID and connection string.

The connection string looks like:

postgres://user:password@host.pandastack.app:5432/fastify-db?sslmode=require

You can test it locally with psql or any Postgres client. SSL is enforced, so the sslmode=require parameter is mandatory.

Link the database to your app so DATABASE_URL is injected automatically

Instead of copy-pasting the connection string into your project settings, link the database at deploy time. The platform injects DATABASE_URL as an environment variable, and your Fastify app reads it from process.env.DATABASE_URL without hard-coding credentials.

Create the project and link the database in one command:

panda projects create \
  --name fastify-api \
  --repo yourname/fastify-api \
  --branch main \
  --type container \
  --link-database fastify-db

The platform:

  1. 1Clones the repo and detects package.json
  2. 2Runs npm install && npm run build (or skips the build step if there is no build script)
  3. 3Detects the start command from package.json scripts or defaults to node index.js
  4. 4Injects DATABASE_URL and any other environment variables you configured
  5. 5Starts the container and waits for a health check on port 3000

If the health check times out, the deploy fails. This is almost always because Fastify is binding to 127.0.0.1 instead of 0.0.0.0.

Fix the localhost binding bug that breaks every production deploy

Fastify defaults to 127.0.0.1, which means "only accept connections from this machine." Kubernetes routes traffic from an external load balancer to the pod, so requests arrive from a different IP address. The app is running, the port is open, but the health check fails because nothing is listening on the interface the probe hits.

Change your server setup from:

const fastify = require('fastify')({ logger: true });

fastify.listen({ port: 3000 }, (err) => {
  if (err) throw err;
});

to:

const fastify = require('fastify')({ logger: true });

fastify.listen({ port: 3000, host: '0.0.0.0' }, (err) => {
  if (err) throw err;
});

The host: '0.0.0.0' parameter tells Fastify to listen on all network interfaces. Push the change, then redeploy:

panda projects deploy <project-id>

The health check passes, the deploy completes, and the API is live.

Connect to the database with connection pooling

Your Fastify app needs a Postgres client. pg is the standard low-level driver; @fastify/postgres is a Fastify plugin that wraps it and handles connection pooling automatically.

Install the plugin:

npm install @fastify/postgres

Register it in your app:

const fastify = require('fastify')({ logger: true });

fastify.register(require('@fastify/postgres'), {
  connectionString: process.env.DATABASE_URL,
});

fastify.get('/users', async (request, reply) => {
  const client = await fastify.pg.connect();
  try {
    const { rows } = await client.query('SELECT id, email FROM users');
    return rows;
  } finally {
    client.release();
  }
});

fastify.listen({ port: 3000, host: '0.0.0.0' }, (err) => {
  if (err) throw err;
});

The plugin creates a connection pool and reuses connections across requests. Releasing the client after the query returns it to the pool instead of closing the connection, which avoids the overhead of TLS handshakes on every request.

Free-tier databases have a 50-connection limit. If your app leaks connections by forgetting client.release(), you will hit the limit and start seeing "sorry, too many clients already" errors. Always release clients in a finally block to guarantee cleanup even when queries throw.

Run database migrations before the app starts

Fastify does not have a built-in migration system, so most projects use a library like node-pg-migrate or Knex. If your migrations live in a migrations/ directory, you need to run them after the database is provisioned but before the app starts handling requests.

The cleanest approach is a npm run migrate script in package.json:

{
  "scripts": {
    "start": "node index.js",
    "migrate": "node-pg-migrate up --database-url-var DATABASE_URL"
  }
}

Then override the start command in your project settings to run migrations first:

panda projects create \
  --name fastify-api \
  --repo yourname/fastify-api \
  --branch main \
  --type container \
  --link-database fastify-db \
  --start-command "npm run migrate && npm start"

The deploy runs migrations, then starts the server. If a migration fails, the deploy fails, and the previous version keeps running. This prevents a broken schema from taking down the app.

Stream logs to debug a failed query

Fastify logs to stdout by default when logger: true is set. PandaStack captures stdout and makes it searchable in the dashboard under the Logs tab. You can also tail logs from the CLI:

panda projects logs <project-id> --follow

This streams live logs until you press Ctrl+C. If a query fails, you will see the error message, stack trace, and the SQL that caused it.

The CLI log streaming is currently unreliable for long-running tails, so the dashboard Logs tab is the better choice when you need to scroll back through hours of output or search for a specific error pattern.

Redeploy after schema changes

Every time you add a migration, commit it to the repo and push. If autoDeploy is enabled, the platform detects the commit, runs the build, executes migrations, and starts the new version. The old version keeps serving traffic until the new one passes health checks, so there is no downtime during the switch.

If a migration is destructive (dropping a column that the old code still reads), you need a two-step deploy: first deploy code that stops using the column, then deploy the migration that removes it. This is the standard zero-downtime migration strategy and applies to any platform.

Add environment variables for API keys and secrets

Your app probably needs more than DATABASE_URL — third-party API keys, JWT signing secrets, feature flags. Add them via the CLI:

panda projects env <project-id> --set STRIPE_SECRET_KEY=sk_live_xxx
panda projects env <project-id> --set JWT_SECRET=random-256-bit-string

The environment is encrypted at rest and injected into the container at startup. You can list all variables with:

panda projects env <project-id> --list

Secrets are masked in the output, so you cannot accidentally leak them in a screenshot or log dump.

References

  • [Fastify documentation](https://www.fastify.io/)
  • [PandaStack CLI commands](https://docs.pandastack.io/cli/commands/)
  • [Managed PostgreSQL guide](https://docs.pandastack.io/databases/)

Ready to deploy?

Start free on PandaStack.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also