How to Deploy a Node + Postgres App with git push: A Practical Checklist
To deploy a Node.js app with Postgres from a git push, your app needs to do four things: start with one predictable command, listen on the port the platform hands it, read its database URL from the environment, and run its own schema migrations on startup. Get those right and most “it works on my machine” deploy failures never happen.
This checklist walks through each item in the order you’ll hit them, with the small code changes that matter. The examples use Candor Deploy where a platform detail is needed, but almost every point applies to any container-based git-push host.
Is your repo ready to be detected?
A git-push platform reads your repository and works out what it is. For Node, that means a package.json at the root of the app with:
- A
startscript that launches a long-running server, e.g."start": "node server.js". A script that runs and exits is not a server; the deploy will fail because nothing stays up. - A
buildscript if you compile anything (TypeScript, a bundler). Keep it separate fromstart. - An
engines.nodefield pinning the major Node version you test against, so the build doesn’t pick a different one. - A committed lockfile (
package-lock.json,pnpm-lock.yamloryarn.lock), so the build installs exactly what you tested.
On Candor Deploy, builds use Nixpacks, which detects Node, Python, Go, Ruby, PHP and static sites. If it finds your app but can’t tell how to start it, the deploy screen asks for a start command instead of failing later (quickstart). A Dockerfile in the repo is ignored unless you opt in, so a stale local-dev Dockerfile won’t break the build.
Does your app listen on $PORT?
This is the most common deploy failure, by a wide margin. Platforms route traffic to a port they choose and pass it to you in the PORT environment variable. Hard-code 3000 and the platform can’t reach you.
const port = process.env.PORT || 3000;
app.listen(port, '0.0.0.0', () => {
console.log(`listening on ${port}`);
});
Two details matter:
- Read
PORTfrom the environment, with a local fallback for development. - Bind to
0.0.0.0, notlocalhostor127.0.0.1. Inside a container, localhost only accepts connections from the container itself.
On Candor Deploy, PORT defaults to 8080 and the platform health-checks your app by connecting to that port. An app that doesn’t listen there sits in “deploying” and never goes live, and the build log tells you why (troubleshooting).
Where does the database URL come from?
Never commit a connection string. Read it from DATABASE_URL:
import pg from 'pg';
const pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });
Checklist for the database side:
- Create the database before (or alongside) the app. On Candor Deploy that’s + Database on the project card, choosing Postgres.
- Attach it rather than copy-pasting. The app’s Environment tab has Attach a database, which writes the connection URI into
DATABASE_URLfor you (databases docs). - Redeploy after any env change. Environment values apply on the next deploy, not to the process already running.
- Give a new database a moment. A freshly created Postgres database generates its credentials a few moments after creation. An app deployed in the same minute can beat it; redeploy once the database’s Connect tab shows a URI.
- Don’t expect to reach it from your laptop. Databases on Candor Deploy are private to your project’s services and have no public hostname. That’s intentional. Talk to the data through your app.
If you use an ORM, check its driver expectations. Most Node Postgres drivers accept postgresql:// URIs as-is.
Who runs the migrations?
Your app does. Many platforms have no separate release step, so the simplest reliable pattern is to migrate on startup, before the server starts listening:
"scripts": {
"build": "tsc",
"migrate": "node-pg-migrate up",
"start": "npm run migrate && node dist/server.js"
}
Swap in your tool of choice (Prisma migrate deploy, Knex migrate:latest, Drizzle, plain SQL files). Guidelines that save pain:
- Make migrations idempotent and forward-only. A restart re-runs the start command, so “already applied” must be a no-op.
- Separate destructive changes. Add a column in one deploy, backfill, then drop the old one in a later deploy. Your previous release may still be serving while the new one starts.
- Fail loudly. If a migration errors, exit non-zero so the deploy fails and you see it in the log, rather than starting a half-migrated app.
Are all your environment variables set?
Make a list of every process.env.X your code reads and check each one exists in the platform’s environment settings before the first deploy. A quick way to find them:
grep -rhoE "process\.env\.[A-Z0-9_]+" src | sort -u
Then fail fast at boot if a required one is missing:
for (const key of ['DATABASE_URL', 'SESSION_SECRET']) {
if (!process.env[key]) throw new Error(`Missing env var ${key}`);
}
A crash with a clear message on line one of the log beats a 500 error twenty minutes later. On Candor Deploy, variables go in the app’s Environment tab, one KEY=value per line, and they’re available during the build as well as at runtime, which matters for frontend bundles that bake values in.
Does the build stay inside the time limit?
Builds don’t run forever. On Candor Deploy a build has 15 minutes to finish (quickstart). If you’re close:
- Keep dev-only tooling in
devDependencies. - Don’t run your full test suite inside the build; run it in CI before the push.
- Avoid downloading large assets at build time that could be fetched once and committed or served elsewhere.
Can you see what happened?
Before you need them, know where your two logs live:
- Build log: why a build failed (missing dependency, compile error, wrong Node version).
- Runtime log: what your app printed after it started (crashes, connection errors, your own
console.log).
On Candor Deploy, clicking a deployment in the Deployments tab expands its build log, and the Logs tab streams runtime output. Log something useful at startup: the port you bound, the Node version, and whether migrations ran.
Do you have a way back?
Every deploy should be reversible in one step. On Candor Deploy, each previous deployment in the history has a Rollback button that redeploys that exact commit, and Deploy a commit rebuilds any commit by SHA (quickstart). Combined with forward-only migrations, that means a bad release is a click away from undone. Your schema stays compatible because you didn’t drop anything in the same deploy.
The checklist, in one place
package.jsonhasstart(long-running),build(if needed),engines.node, and a committed lockfile.- Server reads
process.env.PORTand binds0.0.0.0. - Database URL comes from
DATABASE_URL, never from the repo. - Database created and attached; app redeployed after.
- Migrations run on start, are idempotent, and fail loudly.
- Every
process.envkey is set; missing ones crash at boot with a clear message. - Build fits inside the time limit; tests run before the push.
- You know where the build and runtime logs are.
- You know how to roll back.
Try it on a real app
If you want to run through this end to end, the Candor Deploy quickstart goes from a new account to a live HTTPS URL, and Databases on Candor Deploy covers the Postgres side in detail. If something in your deploy doesn’t match this checklist, the troubleshooting guide is the next stop.