Skip to main content

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:

CapabilityHow it runs
Emailservices/emailService.js calls Resend HTTP API directly
Notificationsservices/notificationService.js uses MongoDB + Socket.io
Real-time / PresenceSocket.io server embedded in the backend Express process
Background jobs3 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:

  • noeviction policy: BullMQ keys have no TTL, so volatile-lru would deadlock. With noeviction, 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 .env file 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: 30s for first-boot initialization.
  • No API key needed: Ports bind to 127.0.0.1 only — 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.

ServiceCommandIntervalTimeoutRetriesStart Period
Backendwget --spider http://localhost:3007/health10s5s530s
Frontendwget --spider http://127.0.0.1:300010s5s320s
Redisredis-cli -a $REDIS_PASSWORD ping10s5s5
Qdrantbash -c ':>/dev/tcp/0.0.0.0/6333'10s5s530s
# 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