Deployment Architecture
This guide provides comprehensive documentation for deploying and maintaining LUStores in production environments. It covers Docker containerization, GitHub Actions CI/CD pipelines, SSL/HTTPS configuration, and auto-update mechanisms.
Overview
LUStores uses a modern containerized deployment architecture with:
Docker for consistent, reproducible environments
GitHub Actions for automated CI/CD
Nginx for reverse proxy and SSL termination
PostgreSQL 15 for data persistence
Redis 7 for session storage and caching
Watchtower for automated container updates
Let’s Encrypt for free SSL certificates
The system supports multiple deployment targets:
Development: Hot-reload enabled, debug tools, local auth
Production: Multi-container orchestration, SSL, auto-updates
Kubernetes: Scalable cluster deployment with load balancing
Docker Architecture
Multi-Stage Dockerfile
The Dockerfile uses a multi-stage build strategy for optimized images:
Build Stages:
deps(Node.js 24 Alpine) - Installs build dependencies (Python, Make, G++) - Copies package.json and runsnpm install- Creates base layer with node_modulesbuild(From deps) - Compiles TypeScript to JavaScript - Bundles frontend with Vite - Builds Sphinx documentation (optional withSKIP_DOCS=1) - Optimizes for productiondevelopment(From deps) - Includes dev dependencies and source files - Enables hot-reload (Vite HMR) - Exposes ports 5000 (backend) and 5173 (frontend) - Command:npm run devtest(From deps) - Includes test frameworks (Jest, testing-library) - SetsNODE_ENV=test- Command:npm run teste2e-test(Microsoft Playwright image) - Includes Playwright browsers (Chromium) - Adds Node.js 24 on top of Playwright base - Command:npm run test:e2e-comprehensiveproduction(Node.js 24 Alpine) - Minimal runtime image (no build tools) - Includes only production dependencies - Copies built artifacts frombuildstage - Adds PostgreSQL client for database operations - Runs as non-root userappuser(UID 1001) - Health check:wget http://localhost:5000/health- Command:npm run start
Key Optimizations:
Layer caching: Dependencies installed before source code
Build args:
SKIP_DOCS=1to skip Sphinx buildMemory limits:
NODE_OPTIONS="--max-old-space-size=2048"Production image size: ~350MB (vs ~1.2GB with dev dependencies)
Development Environment
File: docker-compose.yml
Services:
app (development target) - Ports: 5000 (API), 5173 (Vite dev server) - Volumes: Source code mounted for hot-reload - Environment:
NODE_ENV=development,DEV_ADMIN_OVERRIDE=true- Depends on:db,replit-authdb (PostgreSQL 15) - Port: 5432 - Volume:
postgres_datafor persistence - Initialization:init.sqlauto-executed on first start - Health check:pg_isready -U postgresredis (Redis 7) - Port: 6379 - Volume:
redis_datafor AOF persistence - Command:redis-server --appendonly yesreplit-auth (Local auth service) - Port: 3001 - Mock authentication for development - Environment:
JWT_SECRET=local-replit-jwt-secret
Test Services (Profile: testing):
test: Runs unit tests with coverage
test-sales: Runs sales module tests specifically
test-watch: Interactive test watcher
test-coverage: Generates coverage reports
test-integration: Integration tests with live database
test-db: Isolated PostgreSQL for testing (port 5433)
Usage:
# Start development environment
docker compose up -d
# Run unit tests
docker compose --profile testing run test
# Watch mode for development
docker compose --profile testing run test-watch
# View logs
docker compose logs -f app
# Stop all services
docker compose down
Production Environment
File: docker-compose.prod.yml
This file extends docker-compose.yml with production overrides:
Additional Services:
nginx (Nginx Alpine) - Ports: 80 (HTTP), 443 (HTTPS) - Reverse proxy with SSL termination - Rate limiting: 10 req/s for API, 5 req/min for login - Config templates: Dynamic domain substitution - Volumes: SSL certificates, Nginx configs, logs - Health check:
nc -z 127.0.0.1 80- Startup script:nginx-startup.sh(handles SSL setup)certbot (Certbot for Let’s Encrypt) - Auto-renewal: Checks every 12 hours - Volumes: Shared with Nginx for certificate storage - Environment:
DOMAIN,EMAIL,CERTBOT_STAGING- Restart policy:unless-stoppedcertbot-init (One-time setup, profile:
init) - Obtains initial SSL certificate - Supports self-signed certificates (USE_SELF_SIGNED=true) - Script:generate-ssl.shwatchtower (Container auto-updater) - Monitors: Containers with
com.centurylinklabs.watchtower.enable=true- Schedule: Every 15 minutes (0 */15 * * * *) - Cleanup: Removes old images after update - Webhook:POST /api/webhook/watchtowerfor notifications - Critical: Only monitors labeled containers (app, db, redis, nginx)
Modified Services:
app: - Image:
st7ma784/lustores:latest(from Docker Hub) - Port: 5000 (internal only, Nginx proxies) - Environment: Production secrets from.env- Health check: 60s start period, 5 retries - Watchtower label:enable=truedb: - Volume:
/db:/var/lib/postgresql/data(host mount for backups) - Password:${DB_PASSWORD}from environment - Authentication: SCRAM-SHA-256 - PGDATA: Custom location/var/lib/postgresql/data/pgdatareplit-auth: - Image:
st7ma784/replitauth:latest- Environment: Production JWT secret - Allowed origins:https://${DOMAIN}
Network:
Custom bridge network:
lustores_networkSubnet:
172.20.0.0/16Enables service discovery by name
Production Deployment:
# First-time setup: Generate SSL certificate
docker compose -f docker-compose.prod.yml --profile init up certbot-init
# Start production stack
docker compose -f docker-compose.prod.yml up -d
# Check service health
docker compose -f docker-compose.prod.yml ps
docker compose -f docker-compose.prod.yml logs watchtower
# Manual container update (Watchtower does this automatically)
docker compose -f docker-compose.prod.yml pull app
docker compose -f docker-compose.prod.yml up -d app
GitHub Actions CI/CD
Automated pipelines run on every push to main branch.
Docker Build & Push Workflow
File: .github/workflows/docker-build-push.yml
Triggers:
Push to
mainbranchPull requests to
mainManual dispatch (
workflow_dispatch)Ignores:
docs/**,**.md, docs workflow changes
Jobs:
build-and-push - Runs on:
ubuntu-latest- Uses Docker Buildx for advanced builds - Builds two images:st7ma784/lustores:latest(main app)st7ma784/replitauth:latest(auth service)
Tags: -
latest(on main branch) -main-<git-sha>(commit-specific)Cache: GitHub Actions cache (
type=gha)Push: Only on main branch pushes (not PRs)
Docker Hub Integration:
Requires secrets:
DOCKERHUB_USERNAME,DOCKERHUB_TOKENImages automatically pulled by Watchtower in production
Tests Workflow
File: .github/workflows/tests.yml
Triggers:
Push to
mainPull requests
Manual dispatch
Ignores:
docs/**,**.md
Jobs:
lint-and-type-check - Node.js 24 setup - npm ci (clean install) - TypeScript type checking:
npm run type-check- ESLint (if configured):npm run lintunit-tests - PostgreSQL 15 service container (port 5432) - Database initialization:
init.sqlapplied - Test command:npm run test:ciornpm test- Coverage upload: Artifact retention 7 days - Environment:NODE_ENV=test,DATABASE_URL=postgresql://postgres:password@localhost:5432/test_inventorysecurity-scan - npm audit: Critical and high vulnerabilities - Trivy scanner: Filesystem vulnerability scan - SARIF upload: GitHub Security tab integration - Artifact upload:
npm-audit.json,trivy-results.sarif
Key Features:
Continues on error: Tests report failures but don’t block
GitHub service containers: PostgreSQL runs alongside tests
Caching: npm cache for faster installs
Permissions:
security-events: writefor SARIF uploads
Documentation Workflow
File: .github/workflows/docs.yml
Triggers:
Push to
mainaffecting: -docs/**-**.md-typedoc.jsonPull requests with docs changes
Manual dispatch
Jobs:
build - Python 3.x setup for Sphinx - Node.js 24 for TypeDoc - Python dependencies:
sphinx
sphinx-rtd-theme
sphinxcontrib-httpdomain
myst-parser
sphinxcontrib-mermaid
TypeDoc: Generates API documentation from TypeScript
Sphinx: Builds HTML documentation (
make html)Creates
.nojekyllfile for GitHub PagesUploads artifact:
documentation
deploy - Runs only on main branch pushes - Downloads documentation artifact - Deploys to GitHub Pages - URL:
https://st7ma784.github.io/LUStores/- Environment:github-pages
Caching:
Python pip cache:
~/.cache/pipnpm cache: Automatic via
setup-node
E2E Tests Workflow (Disabled)
File: .github/workflows/e2e-tests.yml
Status: Currently disabled (all triggers commented out)
Why Disabled: End-to-end tests with Playwright are resource-intensive and can be flaky in CI environments. Enabled via manual dispatch only.
Architecture (when enabled):
Microsoft Playwright Docker image (v1.48.2)
Node.js 24 installed on top
Ports: 5001 (app), 5433 (db), 6380 (redis) to avoid conflicts
Docker Compose: Builds and starts full stack
Health checks: Waits up to 5 minutes for app readiness
Test execution:
npx playwright testwith HTML/JUnit reportersCleanup: Removes all containers and volumes after tests
Manual Execution:
# Via GitHub Actions UI
# Navigate to Actions > E2E Tests > Run workflow
# Locally with Docker Compose
docker compose --profile e2e run e2e-test
Deployment Options
Docker Compose (Recommended)
Best for: Small to medium deployments (1-100 concurrent users)
Advantages:
Simple setup and management
Single-server deployment
Automatic service discovery
Easy SSL configuration with Certbot
Watchtower auto-updates
Requirements:
Docker 24+ with Compose V2
2GB+ RAM (4GB recommended)
20GB+ disk space
Public domain name (for SSL)
Setup:
# Clone repository
git clone https://github.com/st7ma784/LUStores.git
cd LUStores
# Configure environment
cp .env.prod.example .env
nano .env # Edit with your values
# Generate SSL certificate (first time only)
docker compose -f docker-compose.prod.yml --profile init up certbot-init
# Start production stack
docker compose -f docker-compose.prod.yml up -d
# Verify deployment
curl https://yourdomain.com/health
Environment Variables (.env):
# Domain Configuration
DOMAIN=yourdomain.com
EMAIL=admin@yourdomain.com
# Database
DB_PASSWORD=<strong-random-password>
DATABASE_URL=postgresql://postgres:${DB_PASSWORD}@db:5432/university_inventory
# Security
SESSION_SECRET=<64-char-random-string>
JWT_SECRET=<64-char-random-string>
JWT_EXPIRES_IN=7d
# SSL
CERTBOT_STAGING= # Leave empty for production certs
USE_SELF_SIGNED=false
# Watchtower
WATCHTOWER_NOTIFICATION_WEBHOOK_URL=http://app:5000/api/webhook/watchtower
Maintenance:
# View logs
docker compose -f docker-compose.prod.yml logs -f app
# Restart a service
docker compose -f docker-compose.prod.yml restart app
# Update to latest version (Watchtower does this automatically)
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
# Database backup
docker exec lustores_db pg_dump -U postgres university_inventory > backup.sql
# Database restore
docker exec -i lustores_db psql -U postgres university_inventory < backup.sql
Kubernetes Deployment
Best for: Large deployments (100+ concurrent users), high availability
File: kube.yml
Architecture:
Namespace:
lustoresReplicas: 2x app pods (horizontal scaling)
Load balancing: Nginx service with LoadBalancer type
Persistent volumes: PostgreSQL (10Gi), Redis (5Gi)
Secrets: Database password, JWT secrets, session secrets
Health checks: Liveness and readiness probes
Resources (Kubernetes):
Namespace:
lustores(isolation)Persistent Volumes: -
postgres-prod-pv: 10Gi (host path:/db) -redis-pv: 5Gi (host path:/mnt/data/redis)Deployments: -
app: 2 replicas, 512Mi-1Gi RAM, 0.5-1 CPU -db: 1 replica (PostgreSQL 17) -redis: 1 replica, 256Mi-512Mi RAM -nginx: 1 replica (reverse proxy) -replit-auth: 1 replica -github-runner: 1 replica (optional, for self-hosted CI)Services: -
nginx: LoadBalancer (external access) -app,db,redis,replit-auth: ClusterIP (internal)ConfigMaps: -
init-sql-config: Database schema initialization -nginx-config: Nginx reverse proxy configuration -nginx-http-config: HTTP-only config (development)Secrets (Base64 encoded): -
db-secret: Database password -app-secret: Session secret, JWT secret, database URL -github-runner-secret: GitHub runner token (optional)
Deployment:
# Create secrets (replace with actual base64-encoded values)
kubectl create secret generic db-secret \\
--from-literal=password=$(echo -n 'your-db-password' | base64) \\
-n lustores
kubectl create secret generic app-secret \\
--from-literal=session-secret=$(echo -n 'your-session-secret' | base64) \\
--from-literal=jwt-secret=$(echo -n 'your-jwt-secret' | base64) \\
--from-literal=database-url=$(echo -n 'postgresql://...' | base64) \\
-n lustores
# Apply Kubernetes manifest
kubectl apply -f kube.yml
# Check deployment status
kubectl get pods -n lustores
kubectl get services -n lustores
# View logs
kubectl logs -f deployment/app -n lustores
# Scale app deployment
kubectl scale deployment app --replicas=4 -n lustores
Health Checks:
Liveness probes: Restart pod if app crashes - App:
GET /healthevery 30s - Database:pg_isreadyevery 10sReadiness probes: Remove from load balancer if not ready - App:
GET /healthevery 10s - Database:pg_isreadyevery 5s
Advantages:
Horizontal scaling: Add more app pods as needed
Self-healing: Automatic pod restarts on failure
Rolling updates: Zero-downtime deployments
Resource limits: Prevent resource exhaustion
Network policies: Enhanced security
Limitations:
More complex setup and management
Requires Kubernetes cluster (GKE, EKS, AKS, self-hosted)
ConfigMap has abbreviated
init.sql(full schema needed for production)
SSL/HTTPS Configuration
Let’s Encrypt Integration
LUStores uses Let’s Encrypt for free, automated SSL certificates.
Components:
Certbot (Docker service) - Image:
certbot/certbot- Auto-renewal: Every 12 hours - Certificate storage:./certbot/conf(shared with Nginx) - ACME challenge:./certbot/www(HTTP-01 validation)Nginx (Reverse proxy) - Serves ACME challenge:
/.well-known/acme-challenge/- SSL termination: Decrypts HTTPS, proxies HTTP to app - Certificate paths:/etc/letsencrypt/live/${DOMAIN}/fullchain.pem/etc/letsencrypt/live/${DOMAIN}/privkey.pem
First-Time Setup:
# Set environment variables
export DOMAIN=inventory.university.edu
export EMAIL=admin@university.edu
# Generate certificate (production)
docker compose -f docker-compose.prod.yml --profile init up certbot-init
# OR: Generate staging certificate (for testing, avoids rate limits)
export CERTBOT_STAGING=--staging
docker compose -f docker-compose.prod.yml --profile init up certbot-init
# OR: Generate self-signed certificate (development/testing)
export USE_SELF_SIGNED=true
docker compose -f docker-compose.prod.yml --profile init up certbot-init
How It Works:
certbot-initrunsgenerate-ssl.shscriptScript checks if certificate already exists
If not, runs Certbot with ACME HTTP-01 challenge
Certbot places challenge file in
./certbot/www/.well-known/acme-challenge/Let’s Encrypt verifies ownership by fetching
http://${DOMAIN}/.well-known/acme-challenge/<token>Nginx serves challenge from
./certbot/wwwLet’s Encrypt issues certificate
Certificate saved to
./certbot/conf/live/${DOMAIN}/Nginx reloads to use new certificate
Auto-Renewal:
The certbot service runs continuously and checks for renewal twice daily:
# Certbot entrypoint (from docker-compose.prod.yml)
entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
Certificates are renewed 30 days before expiration (Let’s Encrypt issues 90-day certs).
Manual Renewal (if needed):
# Force renewal
docker compose -f docker-compose.prod.yml exec certbot certbot renew --force-renewal
# Reload Nginx to use new certificate
docker compose -f docker-compose.prod.yml exec nginx nginx -s reload
Nginx Reverse Proxy
Configuration: nginx/nginx.conf.template
Key Features:
HTTP to HTTPS Redirect:
server { listen 80; server_name ${NGINX_HOST}; location /.well-known/acme-challenge/ { root /var/www/certbot; } location / { return 301 https://$server_name$request_uri; } }
HTTPS Server (SSL termination):
server { listen 443 ssl http2; server_name ${NGINX_HOST}; ssl_certificate /etc/letsencrypt/live/${NGINX_HOST}/fullchain.pem; ssl_certificate_key /etc/letsencrypt/live/${NGINX_HOST}/privkey.pem; ssl_protocols TLSv1.2 TLSv1.3; ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:...; location / { proxy_pass http://app:5000; proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Real-IP $remote_addr; } }
Security Headers:
Strict-Transport-Security: Force HTTPS for 2 yearsX-Frame-Options: DENY: Prevent clickjackingX-Content-Type-Options: nosniff: Prevent MIME sniffingX-XSS-Protection: Enable XSS filtering
Rate Limiting:
limit_req_zone $binary_remote_addr zone=api:10m rate=10r/s; limit_req_zone $binary_remote_addr zone=login:10m rate=5r/m; location / { limit_req zone=api burst=20 nodelay; proxy_pass http://app:5000; }
API endpoints: 10 requests/second (burst 20)
Login endpoint: 5 requests/minute (brute-force protection)
Upstream Health Checks:
upstream app { server app:5000 max_fails=3 fail_timeout=30s weight=1; keepalive 32; }
Mark backend as down after 3 failures
Retry after 30 seconds
Persistent connections (keepalive)
Dynamic Configuration:
Environment variables are substituted at runtime:
${NGINX_HOST}: Domain name (fromDOMAINenv var)${DOMAIN}: Same asNGINX_HOST
This allows the same config template to work for any domain.
Troubleshooting:
# Check Nginx config syntax
docker compose -f docker-compose.prod.yml exec nginx nginx -t
# View Nginx logs
docker compose -f docker-compose.prod.yml logs nginx
# Reload config without downtime
docker compose -f docker-compose.prod.yml exec nginx nginx -s reload
# Test SSL certificate
curl -vI https://yourdomain.com
openssl s_client -connect yourdomain.com:443 -servername yourdomain.com
Self-Signed Certificates (Development)
For local development or testing without a public domain:
# Set environment
export USE_SELF_SIGNED=true
export DOMAIN=localhost
# Generate self-signed certificate
docker compose -f docker-compose.prod.yml --profile init up certbot-init
# Trust certificate (browser will still show warning)
# On macOS:
sudo security add-trusted-cert -d -r trustRoot \\
-k /Library/Keychains/System.keychain \\
certbot/conf/live/localhost/cert.pem
Script: scripts/generate-self-signed-ssl.sh
Creates 365-day self-signed certificate with OpenSSL.
Automatic Updates with Watchtower
Watchtower monitors Docker containers and automatically updates them when new images are available.
Configuration
From docker-compose.prod.yml:
watchtower:
image: containrrr/watchtower:latest
volumes:
- /var/run/docker.sock:/var/run/docker.sock
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_INCLUDE_STOPPED=true
- WATCHTOWER_REVIVE_STOPPED=false
- WATCHTOWER_NOTIFICATION_WEBHOOK_URL=http://app:5000/api/webhook/watchtower
- WATCHTOWER_MONITOR_ONLY=false
- WATCHTOWER_SCHEDULE=0 */15 * * * *
- WATCHTOWER_LABEL_ENABLE=true
restart: unless-stopped
How It Works:
Schedule: Runs every 15 minutes (cron:
0 */15 * * * *)Label-based monitoring: Only updates containers with
com.centurylinklabs.watchtower.enable=trueImage check: Pulls latest image from Docker Hub
Comparison: Compares local image digest with remote
Update: If newer image available: - Stops old container - Removes old container - Creates new container with same configuration - Starts new container
Cleanup: Removes old Docker images (
WATCHTOWER_CLEANUP=true)Notification: POSTs update status to
/api/webhook/watchtower
Monitored Services (have Watchtower labels):
app(main application)db(PostgreSQL database)redis(session store)nginx(reverse proxy)replit-auth(authentication service)
Not Monitored:
watchtoweritself (enable=false)certbot(manual certificate management)
Deployment Workflow
This enables continuous deployment:
Developer pushes code to
mainbranchGitHub Actions builds Docker image (
docker-build-push.yml)Tests run (
tests.yml)Image pushed to Docker Hub (
st7ma784/lustores:latest)Within 15 minutes, Watchtower detects new image
Watchtower updates production containers automatically
Webhook notifies application of update
Zero-Downtime Updates:
Watchtower updates containers one at a time. With Kubernetes (2 app replicas), one pod stays running while the other updates.
Rollback:
If an update breaks production:
# View available image tags
docker images st7ma784/lustores
# Roll back to previous version
docker tag st7ma784/lustores:<previous-sha> st7ma784/lustores:latest
docker compose -f docker-compose.prod.yml up -d app
# Or: Pull specific commit
docker pull st7ma784/lustores:main-abc123def
docker tag st7ma784/lustores:main-abc123def st7ma784/lustores:latest
docker compose -f docker-compose.prod.yml up -d app
Disabling Auto-Updates
To disable Watchtower for a specific service:
app:
labels:
- "com.centurylinklabs.watchtower.enable=false"
To disable Watchtower entirely:
docker compose -f docker-compose.prod.yml stop watchtower
docker compose -f docker-compose.prod.yml rm watchtower
Best Practices
Security
Never commit secrets: - Use
.envfiles (gitignored) - Rotate secrets regularly (90 days) - Use strong random strings (64+ characters)Database security: - Change default password immediately - Use SCRAM-SHA-256 authentication - Restrict network access (Docker internal network) - Regular backups (automated cron job)
SSL/TLS: - Always use HTTPS in production - Enable HSTS (Strict-Transport-Security) - Use modern TLS protocols (1.2, 1.3)
Container security: - Run as non-root user (
appuser) - Minimal production image (no build tools) - Regular security scans (Trivy in CI)Rate limiting: - Protect API endpoints (10 req/s) - Protect login endpoints (5 req/min) - Adjust based on traffic patterns
Monitoring
Health checks:
# Application health curl https://yourdomain.com/health # Container health docker compose -f docker-compose.prod.yml ps # Database health docker compose -f docker-compose.prod.yml exec db pg_isready -U postgres
Logs:
# Real-time logs docker compose -f docker-compose.prod.yml logs -f app # Last 100 lines docker compose -f docker-compose.prod.yml logs --tail=100 app # Nginx access logs docker compose -f docker-compose.prod.yml exec nginx tail -f /var/log/nginx/access.log
Resource usage:
# Container stats docker stats # Disk usage docker system df
Database monitoring:
# Connection count docker compose -f docker-compose.prod.yml exec db \\ psql -U postgres -d university_inventory \\ -c "SELECT count(*) FROM pg_stat_activity;" # Database size docker compose -f docker-compose.prod.yml exec db \\ psql -U postgres -d university_inventory \\ -c "SELECT pg_size_pretty(pg_database_size('university_inventory'));"
Backup Strategy
Database backups (daily):
#!/bin/bash # /etc/cron.daily/lustores-backup.sh BACKUP_DIR=/backups/lustores DATE=$(date +%Y%m%d_%H%M%S) # Create backup directory mkdir -p $BACKUP_DIR # Backup database docker exec lustores_db pg_dump -U postgres university_inventory \\ | gzip > $BACKUP_DIR/db_$DATE.sql.gz # Keep only last 30 days find $BACKUP_DIR -name "db_*.sql.gz" -mtime +30 -delete
Volume backups:
# Backup Docker volumes docker run --rm \\ -v lustores_postgres_data:/source \\ -v /backups/volumes:/backup \\ alpine tar czf /backup/postgres_data_$(date +%Y%m%d).tar.gz -C /source .
Configuration backups:
# Backup .env and docker-compose files tar czf lustores_config_$(date +%Y%m%d).tar.gz \\ .env docker-compose.prod.yml nginx/
Restore procedure:
# Stop application docker compose -f docker-compose.prod.yml down # Restore database gunzip < db_20250101_120000.sql.gz | \\ docker exec -i lustores_db psql -U postgres university_inventory # Restore volumes docker run --rm \\ -v lustores_postgres_data:/target \\ -v /backups/volumes:/backup \\ alpine tar xzf /backup/postgres_data_20250101.tar.gz -C /target # Start application docker compose -f docker-compose.prod.yml up -d
Performance Optimization
Database: - Regular VACUUM and ANALYZE (see Database Organization and Schema) - Connection pooling (built into app) - Indexes on frequently queried columns
Redis caching: - Session storage in Redis (not database) - AOF persistence for durability - Memory limit: 512MB (adjust based on session count)
Nginx: - Gzip compression enabled (6x compression) - Static file caching (future enhancement) - Connection keepalive (reduces overhead)
Application: - Production build (minified, tree-shaken) - No source maps in production - Memory limit: 2GB Node.js heap
Docker: - Multi-stage builds (smaller images = faster pulls) - Layer caching (faster rebuilds) - Volume mounts (not bind mounts in production)
Troubleshooting
Common Deployment Issues
Issue: Container won’t start
# Check logs for errors
docker compose -f docker-compose.prod.yml logs app
# Common causes:
# 1. Missing environment variables - check .env file
# 2. Database not ready - check db health
# 3. Port already in use - check `netstat -tulpn | grep 5000`
Issue: 502 Bad Gateway (Nginx)
# Check if app is running
docker compose -f docker-compose.prod.yml ps app
# Check app health
docker compose -f docker-compose.prod.yml exec app wget -O- http://localhost:5000/health
# Check Nginx upstream config
docker compose -f docker-compose.prod.yml exec nginx cat /etc/nginx/conf.d/default.conf
Issue: SSL certificate not working
# Check certificate exists
ls -la certbot/conf/live/${DOMAIN}/
# Check Nginx SSL config
docker compose -f docker-compose.prod.yml exec nginx nginx -t
# Regenerate certificate
docker compose -f docker-compose.prod.yml --profile init up certbot-init
# Check Let's Encrypt rate limits
# https://letsencrypt.org/docs/rate-limits/
Issue: Database connection refused
# Check database is running
docker compose -f docker-compose.prod.yml ps db
# Check database logs
docker compose -f docker-compose.prod.yml logs db
# Test connection from app container
docker compose -f docker-compose.prod.yml exec app \\
psql postgresql://postgres:password@db:5432/university_inventory -c "SELECT 1;"
Issue: Watchtower not updating
# Check Watchtower logs
docker compose -f docker-compose.prod.yml logs watchtower
# Verify labels are set
docker inspect lustores_app | grep watchtower
# Force manual update
docker compose -f docker-compose.prod.yml pull app
docker compose -f docker-compose.prod.yml up -d app
Issue: Out of disk space
# Check disk usage
df -h
docker system df
# Remove old images
docker image prune -a
# Remove old volumes
docker volume prune
# Clean build cache
docker builder prune
Performance Issues
Symptom: Slow API responses
Check database query performance (see Database Organization and Schema)
Check container resource limits:
docker statsEnable query logging to identify slow queries
Add database indexes for frequently filtered columns
Symptom: High memory usage
# Check container memory
docker stats --no-stream
# Increase Node.js heap if needed (in docker-compose.prod.yml)
environment:
- NODE_OPTIONS="--max-old-space-size=4096" # 4GB
Symptom: Database running out of connections
# Check active connections
docker compose -f docker-compose.prod.yml exec db \\
psql -U postgres -d university_inventory \\
-c "SELECT count(*) FROM pg_stat_activity;"
# Increase max connections in PostgreSQL (init.sql or ALTER SYSTEM)
# Default: 100
For more troubleshooting guidance, see System Recovery and Emergency Procedures and Troubleshooting Guide.