Docker
Every project generated by xpress-generator includes a multi-stage Dockerfile, a database-specific docker-compose.yml, and a .dockerignore — ready to use immediately.
Quick start
# Start the app + database in development mode (hot-reload)
docker compose up
# Force a rebuild after code changes
docker compose up --build
# Run in the background
docker compose up -d
# Stop everything
docker compose down
# Stop and remove volumes (wipes the database)
docker compose down -v
The Dockerfile
The generated Dockerfile uses multi-stage builds to keep the production image small and secure:
# Stage 1 — Base (shared node + package.json)
FROM node:20-alpine AS base
WORKDIR /app
COPY package*.json ./
# Stage 2 — Production dependencies only
FROM base AS deps
RUN npm ci --only=production
# Stage 3 — Development (all deps + hot-reload)
FROM base AS dev
RUN npm ci
COPY . .
EXPOSE 3000
CMD ["npm", "run", "dev"]
# Stage 4 — Production (minimal image)
FROM node:20-alpine AS prod
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY src ./src
COPY package*.json ./
EXPOSE 3000
CMD ["node", "src/server.js"]
TypeScript projects include a builder stage that runs npm run build and copies only the compiled dist/ folder to the production image — source code never ships.
What each stage does
| Stage | Purpose | Contents |
|---|---|---|
base | Common foundation | Node 20 Alpine + package.json |
deps | Prod-only deps | node_modules without dev/test packages |
dev | Local development | All deps + source, nodemon hot-reload |
prod | Production deploy | Prod node_modules + source only |
The production image is typically 60–80% smaller than an image with all dependencies.
The docker-compose.yml
Each database gets its own compose file optimized for that DB:
MongoDB
services:
app:
build:
context: .
target: dev ← uses the dev stage for hot-reload
ports: ['3000:3000']
volumes: ['.:/app', '/app/node_modules']
depends_on:
mongo:
condition: service_healthy
mongo:
image: mongo:7
ports: ['27017:27017']
healthcheck:
test: ['CMD', 'mongosh', '--eval', 'db.adminCommand("ping")']
interval: 10s
retries: 5
MySQL / PostgreSQL
The app waits for the database to pass its healthcheck before starting — no more "connection refused on startup":
depends_on:
mysql:
condition: service_healthy # ← waits for DB to be truly ready
SQL Server
sqlserver:
image: mcr.microsoft.com/mssql/server:2022-latest
environment:
ACCEPT_EULA: 'Y'
MSSQL_SA_PASSWORD: Dev@Pass123
MSSQL_PID: Developer
Environment variables in Docker
The compose file loads your .env automatically via env_file: .env. Your .env values are available inside the container, just like they are locally.
.env to gitThe generated .gitignore and .dockerignore both exclude .env. Always use environment variables or secrets management in production.
For production deployments, pass secrets via your platform's secret manager instead of a .env file:
# Example with Docker run
docker run -e JWT_SECRET=prod-secret -e MONGO_URI=mongodb://... myapp:prod
Running migrations in Docker
After starting the containers, run migrations:
# In a separate terminal (while compose is running)
docker compose exec app npm run db:migrate
# Or as a one-off command
docker compose run --rm app npm run db:migrate
Production deployment
To build and run the production image locally:
# Build the production stage
docker build --target prod -t myapp:prod .
# Run it
docker run -p 3000:3000 --env-file .env myapp:prod
Or use the compose file with a target override:
# docker-compose.prod.yml
services:
app:
build:
target: prod
restart: always
docker compose -f docker-compose.yml -f docker-compose.prod.yml up
.dockerignore
The generated .dockerignore keeps unnecessary files out of the build context:
node_modules
npm-debug.log*
.env
coverage
dist
.git
.gitignore
tests
.husky
jest.config*
This makes docker build faster and prevents secrets or test artifacts from leaking into images.