1. Dockerfile Multi-stage Build
# === Stage 1: Build ===
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
RUN npm run build
# Prune devDependencies
RUN npm prune --production
# === Stage 2: Production ===
FROM node:20-alpine AS production
RUN addgroup -g 1001 -S nestjs && \
adduser -S nestjs -u 1001
WORKDIR /app
COPY --from=builder --chown=nestjs:nestjs /app/dist ./dist
COPY --from=builder --chown=nestjs:nestjs /app/node_modules ./node_modules
COPY --from=builder --chown=nestjs:nestjs /app/package.json ./
USER nestjs
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=3s \
CMD wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "dist/main.js"]
# .dockerignore
node_modules
dist
.git
.env
*.md
test
coverage
.github
2. Docker Compose
# docker-compose.yml
services:
app:
build:
context: .
dockerfile: Dockerfile
target: production
ports:
- "3000:3000"
env_file: .env
environment:
- DATABASE_URL=postgresql://postgres:postgres@db:5432/nestapp
- 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: nestapp
POSTGRES_USER: postgres
POSTGRES_PASSWORD: postgres
volumes:
- pgdata:/var/lib/postgresql/data
ports:
- "5432:5432"
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
timeout: 5s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redisdata:/data
volumes:
pgdata:
redisdata:
# docker-compose.dev.yml — override cho development
services:
app:
build:
target: builder
command: npm run start:dev
volumes:
- .:/app
- /app/node_modules
ports:
- "3000:3000"
- "9229:9229" # Debug port
3. GitHub Actions CI/CD
# .github/workflows/ci.yml
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
test:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: test
POSTGRES_USER: test
POSTGRES_PASSWORD: test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
cache: npm
- run: npm ci
- run: npm run lint
- run: npm run test -- --coverage
- run: npm run test:e2e
env:
DATABASE_URL: postgresql://test:test@localhost:5432/test
- uses: actions/upload-artifact@v4
with:
name: coverage
path: coverage/
build:
needs: test
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/metadata-action@v5
id: meta
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=sha
type=raw,value=latest
- uses: docker/build-push-action@v5
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy:
needs: build
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main'
environment: production
steps:
- name: Deploy to server
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_KEY }}
script: |
cd /opt/nestapp
docker compose pull
docker compose up -d --remove-orphans
docker image prune -f
4. Environment Configuration
# .env.example
NODE_ENV=development
PORT=3000
# Database
DATABASE_URL=postgresql://postgres:postgres@localhost:5432/nestapp
# Redis
REDIS_URL=redis://localhost:6379
# JWT
JWT_ACCESS_SECRET=your-access-secret
JWT_REFRESH_SECRET=your-refresh-secret
# S3
AWS_REGION=ap-southeast-1
AWS_ACCESS_KEY_ID=
AWS_SECRET_ACCESS_KEY=
AWS_S3_BUCKET=
5. Summary
- Multi-stage construction: Reduce image size (copy dist + production deps only)
- Docker Compose: Orchestrate app + DB + Redis
- CI Pipeline: Lint → Test → Build → Push image
- CD Pipeline: Auto deploy when merged into main
- Security: Non-root user, health checks, .dockerignore
The next article will explore Production Deployment and Monitoring.