Skip to content

Deploy Node.js Applications

Framework-specific guide for deploying Node.js apps to Airbase

This guide covers Node.js-specific deployment patterns, best practices, and common issues.


Quick Start

Complete example available: Node.js Express Example

Basic workflow:

# 1. Create Dockerfile
# 2. Build container
airbase container build

# 3. Deploy
airbase container deploy --yes staging

Time: 5 minutes


Dockerfile Pattern for Node.js

Pattern:

# Stage 1: Install dependencies
FROM gdssingapore/airbase:node-22-builder AS builder
WORKDIR /app
COPY --chown=app:app package.json package-lock.json ./
RUN npm ci --only=production

# Stage 2: Runtime
FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --chown=app:app --from=builder /app/node_modules ./node_modules
COPY --chown=app:app package.json ./
COPY --chown=app:app src ./src
USER app
CMD ["node", "src/index.js"]

Why multi-stage? - ✅ Smaller final image (~200MB vs ~1GB) - ✅ No build tools in production - ✅ Faster deployments - ✅ Better security

Single-Stage Build (Simple)

When to use: Very simple apps, rapid prototyping

FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --chown=app:app package.json package-lock.json ./
RUN npm ci --only=production
COPY --chown=app:app . ./
USER app
CMD ["node", "index.js"]

Required Configuration

1. Bind to 0.0.0.0

Express:

const PORT = process.env.PORT || 3000;
app.listen(PORT, '0.0.0.0', () => {
  console.log(`Server running on port ${PORT}`);
});

Fastify:

fastify.listen({
  port: process.env.PORT || 3000,
  host: '0.0.0.0'
});

Next.js:

// next.config.js
module.exports = {
  server: {
    host: '0.0.0.0',
    port: process.env.PORT || 3000
  }
}

package.json:

{
  "type": "module",
  "engines": {
    "node": ">=22.0.0"
  }
}

3. Health Check Endpoint

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

Common Frameworks

Express.js

See: Node.js Express Example for complete working code

Key points: - Use express.json() middleware - Bind to 0.0.0.0 - Read PORT from environment - Add health check endpoint

Fastify

package.json:

{
  "dependencies": {
    "fastify": "^4.26.0"
  }
}

index.js:

import Fastify from 'fastify';

const fastify = Fastify({ logger: true });
const PORT = parseInt(process.env.PORT || '3000', 10);

fastify.get('/health', async () => {
  return { status: 'ok' };
});

await fastify.listen({ port: PORT, host: '0.0.0.0' });

Next.js

Dockerfile:

FROM gdssingapore/airbase:node-22-builder AS builder
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . ./
RUN npm run build

FROM gdssingapore/airbase:node-22
WORKDIR /app
COPY --chown=app:app --from=builder /app/.next ./.next
COPY --chown=app:app --from=builder /app/node_modules ./node_modules
COPY --chown=app:app --from=builder /app/package.json ./
COPY --chown=app:app --from=builder /app/public ./public
USER app
CMD ["npm", "start"]

package.json:

{
  "scripts": {
    "start": "next start -p ${PORT:-3000} -H 0.0.0.0"
  }
}


Common Issues

Issue: "Cannot find module"

Cause: Dependencies not installed in Docker image

Solution: - Check COPY --from=builder /app/node_modules line exists - Verify npm ci runs in builder stage - Check .dockerignore doesn't exclude necessary files

Issue: "EADDRINUSE: address already in use"

Cause: Port already taken or not binding to correct interface

Solution: - Ensure binding to 0.0.0.0 not localhost - Use process.env.PORT for port number

Issue: "Permission denied"

Cause: Files owned by root instead of app user

Solution: - Use --chown=app:app in all COPY commands - Ensure USER app before CMD - Check no RUN chmod commands creating wrong permissions

Issue: Large image size (>500MB)

Cause: Not using multi-stage build or including dev dependencies

Solution: - Use multi-stage build pattern - Use npm ci --only=production - Check .dockerignore excludes node_modules, tests, etc.


Best Practices

1. Use Lockfiles

Always commit: package-lock.json

In Dockerfile: Use npm ci not npm install

COPY package.json package-lock.json ./
RUN npm ci --only=production

2. Minimize Dependencies

Check unused packages:

npx depcheck

Remove devDependencies from production:

npm ci --only=production

3. Security Scanning

Audit dependencies:

npm audit
npm audit fix

4. Environment Variables

Read from process.env:

const config = {
  port: process.env.PORT || 3000,
  nodeEnv: process.env.NODE_ENV || 'development',
  databaseUrl: process.env.DATABASE_URL
};

Never hardcode secrets:

// ❌ Bad
const apiKey = 'hardcoded-secret';

// ✅ Good
const apiKey = process.env.API_KEY;

5. Graceful Shutdown

const server = app.listen(PORT, '0.0.0.0');

process.on('SIGTERM', () => {
  server.close(() => {
    console.log('Server closed');
    process.exit(0);
  });
});

Performance Optimization

Enable Production Mode

Set NODE_ENV in Dockerfile:

ENV NODE_ENV=production

Or in .env:

NODE_ENV=production

Use Clustering (Optional)

For CPU-intensive apps:

import cluster from 'cluster';
import os from 'os';

if (cluster.isPrimary) {
  const numCPUs = os.cpus().length;
  for (let i = 0; i < numCPUs; i++) {
    cluster.fork();
  }
} else {
  // Worker process runs app
  app.listen(PORT, '0.0.0.0');
}

Checklist

Before deploying:

  • Dockerfile uses multi-stage build
  • App binds to 0.0.0.0
  • PORT read from environment variable
  • Health check endpoint implemented
  • Using npm ci --only=production
  • All files owned by app:app
  • USER app before CMD
  • .dockerignore excludes unnecessary files
  • No hardcoded secrets in code
  • package-lock.json committed

See Also