MeshiTrack/docs/instructions/docker.md

9 KiB

Docker & Docker Compose Best Practices — MeshiTrack

Instruction file for containerization and local development environment.

Docker Compose Architecture

┌─────────────┐     ┌───────────────┐     ┌──────────┐
│   web:3000  │────→│   api:3001    │────→│ mongodb  │
│  (Next.js)  │     │  (NestJS)     │     │  :27017  │
└─────────────┘     └───────┬───────┘     └──────────┘
                            │
                    ┌───────▼───────┐
                    │   keycloak    │
                    │    :8080      │
                    └───────────────┘

docker-compose.yml

version: '3.8'

services:
  mongodb:
    image: mongo:7
    container_name: meshitrack-mongodb
    restart: unless-stopped
    ports:
      - '27017:27017'
    environment:
      MONGO_INITDB_ROOT_USERNAME: meshitrack
      MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD:-devpassword}
      MONGO_INITDB_DATABASE: meshitrack
    volumes:
      - mongo-data:/data/db
      - ./mongo/init-replica.js:/docker-entrypoint-initdb.d/init-replica.js:ro
    command: ['--replSet', 'rs0', '--bind_ip_all']
    healthcheck:
      test: ['CMD', 'mongosh', '--eval', "db.adminCommand('ping')"]
      interval: 10s
      timeout: 5s
      retries: 5

  keycloak:
    image: quay.io/keycloak/keycloak:24.0
    container_name: meshitrack-keycloak
    restart: unless-stopped
    ports:
      - '8080:8080'
    environment:
      KC_DB: dev-mem
      KEYCLOAK_ADMIN: admin
      KEYCLOAK_ADMIN_PASSWORD: ${KC_ADMIN_PASSWORD:-admin}
    command: start-dev --import-realm
    volumes:
      - ./keycloak/realm-export.json:/opt/keycloak/data/import/realm-export.json:ro
    healthcheck:
      test:
        [
          'CMD-SHELL',
          "exec 3<>/dev/tcp/127.0.0.1/8080; echo -e 'GET /health/ready HTTP/1.1\\r\\nHost: localhost\\r\\nConnection: close\\r\\n\\r\\n' >&3; cat <&3 | grep -q '200'",
        ]
      interval: 15s
      timeout: 5s
      retries: 10

  api:
    build:
      context: ..
      dockerfile: docker/Dockerfile.api
      target: development
    container_name: meshitrack-api
    restart: unless-stopped
    ports:
      - '3001:3001'
    depends_on:
      mongodb:
        condition: service_healthy
      keycloak:
        condition: service_healthy
    environment:
      NODE_ENV: development
      PORT: 3001
      MONGODB_URI: mongodb://meshitrack:${MONGO_PASSWORD:-devpassword}@mongodb:27017/meshitrack?authSource=admin&replicaSet=rs0
      KEYCLOAK_URL: http://keycloak:8080
      KEYCLOAK_REALM: meshitrack
      KEYCLOAK_CLIENT_ID: meshitrack-api
    volumes:
      - ../packages/api/src:/app/packages/api/src:ro # Hot reload
      - ../packages/shared/src:/app/packages/shared/src:ro

  web:
    build:
      context: ..
      dockerfile: docker/Dockerfile.web
      target: development
    container_name: meshitrack-web
    restart: unless-stopped
    ports:
      - '3000:3000'
    depends_on:
      - api
    environment:
      NODE_ENV: development
      NEXT_PUBLIC_API_URL: http://localhost:3001/api/v1
      NEXT_PUBLIC_KEYCLOAK_URL: http://localhost:8080
      NEXT_PUBLIC_KEYCLOAK_REALM: meshitrack
      NEXT_PUBLIC_KEYCLOAK_CLIENT_ID: meshitrack-web
    volumes:
      - ../packages/web/src:/app/packages/web/src:ro

  # Dev-only services
  mongo-express:
    image: mongo-express
    container_name: meshitrack-mongo-express
    ports:
      - '8081:8081'
    depends_on:
      mongodb:
        condition: service_healthy
    environment:
      ME_CONFIG_MONGODB_ADMINUSERNAME: meshitrack
      ME_CONFIG_MONGODB_ADMINPASSWORD: ${MONGO_PASSWORD:-devpassword}
      ME_CONFIG_MONGODB_URL: mongodb://meshitrack:${MONGO_PASSWORD:-devpassword}@mongodb:27017/
    profiles:
      - dev

volumes:
  mongo-data:

Dockerfile Best Practices

Multi-stage builds

# docker/Dockerfile.api

# ---- Base ----
FROM node:20-alpine AS base
WORKDIR /app
RUN corepack enable

# ---- Dependencies ----
FROM base AS dependencies
COPY package.json package-lock.json ./
COPY packages/api/package.json ./packages/api/
COPY packages/shared/package.json ./packages/shared/
RUN npm ci --workspace=packages/shared --workspace=packages/api

