# ECIJA Intranet - NUC Deployment Guide > **Purpose:** Step-by-step deployment of the ECIJA Intranet Django backend on the NUC home server. > **Source project:** `/Users/agutierrez/Desktop/ECIJA-Intranet/` > **Database dump:** `/Users/agutierrez/Desktop/flexicar-intranet-backend/dump-flexicar-202602071346-backup/` > **Credentials file:** `/Users/agutierrez/Desktop/flexicar-intranet-backend/windy-shoreline-225910-efd33901e56c.json` > **Installation reference:** `/Users/agutierrez/Desktop/flexicar-intranet-backend/instalacion_intra.docx` --- ## Architecture Overview ``` ┌─────────────────────────────────────────────┐ │ NUC Server (192.168.1.3) │ │ Tailscale: 100.113.153.45 │ │ │ intranet.nuc.lan │ ┌──────────┐ ┌────────────────────┐ │ ──────────────────►│ │ Traefik │───►│ Django/Gunicorn │ │ │ │ :80/:443 │ │ :8010 │ │ │ └──────────┘ └────────┬───────────┘ │ │ │ │ │ ┌────────┼───────┐ │ │ │ │ │ │ │ ┌──────▼──┐ ┌───▼───┐ │ │ │ │Postgres │ │ Redis │ │ │ │ │ :5434 │ │ :6379 │ │ │ │ └─────────┘ └───────┘ │ │ │ │ │ │ ┌──────────────────────┐│ │ │ │ Background Worker ││ │ │ │ (process_tasks) ││ │ │ └──────────────────────┘│ │ └───────────────────────────────────┼────────┘ │ ▼ ┌──────────────────┐ │ Google Cloud │ │ Storage (GCS) │ │ Bucket: │ │ ecija-intranet │ └──────────────────┘ ``` ### Design principle: change nothing, only deploy The NUC is the **compute host only**. All external integrations (GCS, HubSpot, Azure AD, 3G, reCAPTCHA) stay exactly as the original app expects them. This means: - Zero code changes to `settings.py` - Same env vars as the official installation doc - Same GCS bucket and credentials - If it works locally on a Mac, it works on the NUC ### What the NUC already provides (no setup needed) | Service | Port | Notes | |---------|------|-------| | Redis | 6379 | UUID: `vkg44cgcss4ococgk0cs000o` - reuse existing | | Traefik | 80/443 | Reverse proxy for `intranet.nuc.lan` | | n8n | 5678 | Workflow automation (intranet has n8n webhooks) | | Adminer | 8088 | DB admin UI | | CloudBeaver | 8978 | DB admin UI (alternative) | | Dozzle | 9999 | Container log viewer | | Uptime Kuma | 3001 | Service monitoring | | Kopia | 51515 | Backup management | ### What needs to be deployed | Service | Port | Method | |---------|------|--------| | PostgreSQL (dedicated) | 5434 | Coolify service | | Django + Gunicorn | 8010 | Coolify docker-compose | | Background Worker | - | Same image, different command | --- ## Phase 1: PostgreSQL Database ### 1.1 Deploy a dedicated PostgreSQL instance via Coolify Use port **5434** to avoid conflicts with existing Postgres instances (5432, 5433, 5442 already in use). ```bash # Via Coolify MCP: mcp__coolify__service( action="create", type="postgresql", name="ecija-intranet-db", server_uuid="qk84w0goo4w48g4ggsoo0oss", project_uuid="a8484ggc88c40w4g4k004ow0", environment_name="production", instant_deploy=True ) ``` If native type fails, deploy via docker-compose: ```yaml services: ecija-postgres: image: postgres:17 container_name: ecija-intranet-db restart: unless-stopped environment: POSTGRES_USER: postgres POSTGRES_PASSWORD: postgres POSTGRES_DB: flexicar-prod ports: - "5434:5432" volumes: - ecija_pgdata:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U postgres -d flexicar-prod"] interval: 10s timeout: 5s retries: 5 volumes: ecija_pgdata: ``` ### 1.2 Enable required PostgreSQL extensions ```bash ssh nuc "docker exec ecija-intranet-db psql -U postgres -d flexicar-prod -c 'CREATE EXTENSION IF NOT EXISTS pg_trgm;'" ssh nuc "docker exec ecija-intranet-db psql -U postgres -d flexicar-prod -c 'CREATE EXTENSION IF NOT EXISTS unaccent;'" ``` ### 1.3 Transfer and restore the database dump The dump is in pg_dump **directory format** (4 GB, 453 tables). **Important:** The dump was exported from a database called `flexicar` with owner `flexicar`, but the Django app expects `DB_NAME=flexicar-prod` with `DATABASE_USER=postgres` (per the official install doc). We restore into `flexicar-prod` using `--no-owner` so all objects are owned by `postgres`. ```bash # Step 1: Transfer dump directory to NUC scp -r /Users/agutierrez/Desktop/flexicar-intranet-backend/dump-flexicar-202602071346-backup/ nuc:/tmp/ecija-dump/ # Step 2: Copy dump into the Postgres container ssh nuc "docker cp /tmp/ecija-dump/ ecija-intranet-db:/tmp/ecija-dump/" # Step 3: Restore the dump into flexicar-prod (ignore original ownership) ssh nuc "docker exec ecija-intranet-db pg_restore \ -U postgres \ -d flexicar-prod \ --no-owner \ --no-privileges \ --jobs=4 \ /tmp/ecija-dump/" # Step 4: Verify table count (should be ~453) ssh nuc "docker exec ecija-intranet-db psql -U postgres -d flexicar-prod -c \"SELECT count(*) FROM information_schema.tables WHERE table_schema = 'public';\"" # Step 5: Clean up dump from container ssh nuc "docker exec ecija-intranet-db rm -rf /tmp/ecija-dump/" # Step 6: Clean up dump from NUC host ssh nuc "rm -rf /tmp/ecija-dump/" ``` ### 1.4 Verify database is accessible ```bash # From NUC ssh nuc "docker exec ecija-intranet-db psql -U postgres -d flexicar-prod -c 'SELECT current_database(), current_user;'" # From Adminer (browser) # URL: http://192.168.1.3:8088 # System: PostgreSQL # Server: ecija-intranet-db:5432 (or 192.168.1.3:5434 if external) # Username: postgres # Password: postgres # Database: flexicar-prod ``` --- ## Phase 2: Transfer Project Files ### 2.1 Transfer the project to NUC ```bash # Exclude unnecessary files (venv, node_modules, caches) rsync -avz --progress \ --exclude '.git' \ --exclude 'env/' \ --exclude 'node_modules/' \ --exclude '.DS_Store' \ --exclude '__pycache__' \ --exclude '*.pyc' \ /Users/agutierrez/Desktop/ECIJA-Intranet/ \ nuc:/opt/ecija-intranet/ ``` ### 2.2 Transfer the Google Cloud Storage credentials The GCS service account key is required for static/media file storage. This is the same file used in the original production environment. **Important:** The official install doc sets `GS_CREDENTIALS="/secrets/bucket/credentials.json"` (generic name), while `GS_CREDENTIALS_LOCAL` uses the full filename. We mount the credentials dir to `/secrets/bucket/` and copy the file as `credentials.json` to match the production `GS_CREDENTIALS` path. ```bash # Create the credentials directory on NUC ssh nuc "mkdir -p /opt/ecija-intranet/config/credenciales" # Transfer the service account JSON (keep original name for GS_CREDENTIALS_LOCAL) scp /Users/agutierrez/Desktop/flexicar-intranet-backend/windy-shoreline-225910-efd33901e56c.json \ nuc:/opt/ecija-intranet/config/credenciales/windy-shoreline-225910-efd33901e56c.json # Also copy as credentials.json (for GS_CREDENTIALS=/secrets/bucket/credentials.json) ssh nuc "cp /opt/ecija-intranet/config/credenciales/windy-shoreline-225910-efd33901e56c.json \ /opt/ecija-intranet/config/credenciales/credentials.json" ``` ### 2.3 Optionally push to NUC's Gitea for version control ```bash # From local machine cd /Users/agutierrez/Desktop/ECIJA-Intranet # Create the repo in Gitea first (via browser at http://192.168.1.3:3030 or API) # Then add remote and push git remote add nuc http://192.168.1.3:3030/alezmad/ecija-intranet.git git push nuc main ``` --- ## Phase 3: Django Application Container ### 3.1 Create the production Dockerfile The existing Dockerfile uses `gcr.io/google_appengine/python` (Google App Engine base image) which is unnecessary on the NUC. Create file on NUC at `/opt/ecija-intranet/Dockerfile.nuc`: ```dockerfile FROM python:3.9-slim # System dependencies for psycopg2, Pillow, lxml, pdf libs RUN apt-get update && apt-get install -y --no-install-recommends \ build-essential \ libpq-dev \ libjpeg-dev \ libfreetype6-dev \ libxml2-dev \ libxslt1-dev \ zlib1g-dev \ libffi-dev \ libmupdf-dev \ git \ curl \ && rm -rf /var/lib/apt/lists/* # Install Node.js 18 for frontend build RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - \ && apt-get install -y nodejs \ && rm -rf /var/lib/apt/lists/* WORKDIR /app # Python dependencies (root level - includes all packages) COPY requirements.txt /app/requirements.txt RUN pip install --upgrade pip && \ pip install --use-pep517 -r /app/requirements.txt # Copy application code COPY app/ /app/ # Frontend dependencies and build RUN npm install && npm run build EXPOSE 8010 # Entrypoint script COPY docker-entrypoint.sh /docker-entrypoint.sh RUN chmod +x /docker-entrypoint.sh ENTRYPOINT ["/docker-entrypoint.sh"] CMD ["gunicorn", "ecija.wsgi:application", \ "--workers", "4", \ "--threads", "4", \ "--timeout", "120", \ "--graceful-timeout", "30", \ "--keep-alive", "5", \ "--max-requests", "1000", \ "--max-requests-jitter", "100", \ "--bind", "0.0.0.0:8010", \ "--access-logfile", "-", \ "--error-logfile", "-", \ "--log-level", "info"] ``` ### 3.2 Create the entrypoint script Create file on NUC at `/opt/ecija-intranet/docker-entrypoint.sh`: ```bash #!/bin/bash set -e echo "Running database migrations..." python manage.py migrate --noinput echo "Collecting static files..." python manage.py collectstatic --noinput || true echo "Starting application..." exec "$@" ``` ### 3.3 Deploy via Coolify docker-compose Deploy through Coolify MCP using `docker_compose_raw`: ```yaml services: ecija-web: build: context: /opt/ecija-intranet dockerfile: Dockerfile.nuc container_name: ecija-intranet-web restart: unless-stopped ports: - "8010:8010" env_file: - /opt/ecija-intranet/.env.production volumes: - ecija_media:/app/media - /opt/ecija-intranet/config/credenciales:/secrets/bucket:ro networks: - ecija-network healthcheck: test: ["CMD", "curl", "-f", "http://localhost:8010/"] interval: 30s timeout: 10s retries: 3 ecija-worker: build: context: /opt/ecija-intranet dockerfile: Dockerfile.nuc container_name: ecija-intranet-worker restart: unless-stopped command: ["python", "manage.py", "process_tasks"] env_file: - /opt/ecija-intranet/.env.production volumes: - ecija_media:/app/media - /opt/ecija-intranet/config/credenciales:/secrets/bucket:ro networks: - ecija-network volumes: ecija_media: networks: ecija-network: driver: bridge ``` --- ## Phase 4: Environment Configuration ### 4.1 Create the production env file This uses the **same values from the official installation doc** (`instalacion_intra.docx`), with only the DB host changed to point to the NUC container. Create on NUC at `/opt/ecija-intranet/.env.production`: ```bash # ============================================================ # ECIJA Intranet - NUC Production Environment # Source: /Users/agutierrez/Desktop/flexicar-intranet-backend/instalacion_intra.docx # Principle: IDENTICAL to official install doc, only DB_HOST changed # ============================================================ # Lines below are a 1:1 copy from the docx. Only DB_HOST differs # (container name instead of 127.0.0.1). # Do NOT add extra vars that aren't in the original doc. DATABASE_USER=postgres DATABASE_PASSWORD=postgres DJANGO_ENVIRONMENT=local MS_SECRET_KEY=H6CLoLV?kO.uaZOI3lkiHv=L:jWqk12t G_RECAPTCHA_SECRET_KEY= G_RECAPTCHA_SITE_KEY= DEBUG=True DEMO=False GKE_ENABLED=True AG=False GS=True RECAPTCHA=True DB_NAME_DEMO=ecija_demo DB_NAME=flexicar-prod DB_HOST=ecija-intranet-db DB_PORT=5432 STATIC_URL=/static/ MEDIA_URL=/media/ GS_BUCKET_NAME=ecija-intranet GS_PROJECT_ID=ecija-intranet GS_CREDENTIALS=/secrets/bucket/credentials.json TENANT_ID=8e39b277-105b-4b2e-b21c-e06cd806a070 CLIENT_ID=6858acbb-098f-4261-987d-6a9892b06d86 RELYING_PARTY_ID=api://18d841fc-2054-4b76-992a-2a11fc9ade78 AUDIENCE=api://18d841fc-2054-4b76-992a-2a11fc9ade78 CODIGO_EMPRESA_3G=ecijades LOGIN_3G=ws64304 PASSWORD_3G=z4JcpcTqjd IDIOMA_3G=ES API_KEY_3G=m23o2MHszn128snAkwkaAKnzLK29s91klalzl19Jjkzj19zlxlnwZNmMnN1812nznNMjsjz9MnznWwqsjzlSAK2znqh0z9134 APPS_INSTALADAS=django_auth_adfs,ecija,meta_aepd_app,agile,gestor_licencias,telefonia,administracion,certificados3g,ticketing,analytics,demo,gestion_documental,sp_widgets,configuracion_interfaz,widgets_dashboard,historico,comentarios,avisos,procesal,canal_denuncias,referidos,notificacion,busqueda_google,tareas,bank_movements,notas,oportunidades,reclamaciones,nux,eventos,fleximanage,comunicaciones HUBSPOT_API_URL_COMPANIES=https://api.hubapi.com/companies/v2/companies HUBSPOT_API_URL_CONTACTS=https://api.hubapi.com/contacts/v1/contact HUBSPOT_API_KEY=pat-eu1-5b7e6860-e273-4a92-b6f8-b6eead476505 GS_CREDENTIALS_LOCAL=/secrets/bucket/windy-shoreline-225910-efd33901e56c.json GOOGLE_APPLICATION_CREDENTIALS=/secrets/bucket/windy-shoreline-225910-efd33901e56c.json ``` ### 4.2 Alignment with official install doc The `.env.production` file is a **1:1 copy** of the env vars from `instalacion_intra.docx` with one change: | Variable | Official doc | NUC deployment | Why | |----------|-------------|----------------|-----| | `DB_HOST` | `127.0.0.1` | `ecija-intranet-db` | Container networking (Docker DNS resolves container names) | Everything else is identical. GCS stays because: - The app is built on `django-storages[google]`, not S3 - The credentials already work - Zero code changes required - Same config carries over to real production --- ## Phase 5: Networking & DNS ### 5.1 Add DNS entry for `intranet.nuc.lan` ```bash ssh nuc "ssh -i ~/.ssh/id_ed25519_nuc root@192.168.1.1 ' uci add dhcp domain uci set dhcp.@domain[-1].name=\"intranet.nuc.lan\" uci set dhcp.@domain[-1].ip=\"100.113.153.45\" uci commit dhcp /etc/init.d/dnsmasq restart '" ``` ### 5.2 Add Traefik route Edit on NUC: `/data/coolify/proxy/dynamic/nuc-services.yaml` Add the following router and service (merge with existing entries): ```yaml http: routers: ecija-intranet: rule: Host(`intranet.nuc.lan`) service: ecija-intranet entryPoints: - http services: ecija-intranet: loadBalancer: servers: - url: http://host.docker.internal:8010 ``` ### 5.3 Update Homepage dashboard Edit on NUC: `/opt/homepage/config/services.yaml` Add under appropriate section: ```yaml - ECIJA Intranet: - Intranet: href: http://intranet.nuc.lan description: ECIJA Law Firm Intranet icon: django server: nuc container: ecija-intranet-web ``` --- ## Phase 6: Monitoring & Backup ### 6.1 Add to Uptime Kuma Via browser at `http://192.168.1.3:3001`: 1. Add new monitor 2. Type: HTTP(s) 3. URL: `http://192.168.1.3:8010` 4. Name: ECIJA Intranet 5. Heartbeat interval: 60s ### 6.2 Add database to Kopia backup The PostgreSQL volume `ecija_pgdata` should be included in Kopia's backup schedule. ```bash # Verify the volume exists ssh nuc "docker volume inspect ecija_pgdata" # Kopia should auto-discover Docker volumes # Verify in Kopia UI at http://192.168.1.3:51515 ``` --- ## Phase 7: Verification Checklist Run these commands after deployment to verify everything works: ```bash # 1. Database is running and accessible ssh nuc "docker exec ecija-intranet-db psql -U postgres -d flexicar-prod -c 'SELECT count(*) FROM information_schema.tables WHERE table_schema = '\''public'\'';'" # Expected: ~453 tables # 2. Extensions are enabled ssh nuc "docker exec ecija-intranet-db psql -U postgres -d flexicar-prod -c 'SELECT extname FROM pg_extension;'" # Expected: pg_trgm, unaccent, plpgsql # 3. Django container is running ssh nuc "docker ps --format 'table {{.Names}}\t{{.Status}}\t{{.Ports}}' | grep ecija" # Expected: ecija-intranet-web (Up), ecija-intranet-worker (Up) # 4. Django migrations applied ssh nuc "docker logs ecija-intranet-web 2>&1 | grep -i 'migrat'" # 5. Web server responds ssh nuc "curl -s -o /dev/null -w '%{http_code}' http://localhost:8010/" # Expected: 200 or 302 (redirect to login) # 6. GraphQL API responds ssh nuc "curl -s -o /dev/null -w '%{http_code}' http://localhost:8010/api/graphql/" # Expected: 200 or 400 (no query provided = normal) # 7. Background worker is processing ssh nuc "docker logs ecija-intranet-worker 2>&1 | tail -5" # 8. DNS resolves nslookup intranet.nuc.lan # Expected: 100.113.153.45 # 9. Traefik routes correctly curl -s -o /dev/null -w '%{http_code}' http://intranet.nuc.lan/ # Expected: 200 or 302 # 10. GCS connectivity (from web container) ssh nuc "docker exec ecija-intranet-web python -c \" from google.cloud import storage client = storage.Client() bucket = client.bucket('ecija-intranet') print('GCS connection OK, bucket:', bucket.name) \"" ``` --- ## Quick Reference ### Service URLs (after deployment) | Service | URL | Direct Port | |---------|-----|-------------| | Intranet Web | `http://intranet.nuc.lan` | `http://192.168.1.3:8010` | | Intranet DB | - | `192.168.1.3:5434` | | DB Admin (Adminer) | - | `http://192.168.1.3:8088` | | Container Logs | - | `http://192.168.1.3:9999` (Dozzle) | ### Container Names | Container | Purpose | |-----------|---------| | `ecija-intranet-db` | PostgreSQL 17 database | | `ecija-intranet-web` | Django + Gunicorn web server | | `ecija-intranet-worker` | Background task processor | ### Database Credentials | Field | Value | |-------|-------| | Host (from container) | `ecija-intranet-db` | | Host (from NUC host) | `192.168.1.3` | | Port (internal) | `5432` | | Port (external) | `5434` | | User | `postgres` | | Password | `postgres` | | Database | `flexicar-prod` | ### Key File Locations (on NUC) | File | Path | |------|------| | Project root | `/opt/ecija-intranet/` | | Django app | `/opt/ecija-intranet/app/` | | Environment file | `/opt/ecija-intranet/.env.production` | | GCP credentials | `/opt/ecija-intranet/config/credenciales/windy-shoreline-225910-efd33901e56c.json` | | Dockerfile | `/opt/ecija-intranet/Dockerfile.nuc` | | Entrypoint | `/opt/ecija-intranet/docker-entrypoint.sh` | | Media volume | Docker volume `ecija_media` | | DB volume | Docker volume `ecija_pgdata` | | Traefik config | `/data/coolify/proxy/dynamic/nuc-services.yaml` | | Homepage config | `/opt/homepage/config/services.yaml` | ### Key File Locations (on Mac - source files) | File | Path | |------|------| | Project source | `/Users/agutierrez/Desktop/ECIJA-Intranet/` | | Database dump (directory) | `/Users/agutierrez/Desktop/flexicar-intranet-backend/dump-flexicar-202602071346-backup/` | | Database dump (tar) | `/Users/agutierrez/Desktop/flexicar-intranet-backend/dump-flexicar-202602071346-backup.tar` | | GCP service account key | `/Users/agutierrez/Desktop/flexicar-intranet-backend/windy-shoreline-225910-efd33901e56c.json` | | Installation doc | `/Users/agutierrez/Desktop/flexicar-intranet-backend/instalacion_intra.docx` | | Original docker-compose | `/Users/agutierrez/Desktop/flexicar-intranet-backend/docker-compose.yaml` | ### Coolify Identifiers | Field | Value | |-------|-------| | Server UUID | `qk84w0goo4w48g4ggsoo0oss` | | Project UUID | `a8484ggc88c40w4g4k004ow0` | | Environment | `production` | --- ## Troubleshooting ### Django won't start - missing module ```bash # Check logs ssh nuc "docker logs ecija-intranet-web 2>&1 | tail -30" # If pip dependency issue, exec into container ssh nuc "docker exec -it ecija-intranet-web pip install " ``` ### Database connection refused ```bash # Verify Postgres is running ssh nuc "docker ps | grep ecija-intranet-db" # Check if containers share a network ssh nuc "docker network inspect ecija-network" # Test connectivity from web container ssh nuc "docker exec ecija-intranet-web python -c \" import psycopg2 conn = psycopg2.connect(host='ecija-intranet-db', port=5432, user='postgres', password='postgres', dbname='flexicar-prod') print('Connection OK') conn.close() \"" ``` ### Static files not loading (GCS issues) ```bash # Test GCS credentials from container ssh nuc "docker exec ecija-intranet-web python -c \" from google.cloud import storage client = storage.Client() buckets = list(client.list_buckets()) print('Accessible buckets:', [b.name for b in buckets]) \"" # Verify credentials file is mounted ssh nuc "docker exec ecija-intranet-web ls -la /secrets/bucket/" # Run collectstatic manually ssh nuc "docker exec ecija-intranet-web python manage.py collectstatic --noinput" ``` ### pg_restore fails with "role flexicar does not exist" The dump has `OWNER TO flexicar` statements. Using `--no-owner` should handle this, but if you see warnings about role `flexicar` not existing, you can safely ignore them (objects will be owned by `postgres`). Alternatively, create the role: ```bash ssh nuc "docker exec ecija-intranet-db psql -U postgres -d flexicar-prod -c 'CREATE ROLE flexicar WITH LOGIN;'" 2>/dev/null || true ``` ### Frontend assets not built ```bash # Exec into container and build frontend ssh nuc "docker exec -it ecija-intranet-web bash -c 'cd /app && npm install && npm run build'" ``` ### Memory issues during restore The dump is 4 GB. If the NUC runs low on memory: ```bash # Use single-job restore (slower but less memory) ssh nuc "docker exec ecija-intranet-db pg_restore \ -U postgres \ -d flexicar-prod \ --no-owner \ --no-privileges \ --jobs=1 \ /tmp/ecija-dump/" ``` ### Container can't reach GCS (network issue) ```bash # Test internet connectivity from container ssh nuc "docker exec ecija-intranet-web curl -s -o /dev/null -w '%{http_code}' https://storage.googleapis.com/" # Expected: 200 or 400 # If blocked, check Docker DNS ssh nuc "docker exec ecija-intranet-web cat /etc/resolv.conf" ``` --- ## Notes - The `-e git+https://github.com/Seykotron/django-cruds-adminlte.git` dependency in requirements.txt requires git to be installed in the Docker image (included in the Dockerfile.nuc) - The `jinjacompiler` npm package in `app/package.json` uses a GitHub PAT token in its URL - this may expire and need updating - Azure AD auth (`AG=False`) is disabled - the app falls back to Django's built-in auth with username/password - `GS_CREDENTIALS=/secrets/bucket/credentials.json` (generic name) and `GS_CREDENTIALS_LOCAL` (full filename) both point to the mounted volume at `/secrets/bucket/`. Both files exist because we copy the JSON as both names in Phase 2.2 - Background worker (`process_tasks`) should always be running - it handles async operations like email sending, report generation, etc. - The database dump was created from PostgreSQL 17.7 - the container uses PostgreSQL 17 for compatibility - `DEBUG=True` matches the official install doc. Switch to `False` once everything is confirmed working. - The NUC's existing Redis (port 6379) can be used for Django caching if needed - add `REDIS_URL` to the env file and ensure the containers share a Docker network or use the host IP - The database name difference: the dump creates DB `flexicar` but Django expects `flexicar-prod` (per install doc). We restore into `flexicar-prod` using `--no-owner` which handles this correctly. - `DATABASE_USER=postgres` and `DATABASE_PASSWORD=postgres` match the official install doc exactly. These are development credentials.