Server Setup
Prepare a fresh Linux server for the ZOL RAG system. This covers Docker installation, repository cloning, production secrets, and firewall hardening.
Checklist
- SSH into the server
- Install Docker Engine 24+ and Docker Compose v2.20+
- Clone the repository to
/opt/zol-rag - Create
.env.prodwith strong secrets (includingOPENAI_API_KEY) - Lock down file permissions (
chmod 600 .env.prod) - Configure firewall (only ports 22, 80, 443)
Step 1: Install Docker (5 min)
SSH into the server and run:
# Update system
sudo apt update && sudo apt upgrade -y
# Install Docker (official method)
curl -fsSL https://get.docker.com | sudo sh
# Add your user to the docker group (avoids sudo for docker commands)
sudo usermod -aG docker $USER
# Log out and back in for group change to take effect
exit
# SSH back in
# Verify Docker works
docker --version # Should be 24+
docker compose version # Should be v2.20+
Step 2: Confirm Outbound HTTPS to OpenAI (1 min)
Embedding inference uses OpenAI's hosted text-embedding-3-large model — the on-premise Ollama embedding container documented in earlier revisions of this guide was retired in April 2026 (see ADR-0048). The host therefore needs outbound HTTPS reachability to api.openai.com:443:
# Should print HTTP/2 401 (proves connectivity; the key isn't set yet)
curl -sI https://api.openai.com/v1/models | head -1
If the host is behind a corporate egress policy, allowlist api.openai.com before continuing — without it, EmbeddingService will fail at startup and the RAG pipeline will degrade to keyword-only fallback.
Step 3: Clone the Repository (2 min)
# Create the project directory
sudo mkdir -p /opt/zol-rag
sudo chown $USER:$USER /opt/zol-rag
# Clone the repository
git clone <REPO_URL> /opt/zol-rag
cd /opt/zol-rag
Step 4: Create Production Secrets (5 min)
cd /opt/zol-rag
# Copy the template
cp .env.prod.example .env.prod
# Generate strong secrets (run each, copy the output into .env.prod)
echo "SECRET_KEY=$(openssl rand -hex 32)"
echo "POSTGRES_PASSWORD=$(openssl rand -base64 24)"
echo "REDIS_PASSWORD=$(openssl rand -base64 24)"
echo "MINIO_ROOT_PASSWORD=$(openssl rand -base64 24)"
echo "GRAFANA_ADMIN_PASSWORD=$(openssl rand -base64 16)"
echo "KEYCLOAK_ADMIN_PASSWORD=$(openssl rand -base64 16)"
echo "KEYCLOAK_CLIENT_SECRET=$(openssl rand -hex 32)"
# Edit .env.prod with a text editor
nano .env.prod
Required Variables
| Variable | What to Put | Where to Get It |
|---|---|---|
SECRET_KEY | 64-character hex string | openssl rand -hex 32 |
POSTGRES_PASSWORD | Random strong password | openssl rand -base64 24 |
REDIS_PASSWORD | Random strong password | openssl rand -base64 24 |
MINIO_ROOT_PASSWORD | Random strong password | openssl rand -base64 24 |
GRAFANA_ADMIN_PASSWORD | Random password | openssl rand -base64 16 |
KEYCLOAK_ADMIN_PASSWORD | Keycloak admin console password | openssl rand -base64 16 |
KEYCLOAK_CLIENT_SECRET | Backend OIDC client secret | openssl rand -hex 32 |
OPENROUTER_API_KEY | Your OpenRouter key | https://openrouter.ai/keys |
CORS_ORIGINS | Your domain(s) | e.g., ["https://search.zol.be"] |
Example .env.prod
# === REQUIRED SECRETS ===
SECRET_KEY=<generated-256-bit-hex>
POSTGRES_USER=zol_rag_user
POSTGRES_PASSWORD=<strong-password>
POSTGRES_DB=zol_rag
DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB}
REDIS_PASSWORD=<strong-password>
REDIS_URL=redis://:${REDIS_PASSWORD}@redis:6379/0
MINIO_ROOT_USER=zoladmin
MINIO_ROOT_PASSWORD=<strong-password>
MINIO_ENDPOINT=minio:9000
MINIO_ACCESS_KEY=${MINIO_ROOT_USER}
MINIO_SECRET_KEY=${MINIO_ROOT_PASSWORD}
MINIO_BUCKET=zol-rag-documents
GRAFANA_ADMIN_PASSWORD=<strong-password>
# === KEYCLOAK (OIDC Identity Provider) ===
KEYCLOAK_ADMIN_PASSWORD=<strong-password>
KEYCLOAK_BASE_URL=https://YOUR_DOMAIN
KEYCLOAK_REALM=zol
KEYCLOAK_CLIENT_ID=zol-rag-backend
KEYCLOAK_CLIENT_SECRET=<generated-hex-string>
KEYCLOAK_ENABLED=true
KC_HOSTNAME=YOUR_DOMAIN
# === API KEYS ===
OPENROUTER_API_KEY=sk-or-<your-key>
JINA_API_KEY=jina_<your-key>
# === EMBEDDING ===
EMBEDDING_PROVIDER=openai
EMBEDDING_MODEL=text-embedding-3-large
EMBEDDING_DIMENSIONS=1536
# === APPLICATION ===
APP_NAME=ZOL Hospital Intelligent Search
ENVIRONMENT=production
DEBUG=false
LOG_LEVEL=INFO
CORS_ORIGINS=["https://search.zol.be","https://zol.novation.website"]
# === SAFETY ===
SAFETY_DISCLAIMER_TEXT="\n\n---\n_Dit is geen medisch advies. Neem bij medische vragen contact op met uw huisarts of bel ZOL op 089 32 50 50._"
The KEYCLOAK_CLIENT_SECRET must match the client secret configured in the Keycloak admin console for the zol-rag-backend client. If you use the auto-imported realm from scripts/keycloak/zol-realm.json, update the secret in the realm export file before first startup, or regenerate it via the Keycloak admin console after startup.
Lock Down Permissions
chmod 600 .env.prod
Step 5: Configure Keycloak Realm (Optional)
The Keycloak realm is automatically imported on first startup from scripts/keycloak/zol-realm.json. Before first startup, you may want to review and customize:
# Review the realm configuration
cat scripts/keycloak/zol-realm.json | python3 -m json.tool | head -50
# Key settings to verify:
# - Realm name: "zol"
# - Frontend client: "zol-rag-frontend" (public client)
# - Backend client: "zol-rag-backend" (confidential client)
# - Redirect URIs match your domain
If you need to update redirect URIs for your domain:
# Update the redirect URIs in the realm export before first startup
# Find and replace the placeholder domain with your actual domain
sed -i 's|https://test.medchat.health|https://YOUR_DOMAIN|g' scripts/keycloak/zol-realm.json
Step 6: Firewall Hardening
Only expose SSH, HTTP, and HTTPS. All infrastructure ports stay internal to Docker.
# Install UFW if not present
sudo apt install -y ufw
# Default: deny incoming, allow outgoing
sudo ufw default deny incoming
sudo ufw default allow outgoing
# Allow SSH (change port if you use a non-standard one)
sudo ufw allow 22/tcp
# Allow HTTP and HTTPS
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
# Enable firewall
sudo ufw enable
# Verify
sudo ufw status verbose
Expected output:
Status: active
To Action From
-- ------ ----
22/tcp ALLOW Anywhere
80/tcp ALLOW Anywhere
443/tcp ALLOW Anywhere
Keycloak (port 8080), Grafana (port 3000), and other infrastructure ports are bound to 127.0.0.1 only. They are not exposed to the internet and are accessible via SSH tunnel.
Optional: Restrict SSH to Specific IPs
# Replace with your office/VPN IP
sudo ufw delete allow 22/tcp
sudo ufw allow from YOUR_IP/32 to any port 22 proto tcp
Verify Setup
# Docker is working
docker run --rm hello-world
# OpenAI Embeddings API is reachable (returns HTTP/2 401 until the key is set)
curl -sI https://api.openai.com/v1/models | head -1
# Repository is cloned
ls /opt/zol-rag/docker/docker-compose.infra.yml
# Secrets file exists and is locked down
stat -c "%a %U" /opt/zol-rag/.env.prod
# Expected: 600 <your-user>
# Keycloak realm export is present
ls /opt/zol-rag/scripts/keycloak/zol-realm.json
# Firewall is active
sudo ufw status
Architectural Evolution
This setup procedure has been simplified twice since the original deployment. March 2026: Neo4j password generation and environment variables were removed after entity relationships were migrated to PostgreSQL taxonomy tables — see ADR-0053 (master record). Keycloak environment variables were added to support the new OIDC authentication layer. April 2026: the ollama pull bge-m3 setup step was removed entirely; embedding inference now uses OpenAI's hosted API (ADR-0048), eliminating an on-host system service and ≈1.5 GB of model storage.
Next: Start Infrastructure →