Node.js Production Deployment: Best Practices Guide
Node.js powers millions of production APIs, real-time applications, and backend services worldwide. But moving from node index.js in development to a resilient production deployment requires attention to containerization, process management, health checks, and environment configuration.
This guide covers the best practices for deploying Node.js applications to production using Docker and [PandaStack](https://pandastack.io).
Production vs Development: Key Differences
In development, you likely run your Node.js app directly, restart manually on crashes, and store configuration in a local .env file. In production:
- The app runs inside a Docker container
- The process must restart automatically on crash
- All secrets come from environment variables injected at runtime
- A health check endpoint tells the platform when the app is ready
Writing a Production-Ready Dockerfile
A multi-stage Docker build keeps your production image lean by excluding dev dependencies:
FROM node:20-alpine AS deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
ENV PORT=3000
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN addgroup --system appgroup && adduser --system --ingroup appgroup appuser
USER appuser
EXPOSE 3000
CMD ["node", "src/index.js"]Key practices in this Dockerfile:
npm ci --only=productioninstalls only production dependencies- A non-root user (
appuser) runs the process for security NODE_ENV=productionenables Express performance optimizations and disables dev tooling
Application Entry Point
Your application should start cleanly, bind to the port from the environment, and export a health check endpoint:
// src/index.js
const express = require('express')
const app = express()
const PORT = process.env.PORT || 3000
app.use(express.json())
app.get('/health', (req, res) => {
res.status(200).json({ status: 'ok', uptime: process.uptime() })
})
// your routes
app.use('/api', require('./routes'))
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`)
})
module.exports = appAlways read the port from process.env.PORT — cloud platforms, including PandaStack, inject the port dynamically.
Configuring pandastack.json
PandaStack uses a pandastack.json at your project root to understand your application:
{
"type": "container",
"healthCheckPath": "/health"
}The healthCheckPath tells PandaStack which endpoint to poll after startup. Traffic is only routed to your container once this endpoint returns HTTP 200, giving your app time to fully initialize — including database connections and cache warming.
Environment Variables
Never hardcode secrets. Set all environment variables from the PandaStack dashboard at [dashboard.pandastack.io](https://dashboard.pandastack.io):
NODE_ENV=production
PORT=3000
DATABASE_URL=postgresql://user:pass@host:5432/mydb
REDIS_URL=redis://redis.internal:6379
JWT_SECRET=your-jwt-signing-secret
API_KEY=third-party-api-keyIn your code, read these with process.env:
const { DATABASE_URL, JWT_SECRET, REDIS_URL } = process.env
if (!DATABASE_URL || !JWT_SECRET) {
console.error('Missing required environment variables')
process.exit(1)
}Failing fast on missing configuration prevents silent runtime errors in production.
Graceful Shutdown
Production Node.js apps should handle SIGTERM gracefully, finishing in-flight requests before exiting:
const server = app.listen(PORT)
process.on('SIGTERM', () => {
console.log('SIGTERM received, shutting down gracefully')
server.close(() => {
console.log('HTTP server closed')
process.exit(0)
})
})PandaStack sends SIGTERM before stopping a container during re-deployments, giving your app time to drain requests.
Connecting a Database
Provision a managed PostgreSQL, MySQL, MongoDB, or Redis instance from the Databases section of [dashboard.pandastack.io](https://dashboard.pandastack.io). Copy the connection string and set it as DATABASE_URL. Avoid creating database connections at module load time — use a connection pool that initializes asynchronously:
const { Pool } = require('pg')
const pool = new Pool({ connectionString: process.env.DATABASE_URL })
module.exports = { query: (text, params) => pool.query(text, params) }GitHub Integration and Auto-Deploy
Connect your GitHub repository from the PandaStack dashboard. Every merge to main triggers an automatic build and zero-downtime deployment. No manual steps, no SSH access required.
For manual or scripted deployments, use the CLI:
npm install -g @pandastack/cli
panda deployProduction Checklist
NODE_ENV=productionis set- Port is read from
process.env.PORT - All secrets are environment variables
/healthreturns HTTP 200 with a valid response- Container runs as a non-root user
SIGTERMhandler closes the server gracefully- Database uses a connection pool, not individual connections
Logging Best Practices
Node.js logs to stdout and stderr by default, which PandaStack captures and displays in your deployment dashboard. Avoid writing log files inside the container — they consume disk space and are lost when the container restarts.
For structured logging, consider a lightweight library like pino:
npm install pinoconst pino = require('pino')
const logger = pino({ level: process.env.LOG_LEVEL || 'info' })
logger.info({ port: PORT }, 'Server started')
logger.error({ err: error }, 'Unhandled error')Structured JSON logs make it easy to search for specific request IDs, error codes, or user sessions across your deployment history.
Visit [docs.pandastack.io](https://docs.pandastack.io) for the full deployment reference.