Skip to main content

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

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​

StagePurposeContents
baseCommon foundationNode 20 Alpine + package.json
depsProd-only depsnode_modules without dev/test packages
devLocal developmentAll deps + source, nodemon hot-reload
prodProduction deployProd 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.

Never commit .env to git

The 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.