docs: add Setup Wizard, Production Readiness, Database, K8s, and Licensing guides
Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,354 @@
|
||||
# Database Configuration
|
||||
|
||||
DocuElevate uses [SQLAlchemy](https://www.sqlalchemy.org/) as its ORM and [Alembic](https://alembic.sqlalchemy.org/) for schema migrations. Any SQLAlchemy-compatible database is supported; this guide covers the most common choices.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Supported Databases](#supported-databases)
|
||||
- [Configuration](#configuration)
|
||||
- [SQLite (Development)](#sqlite-development)
|
||||
- [PostgreSQL (Production)](#postgresql-production)
|
||||
- [MySQL / MariaDB](#mysql--mariadb)
|
||||
- [Schema Migrations with Alembic](#schema-migrations-with-alembic)
|
||||
- [Connection Pooling](#connection-pooling)
|
||||
- [Backup Procedures](#backup-procedures)
|
||||
- [Performance Optimization](#performance-optimization)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Supported Databases
|
||||
|
||||
| Database | Recommended Use | Notes |
|
||||
|----------|----------------|-------|
|
||||
| **SQLite** | Development, single-user demos | Default. File-based. Not safe for multi-replica deployments. |
|
||||
| **PostgreSQL** | Production, multi-replica | Strongly recommended for production. Full feature support. |
|
||||
| **MySQL / MariaDB** | Production (alternative) | Supported; PostgreSQL preferred for JSON column support. |
|
||||
|
||||
---
|
||||
|
||||
## Configuration
|
||||
|
||||
Set the `DATABASE_URL` environment variable to point at your database:
|
||||
|
||||
```bash
|
||||
# SQLite (default)
|
||||
DATABASE_URL=sqlite:///./app/database.db
|
||||
|
||||
# PostgreSQL
|
||||
DATABASE_URL=postgresql://user:password@host:5432/docuelevate
|
||||
|
||||
# PostgreSQL with SSL
|
||||
DATABASE_URL=postgresql://user:password@host:5432/docuelevate?sslmode=require
|
||||
|
||||
# MySQL
|
||||
DATABASE_URL=mysql+pymysql://user:password@host:3306/docuelevate
|
||||
```
|
||||
|
||||
For Docker Compose, add this to your `.env` file or `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
environment:
|
||||
DATABASE_URL: postgresql://docuelevate:secret@postgres:5432/docuelevate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## SQLite (Development)
|
||||
|
||||
SQLite requires no additional services and is the zero-configuration default.
|
||||
|
||||
```bash
|
||||
DATABASE_URL=sqlite:///./app/database.db
|
||||
```
|
||||
|
||||
**Limitations:**
|
||||
|
||||
- Cannot be safely shared across multiple processes or containers.
|
||||
- Not suitable for any multi-replica deployment.
|
||||
- No support for concurrent writes under load.
|
||||
- Backup is a simple file copy, but requires the application to be stopped to ensure consistency.
|
||||
|
||||
**When to use:** Local development, automated testing, or single-user self-hosted setups with no redundancy requirement.
|
||||
|
||||
---
|
||||
|
||||
## PostgreSQL (Production)
|
||||
|
||||
PostgreSQL is the recommended database for any production deployment.
|
||||
|
||||
### Install and Create the Database
|
||||
|
||||
```sql
|
||||
-- Run as a PostgreSQL superuser (e.g. postgres)
|
||||
CREATE USER docuelevate WITH PASSWORD 'strongpassword';
|
||||
CREATE DATABASE docuelevate OWNER docuelevate;
|
||||
GRANT ALL PRIVILEGES ON DATABASE docuelevate TO docuelevate;
|
||||
```
|
||||
|
||||
### Docker Compose with Bundled PostgreSQL
|
||||
|
||||
Add a `postgres` service to your `docker-compose.yml`:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
environment:
|
||||
POSTGRES_USER: docuelevate
|
||||
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||
POSTGRES_DB: docuelevate
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
restart: unless-stopped
|
||||
|
||||
api:
|
||||
depends_on:
|
||||
- postgres
|
||||
environment:
|
||||
DATABASE_URL: postgresql://docuelevate:${POSTGRES_PASSWORD}@postgres:5432/docuelevate
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
```
|
||||
|
||||
Set `POSTGRES_PASSWORD` in your `.env` file.
|
||||
|
||||
### Managed Cloud PostgreSQL
|
||||
|
||||
Using a managed service (AWS RDS, Google Cloud SQL, Azure Database for PostgreSQL, Supabase, etc.) is recommended for production because it handles backups, point-in-time recovery, and maintenance.
|
||||
|
||||
Example for Amazon RDS:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql://docuelevate:strongpassword@my-instance.us-east-1.rds.amazonaws.com:5432/docuelevate?sslmode=require
|
||||
```
|
||||
|
||||
### Connection String Format
|
||||
|
||||
```
|
||||
postgresql://<user>:<password>@<host>:<port>/<database>[?<options>]
|
||||
```
|
||||
|
||||
Common options:
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| `sslmode=require` | Require TLS for the connection |
|
||||
| `sslmode=verify-full` | Require TLS and verify the server certificate |
|
||||
| `connect_timeout=10` | Connection timeout in seconds |
|
||||
| `application_name=docuelevate` | Identifies the connection in `pg_stat_activity` |
|
||||
|
||||
---
|
||||
|
||||
## MySQL / MariaDB
|
||||
|
||||
MySQL and MariaDB are supported via the `PyMySQL` driver.
|
||||
|
||||
```bash
|
||||
# Install the driver (it is not included by default)
|
||||
pip install pymysql cryptography
|
||||
|
||||
# Connection string
|
||||
DATABASE_URL=mysql+pymysql://user:password@host:3306/docuelevate?charset=utf8mb4
|
||||
```
|
||||
|
||||
**Requirements:**
|
||||
|
||||
- Use the `utf8mb4` charset for full Unicode support.
|
||||
- Set `innodb_large_prefix=ON` if using MySQL < 5.7.7.
|
||||
- PostgreSQL is preferred because SQLAlchemy's JSON column type has better support with it.
|
||||
|
||||
---
|
||||
|
||||
## Schema Migrations with Alembic
|
||||
|
||||
DocuElevate uses Alembic to manage all database schema changes.
|
||||
|
||||
### Apply Migrations
|
||||
|
||||
Run this command whenever you update DocuElevate or change the schema:
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
This applies all pending migrations in order. It is **idempotent** — safe to run multiple times.
|
||||
|
||||
### Check Current Version
|
||||
|
||||
```bash
|
||||
alembic current
|
||||
```
|
||||
|
||||
### View Migration History
|
||||
|
||||
```bash
|
||||
alembic history --verbose
|
||||
```
|
||||
|
||||
### Roll Back One Migration
|
||||
|
||||
```bash
|
||||
alembic downgrade -1
|
||||
```
|
||||
|
||||
### Roll Back to a Specific Revision
|
||||
|
||||
```bash
|
||||
alembic downgrade <revision_id>
|
||||
```
|
||||
|
||||
### Creating a New Migration (Developers)
|
||||
|
||||
After changing `app/models.py`:
|
||||
|
||||
```bash
|
||||
alembic revision --autogenerate -m "describe your change"
|
||||
```
|
||||
|
||||
Review the generated file in `migrations/versions/` before applying it.
|
||||
|
||||
### Automating Migrations in Docker Compose
|
||||
|
||||
Add a short-lived `migrate` service that runs before the API and Worker:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
migrate:
|
||||
image: ghcr.io/christianlouis/docuelevate:latest
|
||||
command: alembic upgrade head
|
||||
env_file: .env
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: "no"
|
||||
|
||||
api:
|
||||
depends_on:
|
||||
migrate:
|
||||
condition: service_completed_successfully
|
||||
```
|
||||
|
||||
### Automating Migrations in Kubernetes (Helm)
|
||||
|
||||
The Helm chart includes a pre-install and pre-upgrade Job hook that runs `alembic upgrade head` automatically before new pods start. No manual intervention is needed during `helm upgrade`.
|
||||
|
||||
---
|
||||
|
||||
## Connection Pooling
|
||||
|
||||
SQLAlchemy manages a connection pool automatically. The defaults are suitable for most deployments. For high-concurrency or Kubernetes deployments you may want to tune:
|
||||
|
||||
```bash
|
||||
# Optional — these are set via environment variables if you extend app/database.py
|
||||
# Typical production values:
|
||||
DB_POOL_SIZE=10 # Number of persistent connections per worker
|
||||
DB_MAX_OVERFLOW=20 # Additional connections allowed beyond pool_size
|
||||
DB_POOL_TIMEOUT=30 # Seconds to wait for a connection from the pool
|
||||
DB_POOL_RECYCLE=1800 # Recycle connections after 30 minutes (avoids stale connections)
|
||||
```
|
||||
|
||||
> **Note:** These environment variables are not exposed in the default `app/config.py`. If you need to tune them, extend the database engine creation in `app/database.py`.
|
||||
|
||||
For **PgBouncer** (external connection pooling), point `DATABASE_URL` at your PgBouncer instance and use transaction-mode pooling:
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql://docuelevate:password@pgbouncer:5432/docuelevate?prepared_statements=false
|
||||
```
|
||||
|
||||
Disable `prepared_statements` when using PgBouncer in transaction mode.
|
||||
|
||||
---
|
||||
|
||||
## Backup Procedures
|
||||
|
||||
### PostgreSQL
|
||||
|
||||
**Manual backup:**
|
||||
|
||||
```bash
|
||||
pg_dump -h localhost -U docuelevate -F c docuelevate > docuelevate_$(date +%Y%m%d_%H%M).dump
|
||||
```
|
||||
|
||||
**Restore:**
|
||||
|
||||
```bash
|
||||
pg_restore -h localhost -U docuelevate -d docuelevate docuelevate_20240101_1200.dump
|
||||
```
|
||||
|
||||
**Automated daily backup (cron example):**
|
||||
|
||||
```cron
|
||||
0 2 * * * pg_dump -h localhost -U docuelevate -F c docuelevate | gzip > /backups/docuelevate_$(date +\%Y\%m\%d).dump.gz
|
||||
```
|
||||
|
||||
Use your cloud provider's automated backup feature when available (e.g., RDS automated snapshots, Cloud SQL backups).
|
||||
|
||||
### SQLite
|
||||
|
||||
```bash
|
||||
# Stop the application first, or use SQLite's online backup API
|
||||
cp app/database.db /backups/docuelevate_$(date +%Y%m%d).db
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Performance Optimization
|
||||
|
||||
### PostgreSQL Index Recommendations
|
||||
|
||||
Run `ANALYZE` periodically to keep query planner statistics up to date:
|
||||
|
||||
```sql
|
||||
ANALYZE;
|
||||
```
|
||||
|
||||
For large deployments, consider adding indexes on frequently queried columns. Review the Alembic migrations in `migrations/versions/` for the current schema and add any additional indexes as a new Alembic migration.
|
||||
|
||||
### Vacuum and Autovacuum
|
||||
|
||||
PostgreSQL's `autovacuum` daemon runs automatically, but for write-heavy workloads you may want to tune it or run `VACUUM ANALYZE` manually after large batch imports.
|
||||
|
||||
### Query Monitoring
|
||||
|
||||
Enable `pg_stat_statements` to monitor slow queries:
|
||||
|
||||
```sql
|
||||
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
|
||||
|
||||
SELECT query, mean_exec_time, calls
|
||||
FROM pg_stat_statements
|
||||
ORDER BY mean_exec_time DESC
|
||||
LIMIT 20;
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### "Connection refused" when starting
|
||||
|
||||
- Verify the database host and port are reachable from the application container.
|
||||
- For Docker Compose, ensure the `postgres` service has started before the `api` service — use `depends_on` with `condition: service_healthy`.
|
||||
- Check PostgreSQL is listening: `pg_isready -h <host> -p 5432`
|
||||
|
||||
### "SSL connection required"
|
||||
|
||||
Add `?sslmode=require` to your `DATABASE_URL`, or set `sslmode=disable` if connecting to an internal-only database that does not use TLS.
|
||||
|
||||
### Alembic migration fails with "relation already exists"
|
||||
|
||||
The database is ahead of Alembic's tracked version. Stamp the current version without re-running migrations:
|
||||
|
||||
```bash
|
||||
alembic stamp head
|
||||
```
|
||||
|
||||
Then retry `alembic upgrade head`.
|
||||
|
||||
### "too many connections" error
|
||||
|
||||
Either increase `max_connections` in `postgresql.conf` or add PgBouncer in front of PostgreSQL. The default PostgreSQL `max_connections` is `100`; reduce `DB_POOL_SIZE` per worker to stay within this limit.
|
||||
|
||||
For more help, see the [Troubleshooting Guide](Troubleshooting.md).
|
||||
@@ -0,0 +1,563 @@
|
||||
# Kubernetes Deployment Guide
|
||||
|
||||
This guide covers deploying DocuElevate on Kubernetes using the provided Helm chart.
|
||||
|
||||
> **Quick reference:** For a side-by-side comparison of Docker Compose vs. Kubernetes deployment options, see the [Deployment Guide](DeploymentGuide.md).
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Architecture Overview](#architecture-overview)
|
||||
- [Quick Start](#quick-start)
|
||||
- [Helm Values Reference](#helm-values-reference)
|
||||
- [Storage Configuration](#storage-configuration)
|
||||
- [Database Setup](#database-setup)
|
||||
- [Secrets Management](#secrets-management)
|
||||
- [Ingress & TLS](#ingress--tls)
|
||||
- [Scaling & Autoscaling](#scaling--autoscaling)
|
||||
- [Monitoring & Health Checks](#monitoring--health-checks)
|
||||
- [Upgrades](#upgrades)
|
||||
- [Uninstalling](#uninstalling)
|
||||
- [Troubleshooting](#troubleshooting)
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
| Requirement | Minimum Version | Notes |
|
||||
|-------------|----------------|-------|
|
||||
| Kubernetes | 1.24 | 1.27+ recommended |
|
||||
| Helm | 3.10 | |
|
||||
| Storage Class (RWX) | — | Required for multi-replica; NFS, CephFS, Azure Files, EFS, etc. |
|
||||
| PostgreSQL | 14+ | Strongly recommended; SQLite not safe for multi-replica |
|
||||
| cert-manager | 1.12+ | Optional, for automated TLS via Let's Encrypt |
|
||||
|
||||
The Helm chart is located in the repository at `helm/docuelevate/`.
|
||||
|
||||
---
|
||||
|
||||
## Architecture Overview
|
||||
|
||||
```
|
||||
Internet
|
||||
│
|
||||
▼
|
||||
[Ingress Controller] ← TLS termination, host routing
|
||||
│
|
||||
▼
|
||||
[API Deployment] ← FastAPI web server (multiple replicas)
|
||||
│ │
|
||||
│ └── [Shared PVC: /workdir] ← ReadWriteMany volume
|
||||
│ │
|
||||
▼ ▼
|
||||
[Worker Deployment] ← Celery background task workers (multiple replicas)
|
||||
│
|
||||
├── [Redis Service] ← Celery broker & result backend
|
||||
├── [Gotenberg Service] ← Document → PDF conversion
|
||||
└── [Meilisearch Service] ← Full-text search index
|
||||
```
|
||||
|
||||
All services communicate over the cluster's internal network. **Redis and Meilisearch must not be exposed outside the cluster.**
|
||||
|
||||
---
|
||||
|
||||
## Quick Start
|
||||
|
||||
### 1. Add Chart Dependencies
|
||||
|
||||
```bash
|
||||
helm repo add bitnami https://charts.bitnami.com/bitnami
|
||||
helm repo update
|
||||
helm dependency update ./helm/docuelevate
|
||||
```
|
||||
|
||||
### 2. Create a Values Override File
|
||||
|
||||
Create `my-values.yaml` (never commit this file — it contains secrets):
|
||||
|
||||
```yaml
|
||||
env:
|
||||
EXTERNAL_HOSTNAME: docuelevate.example.com
|
||||
AZURE_ENDPOINT: "https://my-resource.cognitiveservices.azure.com/"
|
||||
AUTH_ENABLED: "true"
|
||||
|
||||
secrets:
|
||||
DATABASE_URL: "postgresql://docuelevate:strongpassword@postgres:5432/docuelevate"
|
||||
SESSION_SECRET: "<run: openssl rand -hex 32>"
|
||||
OPENAI_API_KEY: "sk-..."
|
||||
AZURE_AI_KEY: "..."
|
||||
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: docuelevate.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: docuelevate-tls
|
||||
hosts:
|
||||
- docuelevate.example.com
|
||||
```
|
||||
|
||||
### 3. Install the Chart
|
||||
|
||||
```bash
|
||||
helm install docuelevate ./helm/docuelevate \
|
||||
--namespace docuelevate \
|
||||
--create-namespace \
|
||||
-f my-values.yaml
|
||||
```
|
||||
|
||||
### 4. Verify the Deployment
|
||||
|
||||
```bash
|
||||
kubectl get pods -n docuelevate
|
||||
kubectl get svc -n docuelevate
|
||||
kubectl get ingress -n docuelevate
|
||||
```
|
||||
|
||||
Wait until all pods report `Running` and `1/1` (or `2/2` for multi-container pods).
|
||||
|
||||
---
|
||||
|
||||
## Helm Values Reference
|
||||
|
||||
The complete list of values is in [`helm/docuelevate/values.yaml`](../helm/docuelevate/values.yaml). Key sections are summarized below.
|
||||
|
||||
### Container Image
|
||||
|
||||
```yaml
|
||||
image:
|
||||
repository: ghcr.io/christianlouis/docuelevate
|
||||
tag: "" # Defaults to chart appVersion; pin a specific tag in production
|
||||
pullPolicy: IfNotPresent
|
||||
```
|
||||
|
||||
### Non-Secret Configuration (`env`)
|
||||
|
||||
```yaml
|
||||
env:
|
||||
WORKDIR: /workdir
|
||||
AI_PROVIDER: openai
|
||||
OPENAI_MODEL: gpt-4o-mini
|
||||
AZURE_REGION: eastus
|
||||
AZURE_ENDPOINT: "https://my-resource.cognitiveservices.azure.com/"
|
||||
MEILISEARCH_URL: http://docuelevate-meilisearch:7700
|
||||
ENABLE_SEARCH: "true"
|
||||
AUTH_ENABLED: "true"
|
||||
EXTERNAL_HOSTNAME: docuelevate.example.com
|
||||
ALLOW_FILE_DELETE: "true"
|
||||
```
|
||||
|
||||
### Secrets (`secrets`)
|
||||
|
||||
Secrets are stored in a Kubernetes `Secret` resource and injected as environment variables.
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
DATABASE_URL: "postgresql://user:pass@postgres:5432/docuelevate"
|
||||
SESSION_SECRET: "<min-32-char-random-string>"
|
||||
OPENAI_API_KEY: "sk-..."
|
||||
AZURE_AI_KEY: "..."
|
||||
MEILISEARCH_API_KEY: "" # Leave blank for unauthenticated local Meilisearch
|
||||
DROPBOX_APP_KEY: "" # Optional — only if using Dropbox
|
||||
DROPBOX_APP_SECRET: ""
|
||||
GOOGLE_DRIVE_CLIENT_ID: "" # Optional — only if using Google Drive
|
||||
GOOGLE_DRIVE_CLIENT_SECRET: ""
|
||||
```
|
||||
|
||||
> **Tip:** In production, manage secrets with an external secret manager. See [Secrets Management](#secrets-management).
|
||||
|
||||
---
|
||||
|
||||
## Storage Configuration
|
||||
|
||||
### Shared Workdir PVC
|
||||
|
||||
Both the API and Worker pods need to access the same `/workdir` volume for document staging.
|
||||
|
||||
```yaml
|
||||
workdir:
|
||||
persistence:
|
||||
enabled: true
|
||||
accessMode: ReadWriteMany # Required when api.replicaCount > 1 or worker.replicaCount > 1
|
||||
size: 50Gi
|
||||
storageClass: "nfs-client" # Must support ReadWriteMany
|
||||
```
|
||||
|
||||
**Single-replica clusters** can use `ReadWriteOnce`:
|
||||
|
||||
```yaml
|
||||
workdir:
|
||||
persistence:
|
||||
accessMode: ReadWriteOnce
|
||||
size: 20Gi
|
||||
storageClass: "" # Use cluster default
|
||||
```
|
||||
|
||||
### Meilisearch Data
|
||||
|
||||
Meilisearch data is stored in a separate PVC:
|
||||
|
||||
```yaml
|
||||
meilisearch:
|
||||
enabled: true
|
||||
persistence:
|
||||
enabled: true
|
||||
size: 10Gi
|
||||
storageClass: "" # Use cluster default (RWO is fine here)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Database Setup
|
||||
|
||||
For production, deploy PostgreSQL externally (managed service or a separate Helm release) and set `DATABASE_URL` in `secrets`.
|
||||
|
||||
### External PostgreSQL
|
||||
|
||||
```yaml
|
||||
secrets:
|
||||
DATABASE_URL: "postgresql://docuelevate:password@my-postgres-host:5432/docuelevate?sslmode=require"
|
||||
```
|
||||
|
||||
### Bundled PostgreSQL (Not Recommended for Production)
|
||||
|
||||
If you must use a bundled PostgreSQL instance, add it as a Helm dependency or deploy the Bitnami PostgreSQL chart in the same namespace. The Helm chart does not bundle PostgreSQL by default.
|
||||
|
||||
### Database Migrations
|
||||
|
||||
A Kubernetes Job is included in the Helm chart as a pre-install and pre-upgrade hook. It runs `alembic upgrade head` before any pods are updated:
|
||||
|
||||
```yaml
|
||||
# This is automatic — no additional configuration needed
|
||||
```
|
||||
|
||||
To run migrations manually:
|
||||
|
||||
```bash
|
||||
kubectl run alembic-upgrade \
|
||||
--image=ghcr.io/christianlouis/docuelevate:latest \
|
||||
--namespace=docuelevate \
|
||||
--restart=Never \
|
||||
--env-from=secret/docuelevate-secrets \
|
||||
-- alembic upgrade head
|
||||
```
|
||||
|
||||
See the [Database Configuration Guide](DatabaseConfiguration.md) for detailed database setup.
|
||||
|
||||
---
|
||||
|
||||
## Secrets Management
|
||||
|
||||
### Option 1: Values File (Basic)
|
||||
|
||||
Store secrets in `my-values.yaml` and **never commit it to source control**. Pass it with `-f my-values.yaml` at install/upgrade time.
|
||||
|
||||
### Option 2: External Secrets Operator (Recommended)
|
||||
|
||||
Use [External Secrets Operator](https://external-secrets.io/) with HashiCorp Vault, AWS Secrets Manager, or Azure Key Vault:
|
||||
|
||||
```yaml
|
||||
# ExternalSecret resource
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: docuelevate-secrets
|
||||
namespace: docuelevate
|
||||
spec:
|
||||
refreshInterval: 1h
|
||||
secretStoreRef:
|
||||
name: vault-backend
|
||||
kind: ClusterSecretStore
|
||||
target:
|
||||
name: docuelevate-secrets
|
||||
creationPolicy: Owner
|
||||
data:
|
||||
- secretKey: DATABASE_URL
|
||||
remoteRef:
|
||||
key: docuelevate/production
|
||||
property: database_url
|
||||
- secretKey: SESSION_SECRET
|
||||
remoteRef:
|
||||
key: docuelevate/production
|
||||
property: session_secret
|
||||
- secretKey: OPENAI_API_KEY
|
||||
remoteRef:
|
||||
key: docuelevate/production
|
||||
property: openai_api_key
|
||||
```
|
||||
|
||||
Then reference the pre-existing secret in Helm values:
|
||||
|
||||
```yaml
|
||||
existingSecret: docuelevate-secrets # Use this key if the chart supports it
|
||||
```
|
||||
|
||||
### Option 3: Sealed Secrets
|
||||
|
||||
Use [Bitnami Sealed Secrets](https://github.com/bitnami-labs/sealed-secrets) to encrypt secrets before committing to Git.
|
||||
|
||||
---
|
||||
|
||||
## Ingress & TLS
|
||||
|
||||
### Nginx Ingress
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "1g"
|
||||
nginx.ingress.kubernetes.io/proxy-read-timeout: "300"
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: docuelevate.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: docuelevate-tls
|
||||
hosts:
|
||||
- docuelevate.example.com
|
||||
```
|
||||
|
||||
### Traefik Ingress
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: traefik
|
||||
annotations:
|
||||
traefik.ingress.kubernetes.io/router.entrypoints: websecure
|
||||
traefik.ingress.kubernetes.io/router.tls: "true"
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
hosts:
|
||||
- host: docuelevate.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: docuelevate-tls
|
||||
hosts:
|
||||
- docuelevate.example.com
|
||||
```
|
||||
|
||||
### Manual TLS Secret
|
||||
|
||||
If you manage TLS certificates outside cert-manager:
|
||||
|
||||
```bash
|
||||
kubectl create secret tls docuelevate-tls \
|
||||
--cert=path/to/fullchain.pem \
|
||||
--key=path/to/privkey.pem \
|
||||
--namespace=docuelevate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Scaling & Autoscaling
|
||||
|
||||
### Manual Scaling
|
||||
|
||||
```yaml
|
||||
api:
|
||||
replicaCount: 3
|
||||
|
||||
worker:
|
||||
replicaCount: 4
|
||||
```
|
||||
|
||||
### Horizontal Pod Autoscaler
|
||||
|
||||
```yaml
|
||||
api:
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 8
|
||||
targetCPUUtilizationPercentage: 70
|
||||
|
||||
worker:
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 75
|
||||
```
|
||||
|
||||
> **Prerequisite:** The Kubernetes Metrics Server must be installed in your cluster for HPA to function.
|
||||
|
||||
### Resource Requests and Limits
|
||||
|
||||
```yaml
|
||||
api:
|
||||
resources:
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "512Mi"
|
||||
limits:
|
||||
cpu: "1000m"
|
||||
memory: "2Gi"
|
||||
|
||||
worker:
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2000m"
|
||||
memory: "4Gi"
|
||||
```
|
||||
|
||||
### External Redis
|
||||
|
||||
Disable the bundled Redis and point at an external instance for greater resilience:
|
||||
|
||||
```yaml
|
||||
redis:
|
||||
enabled: false
|
||||
|
||||
externalRedis:
|
||||
url: "redis://my-redis-cluster:6379/0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Monitoring & Health Checks
|
||||
|
||||
### Kubernetes Probes
|
||||
|
||||
The Helm chart configures liveness and readiness probes on the API pods via `/api/health`. Default settings:
|
||||
|
||||
```yaml
|
||||
api:
|
||||
livenessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 30
|
||||
periodSeconds: 30
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /api/health
|
||||
port: 8000
|
||||
initialDelaySeconds: 10
|
||||
periodSeconds: 10
|
||||
```
|
||||
|
||||
### Prometheus Scraping
|
||||
|
||||
Add annotations to expose metrics (if using a Prometheus-compatible exporter):
|
||||
|
||||
```yaml
|
||||
api:
|
||||
podAnnotations:
|
||||
prometheus.io/scrape: "true"
|
||||
prometheus.io/path: "/metrics"
|
||||
prometheus.io/port: "8000"
|
||||
```
|
||||
|
||||
### Pod Disruption Budget
|
||||
|
||||
Ensure availability during node maintenance:
|
||||
|
||||
```bash
|
||||
kubectl apply -f - <<EOF
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: docuelevate-api-pdb
|
||||
namespace: docuelevate
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/component: api
|
||||
app.kubernetes.io/instance: docuelevate
|
||||
EOF
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Upgrades
|
||||
|
||||
```bash
|
||||
helm upgrade docuelevate ./helm/docuelevate \
|
||||
--namespace docuelevate \
|
||||
-f my-values.yaml
|
||||
```
|
||||
|
||||
The pre-upgrade hook automatically runs `alembic upgrade head` before new pods are created. Upgrades are rolling by default — old pods continue to serve traffic until new pods are ready.
|
||||
|
||||
**Image tag pinning (recommended):**
|
||||
|
||||
```yaml
|
||||
image:
|
||||
tag: "1.5.2" # Pin a specific version tag instead of using 'latest'
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Uninstalling
|
||||
|
||||
```bash
|
||||
helm uninstall docuelevate --namespace docuelevate
|
||||
```
|
||||
|
||||
> **Warning:** Persistent Volume Claims are **NOT** deleted automatically. Remove them manually if you no longer need the data:
|
||||
|
||||
```bash
|
||||
kubectl delete pvc -l app.kubernetes.io/instance=docuelevate -n docuelevate
|
||||
```
|
||||
|
||||
To delete the namespace entirely:
|
||||
|
||||
```bash
|
||||
kubectl delete namespace docuelevate
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Pods Stuck in `Pending`
|
||||
|
||||
```bash
|
||||
kubectl describe pod <pod-name> -n docuelevate
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- No available nodes with sufficient CPU/memory — adjust resource requests or add nodes.
|
||||
- PVC cannot be bound — verify the StorageClass supports the required `accessMode`.
|
||||
- Image pull failure — check `imagePullSecrets` and network access to the container registry.
|
||||
|
||||
### Pods in `CrashLoopBackOff`
|
||||
|
||||
```bash
|
||||
kubectl logs <pod-name> -n docuelevate --previous
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- `DATABASE_URL` is wrong or the database is unreachable.
|
||||
- `SESSION_SECRET` is missing or too short.
|
||||
- Required environment variable not set in `secrets` or `env`.
|
||||
|
||||
### Migration Job Fails
|
||||
|
||||
```bash
|
||||
kubectl logs job/docuelevate-migrate -n docuelevate
|
||||
```
|
||||
|
||||
Resolve the database connection issue then re-run the job or run the upgrade again.
|
||||
|
||||
### Workdir Volume Mount Errors
|
||||
|
||||
Ensure the StorageClass supports `ReadWriteMany` when `api.replicaCount > 1` or `worker.replicaCount > 1`. Check your storage provisioner documentation.
|
||||
|
||||
For more help, see the [Troubleshooting Guide](Troubleshooting.md).
|
||||
@@ -0,0 +1,148 @@
|
||||
# Licensing & Compliance
|
||||
|
||||
DocuElevate is released under the **Apache License 2.0**. This document explains the project's own license, the obligations associated with third-party dependencies, and how compliance is maintained.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [DocuElevate License](#docuelevate-license)
|
||||
- [LGPL Dependencies](#lgpl-dependencies)
|
||||
- [Third-Party Dependency Summary](#third-party-dependency-summary)
|
||||
- [Compliance Checklist](#compliance-checklist)
|
||||
- [Maintaining Compliance](#maintaining-compliance)
|
||||
|
||||
---
|
||||
|
||||
## DocuElevate License
|
||||
|
||||
DocuElevate is copyright © 2025 Christian Krakau-Louis and is distributed under the **Apache License, Version 2.0**.
|
||||
|
||||
```
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
```
|
||||
|
||||
The full license text is in [`LICENSE`](../LICENSE) at the root of the repository.
|
||||
|
||||
---
|
||||
|
||||
## LGPL Dependencies
|
||||
|
||||
One dependency requires special handling under its license:
|
||||
|
||||
### Paramiko (LGPL 2.1)
|
||||
|
||||
[Paramiko](https://github.com/paramiko/paramiko) is a Python implementation of the SSH protocol and is licensed under the **GNU Lesser General Public License (LGPL) v2.1**.
|
||||
|
||||
**Compliance obligations:**
|
||||
|
||||
1. **Source availability** — The source code for Paramiko is publicly available at `https://github.com/paramiko/paramiko`. Users of DocuElevate have the right to obtain, modify, and redistribute Paramiko under the terms of the LGPL.
|
||||
|
||||
2. **License text** — A copy of the LGPL v2.1 is bundled with DocuElevate at `frontend/static/licenses/lgpl.txt` and is accessible at runtime via `/static/licenses/lgpl.txt`.
|
||||
|
||||
3. **Attribution** — DocuElevate's attribution page (`/attribution`) prominently credits Paramiko and links to its source repository and the LGPL license text. The `NOTICE` file at the root of the repository also includes a formal LGPL attribution notice.
|
||||
|
||||
4. **No modification** — DocuElevate does not modify Paramiko's source code. It is used as an unmodified library dependency installed via pip. This means the "dynamic linking" exception applies, and DocuElevate's own Apache 2.0 license is not affected.
|
||||
|
||||
**In-app compliance endpoints:**
|
||||
|
||||
| URL | Description |
|
||||
|-----|-------------|
|
||||
| `/attribution` | Third-party attribution page listing all major dependencies |
|
||||
| `/static/licenses/lgpl.txt` | Full LGPL v2.1 license text |
|
||||
| `/licenses/lgpl.txt` | Alias served via the API |
|
||||
|
||||
---
|
||||
|
||||
## Third-Party Dependency Summary
|
||||
|
||||
The table below summarizes the licenses of DocuElevate's key runtime dependencies. See `/attribution` in the application or the `NOTICE` file for the complete list.
|
||||
|
||||
| Package | License | Repository |
|
||||
|---------|---------|------------|
|
||||
| FastAPI | MIT | https://github.com/tiangolo/fastapi |
|
||||
| Celery | BSD | https://github.com/celery/celery |
|
||||
| Uvicorn | BSD | https://github.com/encode/uvicorn |
|
||||
| SQLAlchemy | MIT | https://github.com/sqlalchemy/sqlalchemy |
|
||||
| Pydantic | MIT | https://github.com/pydantic/pydantic |
|
||||
| Alembic | MIT | https://github.com/sqlalchemy/alembic |
|
||||
| OpenAI Python | MIT | https://github.com/openai/openai-python |
|
||||
| pypdf | BSD | https://github.com/py-pdf/pypdf |
|
||||
| Requests | Apache 2.0 | https://github.com/psf/requests |
|
||||
| **Paramiko** | **LGPL 2.1** | https://github.com/paramiko/paramiko |
|
||||
| Dropbox SDK | MIT | https://github.com/dropbox/dropbox-sdk-python |
|
||||
| google-auth | Apache 2.0 | https://github.com/googleapis/google-auth-library-python |
|
||||
| msal | MIT | https://github.com/AzureAD/microsoft-authentication-library-for-python |
|
||||
| boto3 (AWS) | Apache 2.0 | https://github.com/boto/boto3 |
|
||||
| Jinja2 | BSD | https://github.com/pallets/jinja |
|
||||
| Tailwind CSS | MIT | https://github.com/tailwindlabs/tailwindcss |
|
||||
| Redis (redis-py) | MIT | https://github.com/redis/redis-py |
|
||||
|
||||
> The canonical list is maintained in `requirements.txt` (runtime dependencies) and `requirements-dev.txt` (development tools).
|
||||
|
||||
---
|
||||
|
||||
## Compliance Checklist
|
||||
|
||||
Use this checklist when preparing a release or auditing the project:
|
||||
|
||||
- [ ] `LICENSE` file is present and contains the Apache 2.0 text.
|
||||
- [ ] `NOTICE` file is present and includes the LGPL attribution notice for Paramiko.
|
||||
- [ ] `frontend/static/licenses/lgpl.txt` contains the full LGPL v2.1 text.
|
||||
- [ ] The `/attribution` page is accessible and lists Paramiko with a link to its source repository and the LGPL license.
|
||||
- [ ] No new LGPL, GPL, or proprietary dependencies have been introduced without review.
|
||||
- [ ] `safety check` passes with no known CVEs in runtime dependencies.
|
||||
- [ ] `pip-licenses` output reviewed for any unexpected license changes after dependency updates.
|
||||
|
||||
### Checking Dependency Licenses
|
||||
|
||||
Install `pip-licenses` and generate a report:
|
||||
|
||||
```bash
|
||||
pip install pip-licenses
|
||||
pip-licenses --format=markdown --order=license
|
||||
```
|
||||
|
||||
Flag any license that is:
|
||||
- **GPL (not LGPL)** — Copyleft; may require open-sourcing DocuElevate itself if statically linked.
|
||||
- **AGPL** — Network copyleft; distribution over a network triggers copyleft obligations.
|
||||
- **Proprietary / commercial** — Requires a separate commercial agreement.
|
||||
|
||||
---
|
||||
|
||||
## Maintaining Compliance
|
||||
|
||||
### When Adding a New Dependency
|
||||
|
||||
1. Identify the license from `pip-licenses` or the package's PyPI page / README.
|
||||
2. If the license is LGPL, GPL, AGPL, or proprietary, raise a discussion before merging.
|
||||
3. If LGPL is approved:
|
||||
- Add an entry to `frontend/templates/attribution.html`.
|
||||
- Add an entry to the `NOTICE` file.
|
||||
- Bundle the license text in `frontend/static/licenses/` if not already present.
|
||||
4. For all new dependencies, verify with `safety check` that the package has no known CVEs.
|
||||
|
||||
### Automated CVE Scanning
|
||||
|
||||
The CI pipeline runs `safety check` on every pull request. Any newly introduced CVE will block the PR from merging.
|
||||
|
||||
```bash
|
||||
# Run locally before submitting a PR
|
||||
safety check
|
||||
```
|
||||
|
||||
### Attribution Page
|
||||
|
||||
The application's built-in attribution page (`/attribution`) is defined in:
|
||||
- Template: `frontend/templates/attribution.html`
|
||||
- Route: `app/views/license_routes.py`
|
||||
|
||||
Keep this page up to date whenever dependencies change.
|
||||
@@ -0,0 +1,505 @@
|
||||
# Production Readiness Guide
|
||||
|
||||
This guide bridges the gap between a working local installation and a hardened, production-ready DocuElevate deployment. Work through the checklist below — every item should be addressed before exposing DocuElevate to real users or sensitive documents.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [Quick Checklist](#quick-checklist)
|
||||
- [1. Database](#1-database)
|
||||
- [2. Persistent Storage](#2-persistent-storage)
|
||||
- [3. Security Hardening](#3-security-hardening)
|
||||
- [4. TLS / HTTPS](#4-tls--https)
|
||||
- [5. Authentication](#5-authentication)
|
||||
- [6. Scaling Workers](#6-scaling-workers)
|
||||
- [7. Monitoring & Alerting](#7-monitoring--alerting)
|
||||
- [8. Backup Strategy](#8-backup-strategy)
|
||||
- [9. Updates & Maintenance](#9-updates--maintenance)
|
||||
- [10. Helm / Kubernetes Specifics](#10-helm--kubernetes-specifics)
|
||||
|
||||
---
|
||||
|
||||
## Quick Checklist
|
||||
|
||||
Use this checklist to track readiness before going live.
|
||||
|
||||
- [ ] **Database** — PostgreSQL configured; SQLite is not used in production
|
||||
- [ ] **Migrations** — `alembic upgrade head` runs cleanly on every deploy
|
||||
- [ ] **Persistent storage** — workdir volume is mounted on durable, backed-up storage
|
||||
- [ ] **TLS** — All traffic served over HTTPS; HTTP redirects to HTTPS
|
||||
- [ ] **Session secret** — `SESSION_SECRET` is a random 32-byte (64-hex-char) value, not a placeholder
|
||||
- [ ] **Admin password** — Strong password set; default placeholder removed
|
||||
- [ ] **Auth enabled** — `AUTH_ENABLED=true`
|
||||
- [ ] **Security headers** — Configured at the reverse proxy or via `SECURITY_HEADERS_ENABLED=true`
|
||||
- [ ] **Rate limiting** — `RATE_LIMITING_ENABLED=true` (default)
|
||||
- [ ] **Redis** — Running and accessible only from internal network
|
||||
- [ ] **Meilisearch** — Running and accessible only from internal network
|
||||
- [ ] **Worker replicas** — At least 2 workers configured for redundancy
|
||||
- [ ] **Monitoring** — `/api/health` polled by uptime checker
|
||||
- [ ] **Backups** — Automated backup of database, workdir, and Meilisearch data
|
||||
- [ ] **Log retention** — Logs shipped to a persistent store or aggregator
|
||||
- [ ] **Secrets management** — API keys not committed to source control
|
||||
|
||||
---
|
||||
|
||||
## 1. Database
|
||||
|
||||
### SQLite → PostgreSQL Migration
|
||||
|
||||
SQLite is the default database and is suitable only for **development or single-node, low-traffic setups**. For any multi-replica deployment or meaningful production load, use PostgreSQL.
|
||||
|
||||
```bash
|
||||
DATABASE_URL=postgresql://docuelevate:strongpassword@postgres-host:5432/docuelevate
|
||||
```
|
||||
|
||||
See the [Database Configuration Guide](DatabaseConfiguration.md) for detailed setup instructions, migration steps, and optimization tips.
|
||||
|
||||
### Alembic Migrations
|
||||
|
||||
Always run database migrations on every deploy **before** new application code starts serving traffic:
|
||||
|
||||
```bash
|
||||
alembic upgrade head
|
||||
```
|
||||
|
||||
- **Docker Compose**: Add a one-shot `migrate` service that runs before `api` and `worker`:
|
||||
|
||||
```yaml
|
||||
migrate:
|
||||
image: ghcr.io/christianlouis/docuelevate:latest
|
||||
command: alembic upgrade head
|
||||
env_file: .env
|
||||
depends_on:
|
||||
- redis
|
||||
```
|
||||
|
||||
- **Helm / Kubernetes**: The Helm chart includes a pre-install/pre-upgrade Job hook that runs `alembic upgrade head` automatically before pods are updated.
|
||||
|
||||
---
|
||||
|
||||
## 2. Persistent Storage
|
||||
|
||||
The `WORKDIR` directory (`/workdir` by default) is where documents are staged during processing. **This must be backed by persistent, durable storage.**
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Map a named volume or a host path:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
volumes:
|
||||
- docuelevate_workdir:/workdir
|
||||
worker:
|
||||
volumes:
|
||||
- docuelevate_workdir:/workdir # Same volume — both services share it
|
||||
|
||||
volumes:
|
||||
docuelevate_workdir:
|
||||
driver: local
|
||||
```
|
||||
|
||||
For production, replace `driver: local` with an NFS or other network-backed volume driver so data survives host failures.
|
||||
|
||||
### Kubernetes
|
||||
|
||||
Use a `ReadWriteMany` (RWX) PersistentVolumeClaim when running multiple replicas:
|
||||
|
||||
```yaml
|
||||
workdir:
|
||||
persistence:
|
||||
enabled: true
|
||||
accessMode: ReadWriteMany # Required for multi-replica
|
||||
size: 50Gi
|
||||
storageClass: "nfs-client" # Or your cluster's RWX storage class
|
||||
```
|
||||
|
||||
Single-replica clusters can use `ReadWriteOnce`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Security Hardening
|
||||
|
||||
### Session Secret
|
||||
|
||||
Generate a strong, unique secret and set it as `SESSION_SECRET`:
|
||||
|
||||
```bash
|
||||
python -c "import secrets; print(secrets.token_hex(32))"
|
||||
# or
|
||||
openssl rand -hex 32
|
||||
```
|
||||
|
||||
This value **must be at least 32 characters** and must not be a known placeholder. Rotating it invalidates all active sessions.
|
||||
|
||||
### HTTP Security Headers
|
||||
|
||||
DocuElevate's built-in security headers are **disabled by default** because most production deployments sit behind a reverse proxy that already adds them.
|
||||
|
||||
**Option A** — Let your reverse proxy add headers (recommended):
|
||||
|
||||
- See the Nginx and Traefik examples in the [Deployment Guide](DeploymentGuide.md#security-headers).
|
||||
|
||||
**Option B** — Enable built-in headers (only if no reverse proxy):
|
||||
|
||||
```bash
|
||||
SECURITY_HEADERS_ENABLED=true
|
||||
```
|
||||
|
||||
Recommended headers to configure at the proxy level:
|
||||
|
||||
| Header | Recommended Value |
|
||||
|--------|-------------------|
|
||||
| `Strict-Transport-Security` | `max-age=31536000; includeSubDomains` |
|
||||
| `X-Frame-Options` | `DENY` |
|
||||
| `X-Content-Type-Options` | `nosniff` |
|
||||
| `Referrer-Policy` | `strict-origin-when-cross-origin` |
|
||||
| `Content-Security-Policy` | See CSP notes below |
|
||||
|
||||
#### Content-Security-Policy Notes
|
||||
|
||||
DocuElevate's frontend uses Tailwind CSS loaded from CDN in development mode. In production, ensure your CSP allows loading scripts and styles from your configured static file origin. A starting point:
|
||||
|
||||
```
|
||||
Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;
|
||||
```
|
||||
|
||||
Audit and tighten this policy for your specific deployment.
|
||||
|
||||
### Rate Limiting
|
||||
|
||||
Rate limiting is enabled by default and requires Redis. Verify it is active:
|
||||
|
||||
```bash
|
||||
RATE_LIMITING_ENABLED=true # default — ensure not overridden to false
|
||||
REDIS_URL=redis://redis:6379/0
|
||||
```
|
||||
|
||||
See the [Configuration Guide — Rate Limiting](ConfigurationGuide.md#rate-limiting) for per-endpoint tuning.
|
||||
|
||||
### Secrets Management
|
||||
|
||||
- **Never commit `.env` files** containing real secrets to source control.
|
||||
- For Kubernetes, use an external secret manager (HashiCorp Vault, External Secrets Operator, Sealed Secrets) and reference secrets by name in Helm values rather than embedding them.
|
||||
- Rotate API keys, the session secret, and database credentials on a regular schedule. See the [Credential Rotation Guide](CredentialRotationGuide.md).
|
||||
|
||||
### File Upload Limits
|
||||
|
||||
Set appropriate upload size limits to prevent resource exhaustion:
|
||||
|
||||
```bash
|
||||
MAX_UPLOAD_SIZE=104857600 # 100 MB — adjust for your use case
|
||||
MAX_REQUEST_BODY_SIZE=1048576 # 1 MB for non-file requests (default)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. TLS / HTTPS
|
||||
|
||||
**All production traffic must be served over HTTPS.**
|
||||
|
||||
### Docker Compose with Traefik
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
labels:
|
||||
- "traefik.enable=true"
|
||||
- "traefik.http.routers.docuelevate.rule=Host(`docuelevate.example.com`)"
|
||||
- "traefik.http.routers.docuelevate.entrypoints=websecure"
|
||||
- "traefik.http.routers.docuelevate.tls.certresolver=letsencrypt"
|
||||
```
|
||||
|
||||
### Nginx Reverse Proxy
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name docuelevate.example.com;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name docuelevate.example.com;
|
||||
|
||||
ssl_certificate /etc/nginx/ssl/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/ssl/privkey.pem;
|
||||
ssl_protocols TLSv1.2 TLSv1.3;
|
||||
ssl_ciphers HIGH:!aNULL:!MD5;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:8000;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
client_max_body_size 1g;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Kubernetes (Helm)
|
||||
|
||||
```yaml
|
||||
ingress:
|
||||
enabled: true
|
||||
className: nginx
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
nginx.ingress.kubernetes.io/proxy-body-size: "1g"
|
||||
hosts:
|
||||
- host: docuelevate.example.com
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
tls:
|
||||
- secretName: docuelevate-tls
|
||||
hosts:
|
||||
- docuelevate.example.com
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Authentication
|
||||
|
||||
Enable authentication and choose an auth method appropriate for your organization.
|
||||
|
||||
```bash
|
||||
AUTH_ENABLED=true
|
||||
ADMIN_USERNAME=admin
|
||||
ADMIN_PASSWORD=<strong-password>
|
||||
SESSION_SECRET=<min-32-char-random-string>
|
||||
```
|
||||
|
||||
For SSO/OIDC (Authentik, Keycloak, Auth0, etc.) see the [Authentication Setup Guide](AuthenticationSetup.md).
|
||||
|
||||
**Best practices:**
|
||||
|
||||
- Use OIDC/SSO for team deployments to centralize access control.
|
||||
- Enforce strong password policies or delegate password management to your identity provider.
|
||||
- Set an appropriate session timeout (handled by the identity provider for OIDC, or by session middleware for basic auth).
|
||||
|
||||
---
|
||||
|
||||
## 6. Scaling Workers
|
||||
|
||||
### Docker Compose
|
||||
|
||||
Use the `deploy.replicas` setting (requires Docker Swarm mode) or simply run multiple workers:
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
deploy:
|
||||
replicas: 3
|
||||
```
|
||||
|
||||
Or scale after deployment:
|
||||
|
||||
```bash
|
||||
docker-compose up -d --scale worker=3
|
||||
```
|
||||
|
||||
Each worker processes tasks from the Celery queue independently. Ensure the shared `workdir` volume is accessible from all worker containers.
|
||||
|
||||
### Kubernetes (Helm)
|
||||
|
||||
```yaml
|
||||
worker:
|
||||
replicaCount: 3
|
||||
autoscaling:
|
||||
enabled: true
|
||||
minReplicas: 2
|
||||
maxReplicas: 10
|
||||
targetCPUUtilizationPercentage: 75
|
||||
```
|
||||
|
||||
### Worker Queue Tuning
|
||||
|
||||
Celery workers process three queues with different priorities:
|
||||
|
||||
| Queue | Purpose |
|
||||
|-------|---------|
|
||||
| `document_processor` | Main document processing tasks (OCR, conversion) |
|
||||
| `default` | Metadata extraction, storage uploads |
|
||||
| `celery` | Built-in Celery management tasks |
|
||||
|
||||
To dedicate workers to specific queues in high-volume deployments:
|
||||
|
||||
```bash
|
||||
# High-priority worker — document processing only
|
||||
celery -A app.celery_worker worker -Q document_processor --concurrency=4
|
||||
|
||||
# General worker — everything else
|
||||
celery -A app.celery_worker worker -Q default,celery --concurrency=2
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Monitoring & Alerting
|
||||
|
||||
### Health Check Endpoint
|
||||
|
||||
DocuElevate exposes `/api/health` for readiness probing. Configure your uptime monitor to poll this endpoint:
|
||||
|
||||
```bash
|
||||
curl http://docuelevate.example.com/api/health
|
||||
# Expected: {"status": "ok", ...}
|
||||
```
|
||||
|
||||
Set `UPTIME_KUMA_URL` to your Uptime Kuma push URL for heartbeat monitoring:
|
||||
|
||||
```bash
|
||||
UPTIME_KUMA_URL=https://uptime.example.com/api/push/abc123
|
||||
```
|
||||
|
||||
### Prometheus / Grafana
|
||||
|
||||
Scrape the `/api/health` endpoint or add a custom Prometheus exporter. Useful metrics to track:
|
||||
|
||||
- Number of documents processed per hour
|
||||
- Queue length (via Redis `LLEN` on Celery queues)
|
||||
- Worker concurrency and CPU utilization
|
||||
- API request latency (p50, p95, p99)
|
||||
|
||||
### Log Aggregation
|
||||
|
||||
- **Docker Compose**: Use the `logging` driver to ship to Loki, Fluentd, or CloudWatch:
|
||||
|
||||
```yaml
|
||||
services:
|
||||
api:
|
||||
logging:
|
||||
driver: "json-file"
|
||||
options:
|
||||
max-size: "50m"
|
||||
max-file: "5"
|
||||
```
|
||||
|
||||
- **Kubernetes**: Logs are written to stdout/stderr and can be captured by your cluster's log aggregator (Fluentd, Vector, Promtail).
|
||||
|
||||
---
|
||||
|
||||
## 8. Backup Strategy
|
||||
|
||||
Back up all three data stores regularly:
|
||||
|
||||
### Database
|
||||
|
||||
**PostgreSQL:**
|
||||
|
||||
```bash
|
||||
pg_dump -h postgres-host -U docuelevate docuelevate > backup_$(date +%Y%m%d).sql
|
||||
```
|
||||
|
||||
Automate with a cron job or your cloud provider's managed backup feature.
|
||||
|
||||
**SQLite** (development only):
|
||||
|
||||
```bash
|
||||
cp app/database.db backup_$(date +%Y%m%d).db
|
||||
```
|
||||
|
||||
### Workdir Volume
|
||||
|
||||
The `WORKDIR` directory contains original uploads and processed documents. Use your volume provider's snapshot feature or rsync to a secondary location:
|
||||
|
||||
```bash
|
||||
rsync -av /workdir/ /backup/workdir/
|
||||
```
|
||||
|
||||
### Meilisearch Data
|
||||
|
||||
Meilisearch stores its index in the directory specified by `MEILI_DB_PATH` (default `/meili_data`). Snapshot it regularly or use [Meilisearch's dump feature](https://www.meilisearch.com/docs/reference/api/dumps):
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:7700/dumps \
|
||||
-H "Authorization: Bearer $MEILISEARCH_API_KEY"
|
||||
```
|
||||
|
||||
### Configuration / Secrets
|
||||
|
||||
Back up your `.env` file or Helm values file to a **secure, encrypted** store (e.g., a password manager or secrets vault). Do not commit it to source control.
|
||||
|
||||
---
|
||||
|
||||
## 9. Updates & Maintenance
|
||||
|
||||
### Docker Compose
|
||||
|
||||
```bash
|
||||
git pull
|
||||
docker-compose pull
|
||||
docker-compose down && docker-compose up -d
|
||||
```
|
||||
|
||||
The `alembic upgrade head` command is run automatically if you include the `migrate` service (see [Database](#1-database)).
|
||||
|
||||
### Helm / Kubernetes
|
||||
|
||||
```bash
|
||||
helm upgrade docuelevate ./helm/docuelevate \
|
||||
--namespace docuelevate \
|
||||
-f my-values.yaml
|
||||
```
|
||||
|
||||
The Helm chart's pre-upgrade hook runs `alembic upgrade head` before new pods start.
|
||||
|
||||
### Keep Dependencies Updated
|
||||
|
||||
```bash
|
||||
pip install --upgrade -r requirements.txt
|
||||
safety check # Scan for known CVEs
|
||||
```
|
||||
|
||||
Enable [GitHub Dependabot](https://docs.github.com/en/code-security/dependabot) or similar automated dependency update tooling.
|
||||
|
||||
---
|
||||
|
||||
## 10. Helm / Kubernetes Specifics
|
||||
|
||||
For a dedicated Kubernetes deployment guide, including architecture diagrams, PVC configuration, HPA, and ingress examples, see:
|
||||
|
||||
- [Deployment Guide — Kubernetes / Helm](DeploymentGuide.md#kubernetes--helm-deployment)
|
||||
|
||||
**Additional production recommendations for Kubernetes:**
|
||||
|
||||
- **Pod Disruption Budgets (PDB)**: Ensure at least one API pod is always available during node maintenance.
|
||||
|
||||
```yaml
|
||||
apiVersion: policy/v1
|
||||
kind: PodDisruptionBudget
|
||||
metadata:
|
||||
name: docuelevate-api-pdb
|
||||
spec:
|
||||
minAvailable: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/component: api
|
||||
```
|
||||
|
||||
- **Resource Requests & Limits**: Set CPU/memory requests and limits on all containers to ensure the scheduler can place pods correctly.
|
||||
|
||||
```yaml
|
||||
api:
|
||||
resources:
|
||||
requests:
|
||||
cpu: "250m"
|
||||
memory: "512Mi"
|
||||
limits:
|
||||
cpu: "1000m"
|
||||
memory: "2Gi"
|
||||
worker:
|
||||
resources:
|
||||
requests:
|
||||
cpu: "500m"
|
||||
memory: "1Gi"
|
||||
limits:
|
||||
cpu: "2000m"
|
||||
memory: "4Gi"
|
||||
```
|
||||
|
||||
- **Network Policies**: Restrict traffic so that Redis and Meilisearch are reachable only from DocuElevate pods, not from the internet or other namespaces.
|
||||
|
||||
- **Image Pull Policy**: Use `IfNotPresent` in production with pinned image tags (not `latest`) for reproducible deployments.
|
||||
|
||||
- **Liveness & Readiness Probes**: Already configured in the Helm chart via `/api/health`. Verify they are tuned to your startup time.
|
||||
+15
-3
@@ -4,19 +4,31 @@ Welcome to the DocuElevate documentation. This directory contains comprehensive
|
||||
|
||||
## Available Documentation
|
||||
|
||||
### Getting Started
|
||||
- [Setup Wizard Guide](SetupWizard.md) - First-run wizard walkthrough and initial configuration
|
||||
- [User Guide](UserGuide.md) - How to use DocuElevate's features and interface
|
||||
- [API Documentation](API.md) - Complete API reference for developers
|
||||
- [Deployment Guide](DeploymentGuide.md) - How to deploy DocuElevate in various environments
|
||||
- [Configuration](ConfigurationMaster.md) - Overview of configuration options, including:
|
||||
|
||||
### Deployment
|
||||
- [Deployment Guide](DeploymentGuide.md) - Docker Compose and Kubernetes/Helm deployment
|
||||
- [Kubernetes Deployment Guide](KubernetesDeployment.md) - Detailed Kubernetes/Helm reference
|
||||
- [Production Readiness Guide](ProductionReadiness.md) - Checklist to go from "it works" to production-hardened
|
||||
|
||||
### Configuration
|
||||
- [Configuration Overview](ConfigurationMaster.md) - Overview of configuration options, including:
|
||||
- [Configuration Guide](ConfigurationGuide.md) - Complete list of all available configuration parameters
|
||||
- [Database Configuration](DatabaseConfiguration.md) - SQLite, PostgreSQL, migrations, and optimization
|
||||
- [Google Drive Setup](GoogleDriveSetup.md) - How to set up Google Drive integration
|
||||
- [Dropbox Setup](DropboxSetup.md) - How to set up Dropbox integration
|
||||
- [OneDrive Setup](OneDriveSetup.md) - How to set up Microsoft OneDrive/Graph integration
|
||||
- [Amazon S3 Setup](AmazonS3Setup.md) - How to set up Amazon S3 integration
|
||||
- [Authentication Setup](AuthenticationSetup.md) - How to set up user authentication
|
||||
- [Notifications Setup](NotificationsSetup.md) - How to set up system notifications
|
||||
|
||||
### Reference
|
||||
- [API Documentation](API.md) - Complete API reference for developers
|
||||
- [Configuration Troubleshooting](ConfigurationTroubleshooting.md) - Solutions to common configuration issues
|
||||
- [Troubleshooting](Troubleshooting.md) - General troubleshooting and solutions to common issues
|
||||
- [Licensing & Compliance](LicensingCompliance.md) - License information and third-party dependency compliance
|
||||
|
||||
## Additional Resources
|
||||
|
||||
|
||||
@@ -0,0 +1,139 @@
|
||||
# Setup Wizard Guide
|
||||
|
||||
DocuElevate includes a first-run Setup Wizard that guides you through configuring the essential settings needed for the system to operate. The wizard is the recommended starting point for any new installation.
|
||||
|
||||
## Table of Contents
|
||||
|
||||
- [When the Wizard Appears](#when-the-wizard-appears)
|
||||
- [Wizard Steps](#wizard-steps)
|
||||
- [Step 1 – Core Infrastructure](#step-1--core-infrastructure)
|
||||
- [Step 2 – Security](#step-2--security)
|
||||
- [Step 3 – AI Services](#step-3--ai-services)
|
||||
- [Skipping and Resuming the Wizard](#skipping-and-resuming-the-wizard)
|
||||
- [After the Wizard](#after-the-wizard)
|
||||
- [Integration-Specific Setup Pages](#integration-specific-setup-pages)
|
||||
- [Advanced: Settings Management](#advanced-settings-management)
|
||||
|
||||
---
|
||||
|
||||
## When the Wizard Appears
|
||||
|
||||
The wizard is shown automatically when DocuElevate detects that critical settings are still at their insecure defaults. Specifically, it triggers when **either** of the following is true:
|
||||
|
||||
- `SESSION_SECRET` is still the built-in insecure placeholder
|
||||
- `ADMIN_PASSWORD` is absent or set to a common placeholder value (`changeme`, `admin`, etc.)
|
||||
|
||||
Once both values have been configured — whether through the wizard or through environment variables — the wizard will no longer be shown on the home page.
|
||||
|
||||
> **Tip:** If you pre-populate all settings via environment variables before the first launch, the wizard will be skipped automatically.
|
||||
|
||||
---
|
||||
|
||||
## Wizard Steps
|
||||
|
||||
The wizard is divided into three focused steps, accessible at `/setup?step=<N>`.
|
||||
|
||||
### Step 1 – Core Infrastructure
|
||||
|
||||
Configure the services that DocuElevate depends on at the infrastructure level.
|
||||
|
||||
| Setting | Description | Default |
|
||||
|---------|-------------|---------|
|
||||
| `DATABASE_URL` | Database connection string. SQLite is fine for development; PostgreSQL is recommended for production. | `sqlite:///./app/database.db` |
|
||||
| `REDIS_URL` | Redis connection URL used by the Celery task queue. | `redis://localhost:6379/0` |
|
||||
| `WORKDIR` | Filesystem path where documents are staged during processing. Must be writable by the API and Worker containers. | `/workdir` |
|
||||
| `GOTENBERG_URL` | URL of the Gotenberg service used for document-to-PDF conversion. | `http://gotenberg:3000` |
|
||||
|
||||
> For Docker Compose deployments the defaults work out of the box. You only need to change these if you are using external services or a custom topology.
|
||||
|
||||
### Step 2 – Security
|
||||
|
||||
Configure authentication and session security.
|
||||
|
||||
| Setting | Description | Notes |
|
||||
|---------|-------------|-------|
|
||||
| `SESSION_SECRET` | Secret key used to sign and encrypt session cookies. Minimum 32 characters. | Click **Auto-generate** to let the wizard create a cryptographically secure value for you. |
|
||||
| `ADMIN_USERNAME` | Username for the built-in admin account. | Defaults to `admin`. |
|
||||
| `ADMIN_PASSWORD` | Password for the built-in admin account. | Required. Must not be a placeholder value. |
|
||||
|
||||
> **Security note:** The auto-generated session secret is a 64-character hex string (32 bytes of entropy). Store it somewhere safe — it cannot be recovered if lost. If you rotate the secret, all existing sessions will be invalidated.
|
||||
|
||||
### Step 3 – AI Services
|
||||
|
||||
Configure the AI provider used for metadata extraction and document understanding.
|
||||
|
||||
| Setting | Description | Notes |
|
||||
|---------|-------------|-------|
|
||||
| `AI_PROVIDER` | The AI backend to use. | Options: `openai`, `azure`, `anthropic`, `gemini`, `ollama`, `openrouter`, `portkey`, `litellm` |
|
||||
| `OPENAI_API_KEY` | API key for OpenAI, Azure OpenAI, or LiteLLM-compatible endpoints. | Not required when using Ollama. |
|
||||
| `OPENAI_MODEL` | Default model name (e.g. `gpt-4o-mini`, `claude-3-5-sonnet-20241022`, `llama3.2`). | Used when `AI_MODEL` is not explicitly set. |
|
||||
|
||||
> DocuElevate can operate without an AI provider — metadata extraction tasks will be skipped. You can always add AI credentials later through the [Settings page](SettingsManagement.md).
|
||||
|
||||
---
|
||||
|
||||
## Skipping and Resuming the Wizard
|
||||
|
||||
### Skipping
|
||||
|
||||
If you are an advanced user who has already configured settings via environment variables, you can skip the wizard by clicking **Skip for now** or by visiting:
|
||||
|
||||
```
|
||||
GET /setup/skip
|
||||
```
|
||||
|
||||
This writes a `_setup_wizard_skipped` marker to the database so the wizard is not re-shown automatically.
|
||||
|
||||
### Resuming
|
||||
|
||||
To re-run the wizard after skipping it:
|
||||
|
||||
1. Navigate to **Settings → System** and click **Re-run Setup Wizard**, or
|
||||
2. Visit `/setup/undo-skip` directly.
|
||||
|
||||
This removes the skip marker and redirects you to Step 1.
|
||||
|
||||
---
|
||||
|
||||
## After the Wizard
|
||||
|
||||
Once all three steps are complete you are redirected to the home page with a `?setup=complete` confirmation banner. At that point DocuElevate is operational with the core settings in place.
|
||||
|
||||
**Recommended next steps:**
|
||||
|
||||
1. **Configure a storage destination** — Set up at least one output destination where processed documents will be sent:
|
||||
- [Dropbox Setup](DropboxSetup.md)
|
||||
- [Google Drive Setup](GoogleDriveSetup.md)
|
||||
- [OneDrive Setup](OneDriveSetup.md)
|
||||
- [Amazon S3 Setup](AmazonS3Setup.md)
|
||||
|
||||
2. **Configure notifications** — Set up Discord, Telegram, or email alerts:
|
||||
- [Notifications Setup](NotificationsSetup.md)
|
||||
|
||||
3. **Harden for production** — Follow the production readiness checklist:
|
||||
- [Production Readiness Guide](ProductionReadiness.md)
|
||||
|
||||
---
|
||||
|
||||
## Integration-Specific Setup Pages
|
||||
|
||||
Several cloud storage integrations include their own guided configuration pages that walk you through the OAuth app registration and token exchange process:
|
||||
|
||||
| Integration | Setup URL | Guide |
|
||||
|-------------|-----------|-------|
|
||||
| Dropbox | `/dropbox-setup` | [DropboxSetup.md](DropboxSetup.md) |
|
||||
| Google Drive | `/google-drive-setup` | [GoogleDriveSetup.md](GoogleDriveSetup.md) |
|
||||
| OneDrive / SharePoint | `/onedrive-setup` | [OneDriveSetup.md](OneDriveSetup.md) |
|
||||
| Amazon S3 | Configured via settings | [AmazonS3Setup.md](AmazonS3Setup.md) |
|
||||
|
||||
These pages are accessed **after** the main Setup Wizard is complete and are independent wizard flows specific to each integration.
|
||||
|
||||
---
|
||||
|
||||
## Advanced: Settings Management
|
||||
|
||||
All settings — including those configured through the wizard — can be updated at any time through the Settings page (`/settings`). Settings saved via the wizard are stored in the database and take precedence over the corresponding environment variables.
|
||||
|
||||
For the full list of every available configuration parameter, see the [Configuration Guide](ConfigurationGuide.md).
|
||||
|
||||
> **Precedence order:** Database value → Environment variable → Default
|
||||
+12
-2
@@ -13,16 +13,26 @@ theme:
|
||||
- search.suggest
|
||||
- content.code.copy
|
||||
nav:
|
||||
- User Guide: UserGuide
|
||||
- Getting Started:
|
||||
- Setup Wizard: SetupWizard
|
||||
- User Guide: UserGuide
|
||||
- API: API
|
||||
- Deployment: DeploymentGuide
|
||||
- Deployment:
|
||||
- Overview: DeploymentGuide
|
||||
- Kubernetes / Helm: KubernetesDeployment
|
||||
- Production Readiness: ProductionReadiness
|
||||
- Configuration:
|
||||
- Overview: ConfigurationMaster
|
||||
- Configuration Guide: ConfigurationGuide
|
||||
- Database: DatabaseConfiguration
|
||||
- Dropbox: DropboxSetup
|
||||
- Google Drive: GoogleDriveSetup
|
||||
- OneDrive: OneDriveSetup
|
||||
- Amazon S3: AmazonS3Setup
|
||||
- Authentication: AuthenticationSetup
|
||||
- Notifications: NotificationsSetup
|
||||
- Troubleshooting:
|
||||
- General: Troubleshooting
|
||||
- Configuration: ConfigurationTroubleshooting
|
||||
- Compliance:
|
||||
- Licensing: LicensingCompliance
|
||||
|
||||
Reference in New Issue
Block a user