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
-
Layer ordering: Copy
package.jsonfiles first, thennpm ci, then source code. This ensures dependency layers are cached unlesspackage.jsonchanges. -
Multi-stage targets: Use
--target=developmentfor dev (with hot reload),--target=productionfor deploy (minimal image). -
Alpine images: Use
node:20-alpinefor smaller images (~180MB vs ~1GB). -
.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.ymlwith:target: productionfor 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