Skip to main content
Advertisement

18.2 Docker + TypeScript — Multi-Stage Builds and Production Optimization

Basic Dockerfile

# Dockerfile

# ===== Build Stage =====
FROM node:20-alpine AS builder

WORKDIR /app

# Copy dependencies first (leverage cache)
COPY package*.json ./
COPY tsconfig*.json ./

# Install all dependencies (production + dev required for build)
RUN npm ci

# Copy source and build
COPY src ./src
RUN npm run build

# ===== Dependencies Stage =====
FROM node:20-alpine AS dependencies

WORKDIR /app
COPY package*.json ./

# Install production dependencies only
RUN npm ci --only=production && npm cache clean --force

# ===== Production Stage =====
FROM node:20-alpine AS production

# Security: run as non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nestjs -u 1001

WORKDIR /app

# Copy build output
COPY --from=builder --chown=nestjs:nodejs /app/dist ./dist

# Copy production dependencies only
COPY --from=dependencies --chown=nestjs:nodejs /app/node_modules ./node_modules

# Required config files
COPY --chown=nestjs:nodejs package.json ./

USER nestjs

# Expose port
EXPOSE 3000

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget -qO- http://localhost:3000/health || exit 1

CMD ["node", "dist/main.js"]

.dockerignore

# .dockerignore
node_modules
dist
build
.git
.gitignore
*.md
*.log
.env
.env.*
coverage
.nyc_output
.DS_Store
Thumbs.db

# Test files
**/*.test.ts
**/*.spec.ts
**/*.test.js
e2e/

docker-compose.yml

# docker-compose.yml
version: '3.8'

services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- '3000:3000'
environment:
NODE_ENV: production
DATABASE_URL: postgresql://postgres:password@db:5432/myapp
JWT_SECRET: ${JWT_SECRET}
REDIS_URL: redis://redis:6379
depends_on:
db:
condition: service_healthy
redis:
condition: service_started
restart: unless-stopped

db:
image: postgres:16-alpine
environment:
POSTGRES_DB: myapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: password
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ['CMD-SHELL', 'pg_isready -U postgres']
interval: 10s
timeout: 5s
retries: 5

redis:
image: redis:7-alpine
volumes:
- redis_data:/data

# Development environment only
app-dev:
build:
context: .
target: builder # Use build stage
command: npm run start:dev
volumes:
- .:/app
- /app/node_modules # Preserve container node_modules
ports:
- '3000:3000'
- '9229:9229' # Debugger port
environment:
NODE_ENV: development
profiles:
- dev

volumes:
postgres_data:
redis_data:

tsconfig.json Build Optimization

// tsconfig.json — base configuration
{
"compilerOptions": {
"target": "ES2022",
"module": "commonjs",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"declarationMap": false,
"sourceMap": false
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
}
// tsconfig.build.json — production build only
{
"extends": "./tsconfig.json",
"compilerOptions": {
"sourceMap": false,
"declaration": false,
"removeComments": true,
"noEmit": false
},
"exclude": [
"node_modules",
"dist",
"test",
"**/*spec.ts",
"**/*test.ts"
]
}

GitHub Actions CI/CD

# .github/workflows/deploy.yml
name: Build and Deploy

on:
push:
branches: [main]

jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- run: npm ci
- run: npm run typecheck # tsc --noEmit
- run: npm run lint
- run: npm run test:ci

build-and-push:
needs: test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Build Docker image
run: |
docker build \
--target production \
-t my-app:${{ github.sha }} \
-t my-app:latest \
.

- name: Push to registry
run: |
echo ${{ secrets.REGISTRY_PASSWORD }} | docker login -u ${{ secrets.REGISTRY_USERNAME }} --password-stdin
docker push my-app:${{ github.sha }}
docker push my-app:latest

deploy:
needs: build-and-push
runs-on: ubuntu-latest
steps:
- name: Apply Prisma migrations
run: |
docker run --rm \
-e DATABASE_URL=${{ secrets.DATABASE_URL }} \
my-app:${{ github.sha }} \
npx prisma migrate deploy

- name: Deploy to production
run: |
# Run deployment script
echo "Deploying version ${{ github.sha }}"

Pro Tips

Node.js Process Signal Handling

// src/main.ts
async function bootstrap() {
const app = await NestFactory.create(AppModule)
await app.listen(process.env.PORT ?? 3000)

// Graceful Shutdown
const signals: NodeJS.Signals[] = ['SIGTERM', 'SIGINT']

signals.forEach(signal => {
process.on(signal, async () => {
console.log(`${signal} received, shutting down gracefully...`)
await app.close()
process.exit(0)
})
})
}

bootstrap()
Advertisement