Docker Deployment
Container-based deployment using Docker and Docker Compose.
Architecture
Development Stack
┌──────────────────────────────────────────────────────────────────────────────┐
│ Docker Compose Stack (Development) │
├──────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Backend │ │ Frontend │ │
│ │ Port: 3007 │ │ Port: 3000 │ │
│ └──────┬──────┘ └─────────────┘ │
│ │ │
│ ┌──────┴──────────────────────────────────────────────────────────┐ │
│ │ rag-network │ │
│ └──────────────┬──────────────────────────────────────┬───────────┘ │
│ │ │ │
│ ┌──────────────┴──┐ ┌────────────┐ ┌────────┴────┐ │
│ │ MongoDB │ │ Redis │ │ Qdrant │ │
│ │ Port: 27017 │ │ Port: 6378 │ │ Port: 6333 │ │
│ └─────────────────┘ └────────────┘ └─────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────────────┘
Production Stack
┌─────────────────────────────────────────────────────────────────────────┐
│ Docker Compose Stack (Production) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ │
│ │ Frontend │ │ Backend │ │
│ │ (Next.js) │ │ (Node.js) │ │
│ │ Port: 3000 │ │ Port: 3007 │ │
│ └──────────────┘ └──────┬───────┘ │
│ │ │
│ ┌────────────┼────────────┐ │
│ │ │ │ │
│ ┌──────┴─────┐ ┌───┴────┐ │
│ │ Qdrant │ │ Redis │ │
│ │ Port: 6333 │ │Port:6379│ │
│ │ 1G limit │ │256MB AOF│ │
│ └────────────┘ └────────┘ │
│ │
│ External: MongoDB Atlas (M0) │
└─────────────────────────────────────────────────────────────────────────┘
Quick Start
# Start all services
docker-compose up -d
# View logs
docker-compose logs -f backend
# Stop all services
docker-compose down
Docker Compose Configuration
# docker-compose.yml
version: '3.8'
services:
# Backend Service (Node.js/Express)
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: rag-backend
ports:
- "3007:3007"
environment:
- NODE_ENV=development
- PORT=3007
- MONGODB_URI=mongodb://mongodb:27017/enterprise_rag
- REDIS_URL=redis://redis:6379
- REDIS_HOST=redis
- REDIS_PORT=6379
- QDRANT_URL=http://qdrant:6333
# Azure OpenAI — set via .env file (not hardcoded here)
# LLM_PROVIDER=azure_openai, AZURE_OPENAI_API_KEY, AZURE_OPENAI_ENDPOINT, etc.
depends_on:
mongodb:
condition: service_healthy
redis:
condition: service_healthy
qdrant:
condition: service_started
volumes:
- ./backend:/app
- /app/node_modules
networks:
- rag-network
restart: unless-stopped
# MongoDB Database
mongodb:
image: mongo:7.0
container_name: rag-mongodb
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db
healthcheck:
test: ["CMD", "mongosh", "--eval", "db.runCommand({ping:1})"]
interval: 10s
timeout: 5s
retries: 5
networks:
- rag-network
restart: unless-stopped
# Redis (Caching & BullMQ)
redis:
image: redis:7-alpine
container_name: rag-redis
ports:
- "6378:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
timeout: 5s
retries: 5
networks:
- rag-network
restart: unless-stopped
# Qdrant Vector Database
qdrant:
image: qdrant/qdrant:v1.13.2
container_name: rag-qdrant
ports:
- "6333:6333"
- "6334:6334"
volumes:
- qdrant_data:/qdrant/storage
networks:
- rag-network
restart: unless-stopped
networks:
rag-network:
driver: bridge
volumes:
mongodb_data:
redis_data:
qdrant_data:
Backend Dockerfile
Multi-stage build for production optimization:
# Stage 1: Dependencies
FROM node:20-alpine AS deps
WORKDIR /app
# Install build dependencies for native modules
RUN apk add --no-cache python3 make g++
# Copy package files
COPY package*.json ./
# Install all dependencies
RUN npm ci --legacy-peer-deps
# Stage 2: Production Dependencies Only
FROM node:20-alpine AS prod-deps
WORKDIR /app
COPY package*.json ./
RUN npm ci --legacy-peer-deps --only=production
# Stage 3: Production Runner
FROM node:20-alpine AS runner
WORKDIR /app
ENV NODE_ENV=production
# Create non-root user for security
RUN addgroup --system --gid 1001 nodejs && \
adduser --system --uid 1001 expressjs
# Copy production dependencies
COPY --from=prod-deps /app/node_modules ./node_modules
# Copy application code
COPY --chown=expressjs:nodejs . .
# Remove unnecessary files
RUN rm -rf tests coverage .env.example .eslintrc* .prettierrc* *.md
# Switch to non-root user
USER expressjs
EXPOSE 3007
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:3007/health || exit 1
CMD ["node", "index.js"]
Service Commands
Start Services
# Start all services in detached mode
docker-compose up -d
# Start specific service
docker-compose up -d backend
# Start with build
docker-compose up -d --build
View Logs
# All services
docker-compose logs -f
# Specific service
docker-compose logs -f backend
# Last 100 lines
docker-compose logs --tail=100 backend
Stop Services
# Stop all services
docker-compose down
# Stop and remove volumes
docker-compose down -v
# Stop specific service
docker-compose stop backend
Rebuild
# Rebuild all images
docker-compose build
# Rebuild specific service
docker-compose build backend
# Rebuild without cache
docker-compose build --no-cache backend
Monolith Architecture Note
The backend is a single Node.js process (port 3007). Email, notifications, and real-time communication are all handled in-process:
| Capability | How it runs |
|---|---|
services/emailService.js calls Resend HTTP API directly | |
| Notifications | services/notificationService.js uses MongoDB + Socket.io |
| Real-time / Presence | Socket.io server embedded in the backend Express process |
| Background jobs | 3 BullMQ workers running in the same process (assessmentWorker, questionnaireWorker, monitoringWorker) |
The codebase includes conditional env vars (EMAIL_SERVICE_URL, NOTIFICATION_SERVICE_URL, REALTIME_SERVICE_URL) that would delegate these concerns to separate services — these are not deployed but act as future extension points. Leave them unset in both development and production.
Health Checks
MongoDB
docker exec rag-mongodb mongosh --eval "db.runCommand({ping:1})"
Redis
docker exec rag-redis redis-cli ping
Backend
curl http://localhost:3007/health
Qdrant
curl http://localhost:6333/collections
Volume Management
List Volumes
docker volume ls | grep rag
Backup MongoDB
docker exec rag-mongodb mongodump --archive > backup.archive
Restore MongoDB
docker exec -i rag-mongodb mongorestore --archive < backup.archive
Backup Qdrant
# Qdrant data is stored in the qdrant_data volume
docker run --rm -v rag_qdrant_data:/data -v $(pwd):/backup alpine \
tar cvf /backup/qdrant-backup.tar /data
Production Considerations
1. Environment Variables
Create a .env.production file:
# Copy example and configure
cp backend/.env.example backend/.env.production
# Edit with production values
vim backend/.env.production
2. MongoDB Authentication
mongodb:
image: mongo:7.0
environment:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: ${MONGO_PASSWORD}
command: ["--auth"]
3. Redis (Production — Self-Hosted)
In production, Redis runs as a Docker service on the same droplet (see docker-compose.production.yml):
redis:
image: redis:7-alpine
container_name: retrieva-redis
command: >
redis-server
--maxmemory 256mb
--maxmemory-policy noeviction
--appendonly yes
--appendfsync everysec
--save 900 1
--save 300 10
--requirepass ${REDIS_PASSWORD}
ports:
- "127.0.0.1:6379:6379"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "-a", "${REDIS_PASSWORD}", "ping"]
interval: 10s
timeout: 5s
retries: 5
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.25'
memory: 300M
Key configuration choices:
noevictionpolicy: BullMQ keys have no TTL, sovolatile-lruwould deadlock. Withnoeviction, writes fail with OOM errors instead of silently dropping jobs.- 256MB maxmemory: Enough for ~3,000+ doc syncs with BullMQ job queues and RAG response caching.
- AOF + RDB persistence: AOF (
appendfsync everysec) for durability, RDB snapshots as backup. ${REDIS_PASSWORD}: Interpolated from a root.envfile extracted during CD deployment.
Generate a Redis password:
openssl rand -base64 32
4. Qdrant (Production — Self-Hosted)
In production, Qdrant runs as a Docker service on the same droplet (see docker-compose.production.yml):
qdrant:
image: qdrant/qdrant:v1.13.2
container_name: retrieva-qdrant
ports:
- "127.0.0.1:6333:6333"
- "127.0.0.1:6334:6334"
volumes:
- qdrant_data:/qdrant/storage
environment:
- QDRANT__SERVICE__GRPC_PORT=6334
healthcheck:
test: ["CMD-SHELL", "bash -c ':>/dev/tcp/0.0.0.0/6333' || exit 1"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.5'
memory: 1G
Key configuration choices:
- Pinned version (
v1.13.2): Matches development for consistency. Avoids surprise breaking changes from:latest. - 1G memory limit: Sufficient for ~3,200 documents with dense + sparse vectors. Actual usage ~200-400MB.
- Volume persistence (
qdrant_data): Data survives container restarts. - Health check via bash TCP probe on port 6333: Backend depends on this to start only after Qdrant is ready. Uses
start_period: 30sfor first-boot initialization. - No API key needed: Ports bind to
127.0.0.1only — no external access possible.
5. Compose-Level Healthchecks (Production)
In production, all five services have compose-level healthchecks in docker-compose.production.yml. These are separate from Dockerfile HEALTHCHECK directives — compose-level healthchecks enable depends_on: condition: service_healthy and allow the CD pipeline to verify readiness via docker inspect.
| Service | Command | Interval | Timeout | Retries | Start Period |
|---|---|---|---|---|---|
| Backend | wget --spider http://localhost:3007/health | 10s | 5s | 5 | 30s |
| Frontend | wget --spider http://127.0.0.1:3000 | 10s | 5s | 3 | 20s |
| Redis | redis-cli -a $REDIS_PASSWORD ping | 10s | 5s | 5 | — |
| Qdrant | bash -c ':>/dev/tcp/0.0.0.0/6333' | 10s | 5s | 5 | 30s |
# Example: backend healthcheck in docker-compose.production.yml
backend:
healthcheck:
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3007/health"]
interval: 10s
timeout: 5s
retries: 5
start_period: 30s
Check container health status:
docker compose -f docker-compose.production.yml ps
# Or inspect a specific container:
docker inspect --format='{{.State.Health.Status}}' retrieva-backend
6. Resource Limits
backend:
deploy:
resources:
limits:
cpus: '2'
memory: 4G
reservations:
cpus: '0.5'
memory: 512M
7. Logging Driver
backend:
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
Scaling
Horizontal Scaling
# Scale backend instances
docker-compose up -d --scale backend=3
# Note: Requires load balancer configuration
Load Balancer Example (nginx)
upstream backend {
server backend_1:3007;
server backend_2:3007;
server backend_3:3007;
}
server {
listen 80;
location / {
proxy_pass http://backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
}
}
Troubleshooting
Container Won't Start
# Check logs
docker-compose logs backend
# Check health
docker-compose ps
# Inspect container
docker inspect rag-backend
Network Issues
# List networks
docker network ls
# Inspect network
docker network inspect rag_rag-network
# Test connectivity
docker exec rag-backend ping mongodb
Storage Issues
# Check disk usage
docker system df
# Clean up unused resources
docker system prune -a