Add operational documentation

CloudBeaver database manager guide, Ecija intranet deployment,
Gitea-Coolify auto-deploy and integration docs, monitoring setup
with presentation, remote access guide, security architecture,
and Turbostarter deployment procedure.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Alejandro Gutiérrez
2026-02-18 15:17:18 +01:00
parent 1aa7ebcde3
commit 8b503a549c
9 changed files with 3817 additions and 0 deletions

View File

@@ -0,0 +1,731 @@
# 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 <missing-package>"
```
### 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.