Deploying a Node.js / Express App with Neo

A minimal Dockerfile and .neo.yml for a Node API — port detection, env vars, and a PM2-free process model.

5 min read

A minimal Dockerfile

FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
EXPOSE 3000
CMD ["node", "server.js"]

Neo reads the EXPOSE 3000 line to auto-detect the port when nothing else specifies it — no config needed for a first deploy.

No process manager required

Skip PM2, forever, or nodemon inside the container. Docker's own restart policy (restart: unless-stopped, Neo's default) handles crash recovery, and Neo's blue-green swap handles zero-downtime deploys at the container level — a process manager inside the container is solving a problem Neo already solves outside it.

.neo.yml

name: api
port: 3000
env:
  NODE_ENV: production
env_file: .env.production
neo deploy --domain api.example.com

Background jobs

If the app has a separate worker process (a BullMQ consumer, a cron-like loop), give it its own entry rather than running it inside the same container as the HTTP server:

workers:
  jobs:
    command: node worker.js

This runs as its own container sharing the app's image and environment — so a crash in the worker never takes the API down with it, and vice versa.

Multi-stage builds for smaller images

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build

FROM node:22-alpine
WORKDIR /app
COPY --from=build /app/dist ./dist
COPY --from=build /app/node_modules ./node_modules
EXPOSE 3000
CMD ["node", "dist/server.js"]

Neo builds whichever stage is last in the file by default — nothing special to configure for a multi-stage Dockerfile.

Try it yourself — Neo is free, with every feature unlocked.

Get started free