Express apps fail in production for predictable reasons: the build command runs a dev server, environment variables are missing, the health check probes the wrong path, or a bad deploy breaks everything with no rollback. This guide walks through each step and the commands that fix it.
Build
For pure API servers with no frontend build step, the build phase is just dependency installation. PandaStack runs npm install automatically when it detects a package.json. If you use Yarn, Pnpm, or Bun, the platform detects the lock file and uses the right package manager.
Override the install command if needed:
{
"type": "container",
"language": "nodejs",
"buildCommand": "npm ci"
}Do not set buildCommand to npm run dev — that starts the development server, not a production build. If your app requires a transpilation step (TypeScript, Babel), add a build script to package.json:
{
"scripts": {
"build": "tsc",
"start": "node dist/server.js"
}
}Then set buildCommand to npm run build in pandastack.json.
Run
The platform executes the start script from package.json:
{
"scripts": {
"start": "node app.js"
}
}Your start command must:
- 1Bind to
0.0.0.0, not127.0.0.1 - 2Listen on
process.env.PORT(PandaStack injects it, default 3000) - 3Exit with a non-zero code on fatal errors so the platform restarts the process
Example:
const express = require('express');
const app = express();
app.get('/health', (req, res) => {
res.json({ status: 'healthy' });
});
const PORT = process.env.PORT || 3000;
const server = app.listen(PORT, '0.0.0.0', () => {
console.log(`Listening on port ${PORT}`);
});
process.on('SIGTERM', () => {
server.close(() => process.exit(0));
});Deploy via the CLI:
panda projects create \
--name express-api \
--repo yourorg/express-api \
--branch main \
--auto-deployThe --auto-deploy flag makes PandaStack redeploy automatically on every push to main.
Environment variables
Inject secrets without committing them:
panda projects env set express-api DATABASE_URL=postgresql://user:pass@host/db
panda projects env set express-api JWT_SECRET=your-secret-key
panda projects env set express-api NODE_ENV=productionRead them in your code:
const dbUrl = process.env.DATABASE_URL;
const jwtSecret = process.env.JWT_SECRET;Declare required variables in pandastack.json so deployers are prompted:
{
"type": "container",
"env": [
{ "name": "DATABASE_URL", "description": "PostgreSQL connection string" },
{ "name": "JWT_SECRET", "description": "Secret for signing tokens" }
]
}Custom domain
Add your domain in the dashboard under Project Settings → Domains. You get a CNAME target like express-api.pandastack-dns.com. Point your DNS:
api.yourdomain.com CNAME express-api.pandastack-dns.comPandaStack provisions a Let's Encrypt TLS certificate automatically. HTTPS works within minutes of DNS propagation.
Rollback
A bad deploy breaks your API. The logs show an error, and you need to revert immediately. List recent deployments:
panda projects info express-apiThe output includes deployment IDs. Roll back to the last working one:
panda projects deploy express-api --deployment-id abc123Or use the dashboard: navigate to the project → Deployments tab → click "Rollback" next to the known-good deployment. The platform redeploys that exact commit and environment variable snapshot.
Zero-downtime redeploys
When you push a new commit, PandaStack:
- 1Builds the new image
- 2Starts new pods running the updated code
- 3Waits for health checks to pass
- 4Routes traffic to the new pods
- 5Sends SIGTERM to the old pods
- 6Waits up to 30 seconds for graceful shutdown
- 7Kills any remaining old pods
If the new pods fail health checks, the old ones keep running and the deploy is marked as failed. Traffic never hits the broken version.
Handle SIGTERM to avoid dropping in-flight requests:
process.on('SIGTERM', () => {
console.log('Shutting down gracefully');
server.close(() => {
console.log('HTTP server closed');
// Close database connections, flush logs, etc.
process.exit(0);
});
setTimeout(() => {
console.error('Forced shutdown after 30s');
process.exit(1);
}, 30000);
});Deploy from a CI pipeline
Generate a psk_ token in the dashboard (Settings → API Keys) and add it to your CI secrets as PANDASTACK_TOKEN. Then trigger a deploy on every merge to main:
# .github/workflows/deploy.yml
name: Deploy
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Trigger deploy
run: |
curl -X POST https://api.pandastack.io/v1/projects/express-api/deploy \
-H "Authorization: Bearer ${{ secrets.PANDASTACK_TOKEN }}"The API returns a deploymentId. Poll the status endpoint or stream logs if you need to block the CI job until the deploy completes.
Health checks and startup probes
The platform probes GET / by default. If your API does not expose a root route, set a custom health check path:
{
"type": "container",
"healthCheckPath": "/health"
}The probe expects a 2xx or 3xx response. If the endpoint returns 5xx or times out after 10 seconds, the platform marks the pod as unhealthy and restarts it.
For slow-starting apps (database migrations on boot, pre-warming caches), increase the startup timeout in your pandastack.json or via the dashboard.
Every step here traces to a real command. Use this as a checklist for production deployments.
References
- [PandaStack deployment docs](https://docs.pandastack.io)
- [Express error handling](https://expressjs.com/en/guide/error-handling.html)
- [Node.js graceful shutdown patterns](https://nodejs.org/en/docs/guides/nodejs-docker-webapp/)