# Banatie Service Infrastructure Documentation v1.0 ## Overview This document defines the complete containerization and deployment architecture for the Banatie AI image generation service on the usul.su VPS infrastructure. **Target VPS:** 62.146.239.118 (Contabo Singapore) **Environment:** Ubuntu 24.04.2 LTS with Docker User Namespace Remapping ## Architecture Summary ### Core Principles - **Complete Service Isolation**: Banatie ecosystem is fully isolated from core VPS services (NextCloud, Gitea) - **Dedicated Database**: Separate PostgreSQL container exclusively for Banatie - **S3-Compatible Storage**: MinIO SNMD (Single Node Multi Drive) for full S3 compatibility - **Container-First Approach**: All components run as Docker containers - **Network Segregation**: Internal communication via dedicated Docker networks - **No Port Exposure**: All external access via Caddy reverse proxy only ### Service Components ``` Banatie Ecosystem (Isolated from /opt/services/) ├── Banatie API (Express.js/TypeScript) → api.banatie.app ├── Banatie Landing (Next.js standalone) → banatie.app ├── PostgreSQL 15 (Dedicated instance) ├── MinIO SNMD (S3-compatible storage) │ ├── Console → storage.banatie.app │ └── S3 API → cdn.banatie.app └── Storage Init (one-time bucket setup) ``` ### Domain Architecture | Domain | Service | Container | Port | |--------|---------|-----------|------| | `banatie.app` | Landing Page | banatie-landing | 3000 | | `api.banatie.app` | REST API | banatie-api | 3000 | | `storage.banatie.app` | MinIO Console | banatie-minio | 9001 | | `cdn.banatie.app` | MinIO S3 (images) | banatie-minio | 9000 | ## Network Architecture ### Production Networks ```yaml networks: banatie-internal: driver: bridge internal: true # 🔒 CRITICAL: No external internet access proxy-network: external: true # Existing Caddy reverse proxy network ``` ### Network Access Matrix | Service | banatie-internal | proxy-network | External Access | |---------|-----------------|---------------|-----------------| | banatie-api | ✅ Internal | ✅ HTTP only | ❌ Direct | | banatie-landing | ✅ Internal | ✅ HTTP only | ❌ Direct | | banatie-postgres | ✅ Internal | ❌ None | ❌ None | | banatie-minio | ✅ Internal | ✅ Console+S3 | ❌ Direct | | Caddy (external) | ❌ None | ✅ Routing | ✅ Internet | ### VPS Integration Points **Isolation from Core Services:** - **NO** shared resources with `/opt/services/` infrastructure - **NO** access to NextCloud, Gitea, or core PostgreSQL - **ONLY** connection point: Caddy reverse proxy (proxy-network) **Shared Infrastructure:** - Caddy reverse proxy for SSL termination and routing - Host filesystem for persistent data storage - UFW firewall rules and Docker User Namespace Remapping ## Directory Structure ### Production Environment (VPS) ``` /opt/banatie/ # Isolated Banatie deployment ├── docker-compose.yml # Production configuration ├── .env # Public environment variables ├── secrets.env # Sensitive secrets (chmod 600) ├── scripts/ │ └── init-db.sql # PostgreSQL 15 permission fix (minimal) ├── data/ # Persistent data (Docker-managed!) │ ├── postgres/ # PostgreSQL data │ ├── minio/ # MinIO SNMD storage │ │ ├── drive1/ │ │ ├── drive2/ │ │ ├── drive3/ │ │ └── drive4/ │ └── waitlist-logs/ # Landing waitlist emails └── logs/ ├── api/ # API service logs └── landing/ # Landing app logs # Source code (separate from production) ~/workspace/projects/banatie-service/ ├── apps/ │ ├── api-service/ # Express.js API source │ │ └── Dockerfile # Production build │ └── landing/ # Next.js landing source │ └── Dockerfile # Production build ├── packages/ │ └── database/ # Drizzle ORM schema (source of truth) │ ├── src/schema/ # Actual database schema │ └── migrations/ # Drizzle migrations └── prod-env/ # Local testing (NOT for VPS) ``` ### Source Code Repository Source code is managed in `~/workspace/projects/banatie-service/` and cloned from Gitea: ```bash git clone ssh://git@git.usul.su:2222/usulpro/banatie-service.git ``` Production configuration lives in `/opt/banatie/` (separate from source). ## Docker User Namespace Remapping **Status:** ✅ ENABLED on VPS for enhanced container security **Configuration:** `/etc/docker/daemon.json` ```json { "userns-remap": "default" } ``` **What This Means:** - Container UIDs are mapped to high-numbered host UIDs (base offset: 165536) - PostgreSQL UID 70 inside container → UID 165606 on host - MinIO UID 1000 inside container → UID 166536 on host - Enhanced security: containers cannot access host files with original UIDs ### Critical Rules - ❌ **DO NOT** manually create directories in `/opt/banatie/data/` - ❌ **DO NOT** manually chown data directories - ✅ **LET DOCKER** handle directory creation and permissions - ✅ Use `docker exec` for container file operations ### Troubleshooting Permission Issues If a service fails with permission errors after VPS reboot: ```bash # Check container's internal UID docker exec banatie-postgres id # Expected output: uid=70(postgres) # Calculate host UID: 70 + 165536 = 165606 # Fix permissions if needed sudo chown -R 165606:165606 /opt/banatie/data/postgres ``` ## Container Specifications ### 1. Banatie API Container **Base Image**: `node:20-alpine` **Build Strategy**: Multi-stage for optimization **Source**: `apps/api-service/Dockerfile` **Features:** - Non-root user (apiuser:nodejs, UID 1001) - Health check endpoint at `/health` - Structured logging to `/app/apps/api-service/logs` - Database connection via Drizzle ORM ### 2. Banatie Landing Container **Base Image**: `node:20-alpine` **Build Strategy**: Multi-stage Next.js standalone **Source**: `apps/landing/Dockerfile` **Features:** - Non-root user (nextjs:nodejs, UID 1001) - Health check at root path - Waitlist email logging to `/app/waitlist-logs` ### 3. PostgreSQL Container **Image**: `postgres:15-alpine` **Purpose**: Dedicated database for Banatie services **Internal UID**: 70 (postgres user) **Database Schema** (managed by Drizzle ORM): - `organizations` - Multi-tenant organization data - `projects` - Projects within organizations - `api_keys` - API key authentication - `flows` - Generation workflow definitions - `images` - Image metadata and references - `generations` - Generation history - `prompt_url_cache` - URL caching for prompts - `live_scopes` - Live scope definitions **Note**: Schema is defined in `packages/database/src/schema/` and managed via Drizzle. The `scripts/init-db.sql` contains only PostgreSQL 15 permission fixes. ### 4. MinIO Container **Image**: `quay.io/minio/minio:latest` **Purpose**: S3-compatible object storage **Mode**: SNMD (Single Node Multi Drive) for full S3 compatibility and erasure coding **Storage Configuration:** ```yaml volumes: - /opt/banatie/data/minio/drive1:/data1 - /opt/banatie/data/minio/drive2:/data2 - /opt/banatie/data/minio/drive3:/data3 - /opt/banatie/data/minio/drive4:/data4 command: server /data{1...4} --console-address ":9001" ``` **Why SNMD:** - Full S3 API compatibility - Erasure coding for data durability - Required for Banatie startup project requirements ### 5. Storage Init Container **Image**: `minio/mc:latest` **Purpose**: One-time MinIO initialization **Restart Policy**: `no` (runs once) **Initialization Tasks:** 1. Create main bucket (`banatie`) 2. Create service account for application access 3. Attach readwrite policy to service account 4. Configure lifecycle policy for temp file cleanup (7 days) ## Production Docker Compose **File**: `/opt/banatie/docker-compose.yml` ```yaml # Banatie Production - VPS Isolated Deployment # Last Updated: December 2025 services: # =========================================== # API Service (Express.js) # =========================================== banatie-api: build: context: /home/usul/workspace/projects/banatie-service dockerfile: apps/api-service/Dockerfile target: production container_name: banatie-api restart: unless-stopped networks: - banatie-internal - proxy-network depends_on: banatie-postgres: condition: service_healthy banatie-minio: condition: service_healthy env_file: - /opt/banatie/.env - /opt/banatie/secrets.env environment: - IS_DOCKER=true - NODE_ENV=production volumes: - /opt/banatie/logs/api:/app/apps/api-service/logs healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000/health"] interval: 30s timeout: 10s retries: 3 start_period: 60s # =========================================== # Landing Page (Next.js standalone) # =========================================== banatie-landing: build: context: /home/usul/workspace/projects/banatie-service dockerfile: apps/landing/Dockerfile container_name: banatie-landing restart: unless-stopped networks: - banatie-internal - proxy-network depends_on: - banatie-postgres env_file: - /opt/banatie/.env - /opt/banatie/secrets.env environment: - IS_DOCKER=true - NODE_ENV=production - WAITLIST_LOGS_PATH=/app/waitlist-logs volumes: - /opt/banatie/data/waitlist-logs:/app/waitlist-logs healthcheck: test: ["CMD", "curl", "-f", "http://localhost:3000"] interval: 30s timeout: 10s retries: 3 start_period: 30s # =========================================== # PostgreSQL (Dedicated - ISOLATED) # =========================================== banatie-postgres: image: postgres:15-alpine container_name: banatie-postgres restart: unless-stopped networks: - banatie-internal # NO proxy-network - complete isolation from external access volumes: - /opt/banatie/data/postgres:/var/lib/postgresql/data - /opt/banatie/scripts/init-db.sql:/docker-entrypoint-initdb.d/01-init.sql:ro environment: POSTGRES_DB: ${POSTGRES_DB} POSTGRES_USER: ${POSTGRES_USER} POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} healthcheck: test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] interval: 30s timeout: 10s retries: 3 start_period: 40s # =========================================== # MinIO Object Storage (SNMD - 4 drives) # =========================================== banatie-minio: image: quay.io/minio/minio:latest container_name: banatie-minio restart: unless-stopped networks: - banatie-internal - proxy-network volumes: - /opt/banatie/data/minio/drive1:/data1 - /opt/banatie/data/minio/drive2:/data2 - /opt/banatie/data/minio/drive3:/data3 - /opt/banatie/data/minio/drive4:/data4 environment: MINIO_ROOT_USER: ${MINIO_ROOT_USER} MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD} MINIO_BROWSER_REDIRECT_URL: https://storage.banatie.app MINIO_SERVER_URL: https://cdn.banatie.app command: server /data{1...4} --console-address ":9001" healthcheck: test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"] interval: 30s timeout: 10s retries: 3 start_period: 40s # =========================================== # MinIO Initialization (one-time) # =========================================== banatie-storage-init: image: minio/mc:latest container_name: banatie-storage-init networks: - banatie-internal depends_on: banatie-minio: condition: service_healthy env_file: - /opt/banatie/secrets.env entrypoint: - /bin/sh - -c - | echo '=== MinIO Storage Initialization ===' echo 'Setting up MinIO alias...' mc alias set storage http://banatie-minio:9000 $${MINIO_ROOT_USER} $${MINIO_ROOT_PASSWORD} echo 'Creating main bucket...' mc mb --ignore-existing storage/banatie echo 'Creating service account...' mc admin user add storage $${MINIO_ACCESS_KEY} $${MINIO_SECRET_KEY} || echo 'User may already exist' echo 'Attaching readwrite policy...' mc admin policy attach storage readwrite --user=$${MINIO_ACCESS_KEY} || echo 'Policy may already be attached' echo 'Setting up lifecycle policy for temp cleanup...' cat > /tmp/lifecycle.json <<'EOF' { "Rules": [ { "ID": "temp-cleanup", "Status": "Enabled", "Filter": {"Prefix": "temp/"}, "Expiration": {"Days": 7} } ] } EOF mc ilm import storage/banatie < /tmp/lifecycle.json || echo 'Lifecycle policy may already exist' echo '=== Storage Initialization Completed ===' echo 'Bucket: banatie' echo 'Service User: configured' echo 'Lifecycle: 7-day temp cleanup' exit 0 restart: "no" # =========================================== # Networks # =========================================== networks: banatie-internal: driver: bridge internal: true # 🔒 CRITICAL: No external internet access proxy-network: name: proxy-network external: true # Existing Caddy network from /opt/services/ ``` ## Environment Configuration ### Public Environment (`/opt/banatie/.env`) ```bash # =========================================== # Banatie Production Environment # =========================================== # Application NODE_ENV=production PORT=3000 # Database (internal Docker hostname) DATABASE_URL=postgresql://${POSTGRES_USER}:${POSTGRES_PASSWORD}@banatie-postgres:5432/${POSTGRES_DB} POSTGRES_DB=banatie_db POSTGRES_USER=banatie_user # MinIO (internal Docker hostname) MINIO_ENDPOINT=banatie-minio:9000 MINIO_BUCKET_NAME=banatie MINIO_USE_SSL=false # Public URLs (for generated links) MINIO_PUBLIC_URL=https://cdn.banatie.app API_PUBLIC_URL=https://api.banatie.app # CORS CORS_ORIGIN=https://banatie.app,https://api.banatie.app # Multi-tenancy defaults DEFAULT_ORG_ID=demo DEFAULT_USER_ID=guest ``` ### Secrets Environment (`/opt/banatie/secrets.env`) **Permissions:** `chmod 600 /opt/banatie/secrets.env` ```bash # =========================================== # Banatie Secrets (NEVER COMMIT TO GIT) # =========================================== # Database Password POSTGRES_PASSWORD= # MinIO Root (Admin Console Access) MINIO_ROOT_USER=banatie_admin MINIO_ROOT_PASSWORD= # MinIO Service Account (Application Access) MINIO_ACCESS_KEY=banatie_service MINIO_SECRET_KEY= # AI Services GEMINI_API_KEY= # Security Tokens JWT_SECRET= SESSION_SECRET= ``` ### PostgreSQL Init Script (`/opt/banatie/scripts/init-db.sql`) **Minimal script for PostgreSQL 15 permission fix:** ```sql -- Banatie PostgreSQL 15 Permission Fix -- This ensures service user can create tables in public schema -- PostgreSQL 15 removed default CREATE privileges on public schema -- Grant CREATE permission to database owner GRANT CREATE ON SCHEMA public TO banatie_user; GRANT ALL ON SCHEMA public TO banatie_user; -- Note: Actual table creation is handled by Drizzle ORM -- Run 'pnpm --filter @banatie/database db:push' after first deployment ``` ## Caddy Integration ### Caddy Volume Update Add to `/opt/services/compose-files/caddy.yml`: ```yaml volumes: # ... existing volumes ... - /opt/banatie/logs:/opt/banatie/logs # Banatie access logs ``` ### Caddyfile Addition Add to `/opt/services/configs/caddy/Caddyfile`: ```caddy # ============================================ # BANATIE SERVICES (Isolated Startup) # ============================================ # Landing Page (Next.js standalone) banatie.app, www.banatie.app { reverse_proxy banatie-landing:3000 header { Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" X-XSS-Protection "1; mode=block" Referrer-Policy "strict-origin-when-cross-origin" } log { output file /opt/banatie/logs/landing-access.log { roll_size 10MiB roll_keep 5 } format json } } # API Service (Express.js) api.banatie.app { reverse_proxy banatie-api:3000 header { Strict-Transport-Security "max-age=31536000; includeSubDomains" X-Content-Type-Options "nosniff" X-Frame-Options "DENY" X-XSS-Protection "1; mode=block" } log { output file /opt/banatie/logs/api-access.log { roll_size 10MiB roll_keep 5 } format json } } # MinIO Console (Admin Interface) storage.banatie.app { reverse_proxy banatie-minio:9001 header { Strict-Transport-Security "max-age=31536000" X-Content-Type-Options "nosniff" X-Frame-Options "SAMEORIGIN" } log { output file /opt/banatie/logs/storage-access.log { roll_size 10MiB roll_keep 5 } format json } } # CDN - MinIO S3 (Public Images) cdn.banatie.app { reverse_proxy banatie-minio:9000 # CORS for browser access header { Access-Control-Allow-Origin "*" Access-Control-Allow-Methods "GET, HEAD, OPTIONS" Access-Control-Allow-Headers "Content-Type, Authorization, Range" Access-Control-Expose-Headers "Content-Length, Content-Range" } # Cache static images (1 year for immutable content) header /banatie/* { Cache-Control "public, max-age=31536000, immutable" } log { output file /opt/banatie/logs/cdn-access.log { roll_size 50MiB roll_keep 10 } format json } } ``` ## Deployment Process ### Prerequisites - [ ] VPS access configured (`ssh usul-vps`) - [ ] DNS records configured for banatie.app subdomains - [ ] Gitea access for source code - [ ] GEMINI_API_KEY obtained from Google ### Initial Deployment #### Step 1: Clone Source Code ```bash ssh usul-vps # Create workspace if not exists mkdir -p ~/workspace/projects cd ~/workspace/projects # Clone from Gitea git clone ssh://git@git.usul.su:2222/usulpro/banatie-service.git ``` #### Step 2: Create Production Directory ```bash # Create isolated Banatie directory sudo mkdir -p /opt/banatie/{scripts,logs/api,logs/landing} sudo chown -R usul:usul /opt/banatie # Create waitlist-logs directory (Docker will set permissions) mkdir -p /opt/banatie/data/waitlist-logs ``` #### Step 3: Create Configuration Files ```bash cd /opt/banatie # Create docker-compose.yml (copy from this document) nano docker-compose.yml # Create environment files nano .env nano secrets.env chmod 600 secrets.env # Create init-db.sql mkdir -p scripts nano scripts/init-db.sql ``` #### Step 4: Generate Secure Passwords ```bash # Generate all required secrets echo "POSTGRES_PASSWORD=$(openssl rand -base64 32 | tr -d '\n\r ')" >> secrets.env echo "MINIO_ROOT_USER=banatie_admin" >> secrets.env echo "MINIO_ROOT_PASSWORD=$(openssl rand -base64 32 | tr -d '\n\r ')" >> secrets.env echo "MINIO_ACCESS_KEY=banatie_service" >> secrets.env echo "MINIO_SECRET_KEY=$(openssl rand -base64 32 | tr -d '\n\r ')" >> secrets.env echo "JWT_SECRET=$(openssl rand -base64 64 | tr -d '\n\r ')" >> secrets.env echo "SESSION_SECRET=$(openssl rand -base64 32 | tr -d '\n\r ')" >> secrets.env echo "GEMINI_API_KEY=your_actual_key_here" >> secrets.env chmod 600 secrets.env ``` #### Step 5: Update Caddy Configuration ```bash # Add Banatie volume to Caddy cd /opt/services nano compose-files/caddy.yml # Add: - /opt/banatie/logs:/opt/banatie/logs # Add Banatie routes to Caddyfile nano configs/caddy/Caddyfile # Add Banatie configuration from this document # Restart Caddy to apply changes docker compose -f compose-files/caddy.yml --env-file .env up -d ``` #### Step 6: Remove Old Static Landing (if exists) ```bash # Remove old static deployment sudo rm -rf /var/www/banatie.app ``` #### Step 7: Build and Deploy ```bash cd /opt/banatie # Build all images docker compose build # Start services docker compose up -d # Check status docker compose ps docker compose logs -f ``` #### Step 8: Initialize Database Schema ```bash # After containers are running, push Drizzle schema cd ~/workspace/projects/banatie-service pnpm install # if not done pnpm --filter @banatie/database db:push ``` #### Step 9: Verify Deployment ```bash # Check health endpoints curl -f https://banatie.app curl -f https://api.banatie.app/health curl -f https://storage.banatie.app curl -f https://cdn.banatie.app/minio/health/live # Check SSL certificates echo | openssl s_client -servername banatie.app -connect banatie.app:443 2>/dev/null | openssl x509 -noout -dates # Check logs tail -f /opt/banatie/logs/*.log ``` ### Update Process ```bash ssh usul-vps # Pull latest code cd ~/workspace/projects/banatie-service git pull origin main # Rebuild and redeploy cd /opt/banatie docker compose build docker compose up -d --force-recreate # Run migrations if schema changed cd ~/workspace/projects/banatie-service pnpm --filter @banatie/database db:push # Verify curl -f https://api.banatie.app/health ``` ## Backup Strategy ### Database Backup ```bash # Create backup docker exec banatie-postgres pg_dump -U banatie_user banatie_db > /opt/banatie/backups/banatie_db_$(date +%Y%m%d).sql # Restore backup docker exec -i banatie-postgres psql -U banatie_user banatie_db < /opt/banatie/backups/banatie_db_YYYYMMDD.sql ``` ### MinIO Data Backup ```bash # Backup MinIO data (requires sudo due to Docker User NS Remapping) sudo tar -czf /opt/banatie/backups/minio_$(date +%Y%m%d).tar.gz -C /opt/banatie/data minio/ # Restore MinIO data sudo tar -xzf /opt/banatie/backups/minio_YYYYMMDD.tar.gz -C /opt/banatie/data ``` ### Full Backup ```bash # Complete Banatie backup sudo tar -czf ~/banatie_full_backup_$(date +%Y%m%d).tar.gz \ -C /opt banatie/ \ --exclude='banatie/data/postgres' \ --exclude='banatie/data/minio' # Database backup separately docker exec banatie-postgres pg_dump -U banatie_user banatie_db > ~/banatie_db_$(date +%Y%m%d).sql ``` ## Monitoring ### Health Check Endpoints | Endpoint | Expected | Description | |----------|----------|-------------| | `https://banatie.app` | 200 | Landing page | | `https://api.banatie.app/health` | 200 JSON | API health | | `https://storage.banatie.app` | 200 | MinIO console | | `https://cdn.banatie.app/minio/health/live` | 200 | MinIO S3 | ### Log Locations | Log | Path | |-----|------| | API Access | `/opt/banatie/logs/api-access.log` | | Landing Access | `/opt/banatie/logs/landing-access.log` | | Storage Console | `/opt/banatie/logs/storage-access.log` | | CDN Access | `/opt/banatie/logs/cdn-access.log` | | API Application | `/opt/banatie/logs/api/` | | Waitlist Emails | `/opt/banatie/data/waitlist-logs/` | ### Docker Status ```bash # Check all Banatie containers docker ps --filter "name=banatie" # Check resource usage docker stats --filter "name=banatie" # View logs docker compose -f /opt/banatie/docker-compose.yml logs -f [service] ``` ## Troubleshooting ### Container Won't Start ```bash # Check logs docker compose -f /opt/banatie/docker-compose.yml logs [service] # Check container status docker inspect banatie-[service] | jq '.[0].State' # Rebuild container docker compose -f /opt/banatie/docker-compose.yml build [service] docker compose -f /opt/banatie/docker-compose.yml up -d [service] ``` ### Database Connection Issues ```bash # Test database connectivity docker exec banatie-postgres pg_isready -U banatie_user -d banatie_db # Check environment variables docker exec banatie-api env | grep -E 'DATABASE|POSTGRES' # Connect to database manually docker exec -it banatie-postgres psql -U banatie_user -d banatie_db ``` ### MinIO Access Issues ```bash # Check MinIO status docker exec banatie-minio mc admin info local # Test S3 API curl -f http://localhost:9000/minio/health/live # Check bucket exists docker exec banatie-storage-init mc ls storage/ ``` ### Permission Issues (Docker User NS Remapping) ```bash # Check container UID docker exec banatie-postgres id # Expected: uid=70(postgres) # Calculate host UID and fix # PostgreSQL: 70 + 165536 = 165606 sudo chown -R 165606:165606 /opt/banatie/data/postgres # MinIO: 1000 + 165536 = 166536 sudo chown -R 166536:166536 /opt/banatie/data/minio ``` ### SSL Certificate Issues ```bash # Check Caddy logs docker logs caddy | grep -i "banatie\|certificate\|error" # Force certificate renewal docker exec caddy caddy reload --config /etc/caddy/Caddyfile # Check DNS resolution dig banatie.app +short dig api.banatie.app +short ``` ## Security Considerations ### Network Security - ✅ Internal network (`banatie-internal`) has no external access - ✅ PostgreSQL is not exposed (no `ports:` mapping) - ✅ All external access via Caddy HTTPS only - ✅ MinIO S3 API restricted to GET/HEAD for public access ### Container Security - ✅ Non-root users in application containers - ✅ Docker User Namespace Remapping enabled - ✅ Resource limits can be added as needed - ✅ Health checks for automatic restart ### Data Security - ✅ Secrets stored in separate file with 600 permissions - ✅ Database credentials not exposed - ✅ MinIO service account has limited permissions - ✅ JWT/Session secrets properly generated ### Access Control - ✅ API key authentication required - ✅ Master key for admin operations - ✅ Rate limiting at Caddy level possible - ✅ CORS configured for specific origins --- **Document Version**: 1.0 **Last Updated**: December 2025 **Maintained By**: VPS Project (usul.su) **Related Documentation**: VPS/manuals/banatie-deployment-manual.md