# 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 ```yaml 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 ```dockerfile # 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) ```env # 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 ```yaml 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: ```yaml # 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 ```yaml volumes: mongo-data: # Survives container recreations ``` ### Bind mounts for hot reload in development ```yaml 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: ```javascript // 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 ```bash # 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