This guide covers both development setup and production deployment of text.ur.
text.ur is currently deployed as a single Docker Compose stack for the University of Regensburg, serving a handful of courses. This all-in-one setup is the recommended starting point and should comfortably scale to thousands of users.
For larger deployments, each service (PostgreSQL, Redis, backend, frontend) can be separated and scaled independently based on load.
- 📋 Prerequisites
- 🔧 Environment Variables
- 💻 Development Setup
- 🏭 Production Deployment
- 🔄 Automatic Migrations
- 🌐 Reverse Proxy Configuration
- ♻️ Watchtower Auto-Updates
| Tool | Version | Purpose |
|---|---|---|
| Docker + Docker Compose | Latest | Infrastructure services, production deployment |
| Python | 3.12+ | Backend runtime |
| Node.js | 20+ | Frontend runtime |
| pnpm | 10 | Frontend package manager |
Copy .env.template to .env and configure the values. The tables below list all recognized variables. Values shown are the code defaults (what the application uses if the variable is unset). The .env.template may override some of these with development-friendly values.
PostgreSQL
| Variable | Code Default | Description |
|---|---|---|
POSTGRES_USER |
postgres |
Database user |
POSTGRES_PASSWORD |
dev |
Database password |
POSTGRES_DB |
prod |
Database name |
POSTGRES_HOST |
localhost |
Database host (use postgres in Docker) |
POSTGRES_PORT |
5433 |
Database port |
PGBOUNCER_HOST |
(none) | PgBouncer host - if set, queries are routed through PgBouncer (use pgbouncer in Docker) |
PGBOUNCER_PORT |
(none) | PgBouncer port |
DB_STATEMENT_TIMEOUT |
10000 |
SQL statement timeout in milliseconds |
DB_CONNECTION_TIMEOUT |
10 |
Connection timeout in seconds |
PgBouncer is optional. It is used automatically when
PGBOUNCER_HOSTis set.
Redis
| Variable | Code Default | Description |
|---|---|---|
REDIS_HOST |
localhost |
Redis host (use redis in Docker) |
REDIS_PORT |
6379 |
Redis port |
REDIS_PASSWORD |
(none) | Redis password |
REDIS_COMMANDER_PORT(template default:6016) is only used by the Docker Compose file for the Redis Commander web UI.
Storage
| Variable | Code Default | Description |
|---|---|---|
STORAGE_DIR |
<backend>/storage/ |
Local filesystem directory for uploaded files (PDFs, etc.) |
Path resolution: The code default is an absolute path computed relative to config.py's own location — it always resolves to the storage/ directory inside backend/, regardless of your working directory. If you override STORAGE_DIR via the environment, an absolute path (e.g. /data/storage) is used as-is. A relative path would be resolved against the working directory of the backend process, so prefer absolute paths to avoid ambiguity.
In Docker Compose the volume is mounted to an absolute path so this is not an issue.
Application
| Variable | Code Default | Description |
|---|---|---|
DEBUG |
False |
Enable debug mode (disable in production) |
DEBUG_ALLOW_REGISTRATION_WITHOUT_EMAIL |
False |
Allow registration without email verification (for development or environments without SMTP) |
ENABLE_LOGGING |
False |
Enable file-based logging |
LOG_FILE_DIR |
backend/logs |
Directory for log files |
ORIGIN |
(none) | Required. Public frontend URL (used for CORS, email links) |
INTERNAL_BACKEND_BASEURL |
http://localhost:8000 |
Backend URL used by SvelteKit server during SSR (use http://backend:8000 in Docker) |
PUBLIC_BACKEND_BASEURL |
http://localhost:8000 |
Backend URL used by the browser for client-side API calls and WebSocket connections |
COOKIE_SECURE |
True |
Require HTTPS for cookies (set False for local HTTP development) |
COOKIE_SAMESITE |
lax |
Cookie SameSite attribute |
GUEST_ACCOUNT_TTL_DAYS |
90 |
Guest account lifetime in days (drives refresh-token expiry, cookie max-age, and cleanup) |
PUBLIC_CONTACT_EMAIL |
Text.ur@sprachlit.uni-regensburg.de |
Contact email shown in the footer and legal pages |
Limits
| Variable | Code Default | Description |
|---|---|---|
PUBLIC_MAX_UPLOAD_SIZE_MB |
50 |
Maximum PDF upload size in MB |
PUBLIC_MAX_COMMENT_LENGTH |
2000 |
Maximum comment length in characters |
PUBLIC_MAX_DOCUMENT_NAME_LENGTH |
255 |
Maximum document name length |
PUBLIC_MAX_DOCUMENT_DESCRIPTION_LENGTH |
5000 |
Maximum document description length |
PUBLIC_MAX_TAGS_PER_DOCUMENT |
50 |
Maximum tags per document |
PUBLIC_MAX_TAGS_PER_COMMENT |
15 |
Maximum tags per comment |
Variables prefixed with
PUBLIC_are embedded in the frontend client bundle and available in the browser.
Email / SMTP
| Variable | Code Default | Description |
|---|---|---|
EMAIL_PRESIGN_SECRET |
(none) | Secret for signing email verification URLs |
REGISTER_LINK_EXPIRY_DAYS |
7 |
Registration link validity |
RESET_PASSWORD_LINK_EXPIRY_MINUTES |
30 |
Password reset link validity |
SMTP_USER |
(none) | SMTP username |
SMTP_FROM_EMAIL |
(none) | Sender email address |
SMTP_PASSWORD |
(none) | SMTP password |
SMTP_SERVER |
(none) | SMTP server host |
SMTP_PORT |
(none) | SMTP server port |
SMTP_TLS |
True |
Enable STARTTLS |
SMTP_SSL |
False |
Enable implicit SSL/TLS (port 465) |
If SMTP is not configured, set
DEBUG_ALLOW_REGISTRATION_WITHOUT_EMAIL=Trueto allow registration without email verification.
MAILHOG_PORT(template default:6026) is only used by the Docker Compose file for the MailHog web UI.
JWT
| Variable | Code Default | Description |
|---|---|---|
JWT_SECRET |
(none) | Required. Secret key for signing JWT tokens. Generate with openssl rand -hex 32 |
JWT_ACCESS_EXPIRATION_MINUTES |
30 |
Access token lifetime |
JWT_REFRESH_EXPIRATION_DAYS |
7 |
Refresh token lifetime (non-guest accounts; guest accounts use GUEST_ACCOUNT_TTL_DAYS) |
Development uses docker-compose.dev.yml which runs only the infrastructure services. The backend and frontend run directly on the host for hot-reloading.
cp .env.template .env
# Edit .env as needed (defaults work for local development)
docker compose -f docker-compose.dev.yml up -dThis starts PostgreSQL, PgBouncer, Redis, Redis Commander, and MailHog. See the Environment Variables section for port configuration.
cd backend
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
# Apply database migrations
cd database
alembic upgrade head
cd ..
# Start development server with hot-reload
uvicorn app.main:app --reload --port 8000The API documentation is available at http://localhost:8000/api/docs.
cd frontend
pnpm install
pnpm devThe application is available at http://localhost:5173.
The repository includes pre-configured launch configurations in .vscode/launch.json for easy debugging:
| Configuration | Description |
|---|---|
| Backend | Launches Uvicorn via debugpy - set breakpoints directly in Python code |
| Frontend | Launches the SvelteKit dev server with Node.js debugging attached |
| Frontend: Browser | Opens Chrome with DevTools source maps pointing at the Svelte source |
Use the VS Code Run and Debug panel (Ctrl+Shift+D) to select and launch a configuration. You can run the Backend and Frontend configurations simultaneously for a full debugging experience.
- MailHog captures all outgoing emails at
http://localhost:6026- use this to view registration verification and password reset emails. - Redis Commander is available at
http://localhost:6016for inspecting Redis state. - Set
COOKIE_SECURE=Falsein.envwhen developing without HTTPS, otherwise cookies will not be set. - Set
DEBUG_ALLOW_REGISTRATION_WITHOUT_EMAIL=Trueto bypass email verification during development.
Production uses docker-compose.yml which runs all services including the backend and frontend as containers.
Create .env from the template and update for production:
cp .env.template .envSee the Environment Variables section for descriptions and required variables.
docker compose up -dThis starts all services. The backend automatically applies pending database migrations on startup using a PostgreSQL advisory lock (see Automatic Migrations).
The docker-compose.yml includes commented-out image references. To use pre-built images from the CI pipeline instead of building locally:
# Replace the build context with the image reference:
backend:
image: ghcr.io/realdegrees/text.ur/backend:stable
# build:
# context: ./backend
# dockerfile: Dockerfile
frontend:
image: ghcr.io/realdegrees/text.ur/frontend:stable
# build:
# context: ./frontend
# dockerfile: DockerfileImage tags:
stable- built from themainbranchlatest- built from thedevelopbranchsha-<commit>- built from a specific commit
A reverse proxy is required in production for TLS termination. See Reverse Proxy Configuration.
Database migrations are applied automatically when the Gunicorn master process starts (in the on_starting hook in gunicorn.conf.py), before any workers are forked:
- Acquire advisory lock — uses
pg_try_advisory_lock(100001)to ensure only one process runs migrations at a time. This is critical when scaling to multiple backend replicas. - Run migrations — if the lock is acquired, runs
alembic upgrade head. If it fails, the process exits with an error. - Skip if locked — if another process already holds the lock, migrations are skipped (the other process is handling them).
- Release lock — the lock is released after the migration completes.
This means you can safely run multiple backend containers in parallel — only one will execute migrations, and the others will skip and proceed to serving.
Note: Advisory locks (both migration and cleanup) connect directly to PostgreSQL, bypassing PgBouncer. Session-level advisory locks are incompatible with PgBouncer's
transactionpool mode because PgBouncer multiplexes backend connections between transactions — a lock acquired on one transaction may be invisible to the next. The direct connection usesPOSTGRES_HOST/POSTGRES_PORTfrom the environment.
text.ur requires a reverse proxy in production for TLS termination. The proxy should forward traffic to both the frontend and backend:
- Frontend (port 3000) - serves pages via SSR and static assets
- Backend (port 8000) - serves API requests and WebSocket connections from the browser
The proxy must:
- Terminate TLS (HTTPS)
- Forward
X-Forwarded-ForandX-Real-IPheaders - Support WebSocket upgrade for
/api/documents/*/events
The production docker-compose.yml includes a Watchtower instance that monitors containers labeled with com.centurylinklabs.watchtower.scope=textur. When a new Docker image is pushed to the registry (e.g. after a CI build), Watchtower automatically pulls the new image and restarts the container.
- Poll interval: 30 seconds
- Scope: Only containers with the
texturscope label - Cleanup: Old images are removed after update
To disable auto-updates, remove or stop the watchtower service.