# ---- Build ----
FROM dependencies AS build
COPY packages/shared/ ./packages/shared/
COPY packages/api/ ./packages/api/
COPY tsconfig.base.json ./
RUN npm run build --workspace=packages/shared
RUN npm run build --workspace=packages/api

# ---- Production ----
FROM base AS production
COPY --from=build /app/packages/api/dist ./packages/api/dist
COPY --from=build /app/packages/shared/dist ./packages/shared/dist
COPY --from=build /app/node_modules ./node_modules
COPY --from=build /app/packages/api/package.json ./packages/api/
COPY --from=build /app/packages/shared/package.json ./packages/shared/
COPY --from=build /app/package.json ./

ENV NODE_ENV=production
EXPOSE 3001
CMD ["node", "packages/api/dist/main.js"]

# ---- Development ----
FROM dependencies AS development
COPY packages/shared/ ./packages/shared/
COPY packages/api/ ./packages/api/
COPY tsconfig.base.json ./
RUN npm run build --workspace=packages/shared
EXPOSE 3001
CMD ["npm", "run", "dev", "--workspace=packages/api"]

Key principles

  1. Layer ordering: Copy package.json files first, then npm ci, then source code. This ensures dependency layers are cached unless package.json changes.

  2. Multi-stage targets: Use --target=development for dev (with hot reload), --target=production for deploy (minimal image).

  3. Alpine images: Use node:20-alpine for smaller images (~180MB vs ~1GB).

  4. .dockerignore: Always include to prevent sending unnecessary files to the build context:

    node_modules
    .git
    .next
    dist
    *.md
    docs/
    .env*
    

Environment Variables

.env.example (committed to repo)

# MongoDB
MONGO_PASSWORD=devpassword

# Keycloak
KC_ADMIN_PASSWORD=admin

# API
API_PORT=3001
MONGODB_URI=mongodb://meshitrack:devpassword@localhost:27017/meshitrack?authSource=admin

# Web
NEXT_PUBLIC_API_URL=http://localhost:3001/api/v1
NEXT_PUBLIC_KEYCLOAK_URL=http://localhost:8080
NEXT_PUBLIC_KEYCLOAK_REALM=meshitrack
NEXT_PUBLIC_KEYCLOAK_CLIENT_ID=meshitrack-web

# LLM (Phase 6)
LLM_PROVIDER_TYPE=noop
# OPENAI_API_KEY=sk-...
# LLM_MONTHLY_BUDGET_USD=20.00

.env (gitignored, developer-specific)

Never commit .env. Each developer copies .env.example to .env and fills in secrets.

In Docker Compose, use variable substitution

environment:
  MONGO_PASSWORD: ${MONGO_PASSWORD:-devpassword} # Fallback for dev

Health Checks

Every service should have a health check so depends_on: condition: service_healthy works:

# MongoDB
healthcheck:
  test: ["CMD", "mongosh", "--eval", "db.adminCommand('ping')"]
  interval: 10s
  timeout: 5s
  retries: 5

# API (requires /api/v1/health endpoint in code)
healthcheck:
  test: ["CMD", "wget", "-qO-", "http://localhost:3001/api/v1/health"]
  interval: 10s
  timeout: 5s
  retries: 5

Volume Management

Named volumes for persistence

volumes:
  mongo-data: # Survives container recreations

Bind mounts for hot reload in development

volumes:
  - ../packages/api/src:/app/packages/api/src:ro # Read-only for safety

Use :ro (read-only) for source code mounts — the container shouldn't modify your source files.

MongoDB Replica Set for Transactions

MongoDB transactions require a replica set. For local development, initialize a single-node replica:

// docker/mongo/init-replica.js
// This runs on first start via /docker-entrypoint-initdb.d/
try {
  rs.initiate({ _id: 'rs0', members: [{ _id: 0, host: 'mongodb:27017' }] });
} catch (e) {
  if (e.codeName !== 'AlreadyInitialized') throw e;
}

Useful Commands

# Start all services
docker compose up -d

# Start with dev tools (mongo-express)
docker compose --profile dev up -d

# View logs
docker compose logs -f api

# Rebuild after dependency changes
docker compose build --no-cache api

# Reset database
docker compose down -v  # -v removes volumes
docker compose up -d

# Enter a running container
docker compose exec api sh
docker compose exec mongodb mongosh -u meshitrack -p devpassword

# Backup MongoDB
docker compose exec mongodb mongodump --uri="mongodb://meshitrack:devpassword@localhost:27017/meshitrack?authSource=admin" --archive | gzip > backup-$(date +%Y%m%d).gz

Production Considerations

  • Use separate docker-compose.prod.yml with:
    • target: production for API and web builds
    • No bind mounts
    • No dev services (mongo-express)
    • Proper resource limits
    • External volumes for MongoDB data
    • Log drivers configured
  • Consider adding:
    • Traefik or nginx as reverse proxy with TLS
    • Watchtower for auto-updating container images
    • Automated backup cron container for MongoDB