Deployment Guide
Docker Compose, single-container, and source deployment; plus database configuration, persistence, environment variables, and troubleshooting.
Complete deployment guide for the multi-user schedule and event management tool, covering three deployment methods.
📋 Table of Contents
Section titled “📋 Table of Contents”- System requirements
- Deployment methods
- Database configuration
- Container persistence
- Environment configuration
- Troubleshooting
📦 System requirements
Section titled “📦 System requirements”Docker deployment
Section titled “Docker deployment”- Docker 20.0+
- Docker Compose 2.0+
- Available memory: 1GB+
- Available disk: 2GB+
Source deployment
Section titled “Source deployment”- Python 3.10+
- uv 0.4+
- Bun 1.0+ (frontend requires Bun; do not use npm/yarn)
- Available memory: 512MB+
- Available disk: 1GB+
🚀 Deployment methods
Section titled “🚀 Deployment methods”1. Docker Compose (recommended)
Section titled “1. Docker Compose (recommended)”The simplest one-click deployment path, suitable for production use.
Quick start
Section titled “Quick start”# 1. Clone the projectgit clone <repository-url>cd SDNUChronoSync
# 2. Create data directoriesmkdir -p data/{uploads,config,logs}
# 3. Only write the initial template if no config exists; upgrades and restarts must not overwrite existing configtest -f data/config/config.toml || cp backend/config.toml data/config/config.toml
# 4. Only create the environment file on first deployment; an existing .env must be preserved as-isif [ ! -f .env ]; then umask 077 cat > .env << EOFPOSTGRES_PASSWORD=$(openssl rand -hex 16)APP_ENV=productionSECRET_KEY=$(openssl rand -hex 32)EOFfi
# 5. Start services (automatically starts PostgreSQL + app)docker compose up -d
# 6. Check service statusdocker compose psKey environment variables (required for production)
Section titled “Key environment variables (required for production)”Production must set SECRET_KEY (shared by JWT and email verification code digests; all workers must agree), and explicitly set APP_ENV=production. Docker Compose passes both values in from .env.
Generate a random SECRET_KEY (either method):
# Method A: opensslopenssl rand -hex 32
# Method B: uv + pythoncd backend && uv run python -c "import secrets; print(secrets.token_urlsafe(48))"Then write it into the environment: block of docker-compose.yml (example):
environment: - APP_ENV=production - SECRET_KEY=<paste-your-secret-key-here> # optional: CORS (comma-separated domains) # - ALLOWED_ORIGINS=https://your-domain.com,http://localhost:4321If APP_ENV=production is set but SECRET_KEY is not, the backend refuses to start (to avoid the security risk of a default weak key).
Service access
Section titled “Service access”- App URL: https://localhost:1145
- API docs: https://localhost:1145/docs
- Health check: https://localhost:1145/health
The Docker image’s built-in Nginx has HTTPS + HTTP/2 enabled (single-connection multiplexing). A self-signed certificate is auto-generated by default, so the browser may warn “Not secure” on first visit; for production, mount your own certificates at
/etc/nginx/certs/tls.crtand/etc/nginx/certs/tls.key.
On PostgreSQL 18 and later, the actual PGDATA lives at /var/lib/postgresql/<major>/docker, so Compose mounts the named volume at the parent /var/lib/postgresql. Reverting to /var/lib/postgresql/data would leave the real database cluster in an unmanaged anonymous volume.
Compose mounts ./data/logs to /app/logs. Supervisor reopens existing log files in append mode, so container restarts never truncate old logs; upgrades and migrations must not delete or re-initialize this directory.
Management commands
Section titled “Management commands”# View logsdocker compose logs -f
# Stop servicesdocker compose down
# Rebuild and startdocker compose up -d --build
# Enter the containerdocker compose exec sdnu-chronosync bash
# Back up the PostgreSQL databasemkdir -p backupsdocker compose exec db pg_dump -U chronosync -d chronosync -Fc > "backups/chronosync_$(date +%Y%m%d_%H%M%S)_$$.dump"💾 Database configuration
Section titled “💾 Database configuration”PostgreSQL (recommended)
Section titled “PostgreSQL (recommended)”The sole source of truth for the production schema is scripts/migrations/alembic/. The Docker entrypoint runs uv run alembic -c /app/alembic.ini upgrade head before starting Supervisor; at startup the app re-verifies that alembic_version equals the live head. The PostgreSQL path never calls Base.metadata.create_all() to patch or upgrade tables — if a revision is missing or mismatched, the app refuses to start.
For source deployments or manual upgrades during a maintenance window:
cd backendexport DATABASE_URL='postgresql+psycopg://chronosync:密码@localhost:5432/chronosync'uv run alembic -c alembic.ini upgrade headuv run alembic -c alembic.ini currentuv run alembic -c alembic.ini headscurrent and heads must report the same revision. For a fresh empty database, run upgrade head directly. If the old PostgreSQL instance was created by an early create_all, business tables exist but there is no alembic_version, do not start the app or manually stamp head; you must run the restricted bootstrap after stopping the service and taking a backup:
cd backenduv run python ../scripts/migrations/bootstrap_postgres.py --database-url "$DATABASE_URL"The bootstrap only accepts the c4d5e6f7a8b9 catalog baseline declared in code (complete tables, columns, primary keys, foreign keys, and required indexes), stamps to that explicit revision, then transactionally upgrades to head; any incomplete object is rejected — it cannot be used to bypass arbitrary old schemas.
Environment variables
Section titled “Environment variables”| Variable | Description | Example |
|---|---|---|
POSTGRES_PASSWORD | PostgreSQL password (required) | generate with openssl rand -hex 16 |
DATABASE_URL | SQLAlchemy connection string | postgresql+psycopg://chronosync:密码@db:5432/chronosync |
DB_POOL_SIZE | Connection pool size (default 10) | 10 |
DB_MAX_OVERFLOW | Overflow connections (default 20) | 20 |
DB_POOL_RECYCLE | Connection recycle seconds (default 1800) | 1800 |
The per-process business connection cap is DB_POOL_SIZE + DB_MAX_OVERFLOW, plus one independent health-check connection; multi-process deployments must size PostgreSQL max_connections accounting for one probe connection per worker.
The health check uses an independent single-connection pool; both pool wait and database connect timeouts are 2 seconds, so when the business pool is exhausted the probe returns not-ready immediately instead of waiting on DB_POOL_TIMEOUT.
SQLite (development only)
Section titled “SQLite (development only)”Local development can continue to use SQLite; the app keeps Base.metadata.create_all() on the SQLite path only, to create missing objects. It does not replace field upgrades on old production databases.
cd backend# default DATABASE_URL=sqlite:///./schedule_app.dbuv run python main.pyAn already-deployed legacy SQLite database must run a dedicated upgrader while stopped before switching to PostgreSQL; do not rely on starting the app to implicitly fix the database.
Migrating from SQLite to PostgreSQL
Section titled “Migrating from SQLite to PostgreSQL”The full command list, verification checkpoints, and rollback steps are maintained only in the Migration Runbook. This must be executed during a downtime maintenance window; no application process may write to the source or target database while it runs. Preserve the original SQLite database and old container, upgrade only the working copy, and import only into a fresh empty PostgreSQL.
2. Docker single-container
Section titled “2. Docker single-container”Suitable for simple deployment scenarios where you manage data persistence manually.
Building the image
Section titled “Building the image”The Docker image build freezes frontend and backend dependencies via
backend/uv.lockandfrontend/bun.lock, runninguv sync --frozenandbun install --frozen-lockfilerespectively..dockerignoreexcludes databases, environment files, virtual environments, upload directories, and caches so production data never ends up in the image.
# 1. Build the imagedocker build -t sdnu-chronosync:latest .
# 2. Prepare data directories and config filesmkdir -p ~/sdnu-data/{uploads,config,logs}test -f ~/sdnu-data/config/config.toml || cp backend/config.toml ~/sdnu-data/config/config.toml
# 3. Run the container (requires an external PostgreSQL already running)docker run -d \ --name sdnu-chronosync \ -p 1145:1145 \ -e APP_ENV=production \ -e SECRET_KEY='<replace-with-strong-random-string>' \ -e DATABASE_URL='postgresql+psycopg://chronosync:password@host.docker.internal:5432/chronosync' \ -e ALLOWED_ORIGINS='https://your-domain.com' \ -v ~/sdnu-data/uploads:/app/uploads \ -v ~/sdnu-data/config/config.toml:/app/config.toml:ro \ -v ~/sdnu-data/logs:/app/logs \ --restart unless-stopped \ sdnu-chronosync:latestNote: If you need to edit “System Settings / Code Injection” through the admin panel, change -v ~/sdnu-data/config/config.toml:/app/config.toml:ro to a writable mount (remove :ro).
Important: The external PostgreSQL data is persisted by the database service itself; the app container only bind-mounts the upload directory, config.toml, and the log directory. Make sure ~/sdnu-data/config/config.toml exists before starting the container.
Management commands
Section titled “Management commands”# Check container statusdocker ps
# View logsdocker logs sdnu-chronosync -f
# Stop the containerdocker stop sdnu-chronosync
# Start the containerdocker start sdnu-chronosync
# Delete the containerdocker rm sdnu-chronosync
# Back up the PostgreSQL database (requires connection to the PostgreSQL container)docker exec sdnu-chronosync-db pg_dump -U chronosync -d chronosync -Fc > "backup_$(date +%Y%m%d_%H%M%S)_$$.dump"3. Source deployment (bun + uv)
Section titled “3. Source deployment (bun + uv)”Suitable for development environments or scenarios that require customization.
Environment preparation
Section titled “Environment preparation”# 0. Install uv (https://docs.astral.sh/uv/getting-started/installation/)
# 1. Install Python dependenciescd backenduv sync
# 2. Install frontend dependencies (bun only)cd ../frontendbun install
# 3. Build the frontendbun run buildStarting services
Section titled “Starting services”Method 1: Using the start script (recommended)
Section titled “Method 1: Using the start script (recommended)”# From the project rootchmod +x scripts/start_dev.sh./scripts/start_dev.shMethod 2: Manual start
Section titled “Method 2: Manual start”# Terminal 1: start the backend (port 8000)cd backenduv run python main.py
# Optional: enable HTTPS + HTTP/2 on the backend port itself (requires TLS; a self-signed cert is auto-generated by default)# then visit https://localhost:8000# ENABLE_HTTP2=1 uv run python main.py
# Terminal 2: start the frontend (port 4321)cd frontendbun run dev
# Terminal 3: use nginx as a reverse proxy to port 1145 (optional)# configure nginx.conf to forward requests to the corresponding serviceProduction deployment
Section titled “Production deployment”# 1. Build the frontendcd frontendbun run build
# 2. Configure the production server (nginx + uvicorn)# nginx config example:server { listen 443 ssl http2; server_name your-domain.com;
ssl_certificate /etc/nginx/certs/tls.crt; ssl_certificate_key /etc/nginx/certs/tls.key;
location / { root /path/to/frontend/dist; try_files $uri $uri/ @backend; }
location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; }
location @backend { proxy_pass http://127.0.0.1:8000; }}
# 3. Start the backend with uvcd backenduv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4💾 Container persistence
Section titled “💾 Container persistence”Data persistence strategy
Section titled “Data persistence strategy”Containerized deployment must persist important data to the host so that it survives container restarts.
Host directory structure
Section titled “Host directory structure”The application files recommend the following bind mounts; PostgreSQL data does not live in an SQLite file under this directory but is persisted by the postgres_data named volume:
~/sdnu-data/├── uploads/│ └── avatars/├── config/│ └── config.toml└── logs/ ├── fastapi.log └── nginx.logYou must create config.toml first, or Docker may create the target path as a directory.
1. Docker Compose persistence (auto-configured)
Section titled “1. Docker Compose persistence (auto-configured)”The current docker-compose.yml persistence mapping is:
services: db: volumes: - postgres_data:/var/lib/postgresql
sdnu-chronosync: volumes: - ./data/uploads:/app/uploads - ./data/config/config.toml:/app/config.toml:ro - ./data/logs:/app/logs
volumes: postgres_data:On PostgreSQL 18 and later, the actual PGDATA lives at /var/lib/postgresql/<major>/docker, so the named volume must mount the parent /var/lib/postgresql. If you want to save site config or code injection through the admin panel, remove the :ro from the config.toml mount; otherwise edit it directly on the host and restart the container.
2. Docker single-container persistence (manual configuration)
Section titled “2. Docker single-container persistence (manual configuration)”# Create host directoriesmkdir -p ~/sdnu-data/{database,uploads,config,logs}
# Only copy the initial config on first deployment; existing files must not be overwrittentest -f ~/sdnu-data/config/config.toml || cp backend/config.toml ~/sdnu-data/config/config.toml
# Run the container and mount directories (specify the database path via environment variable)docker run -d \ --name sdnu-chronosync \ -p 1145:1145 \ -e DATABASE_URL=sqlite:////app/data/schedule_app.db \ -v ~/sdnu-data/database:/app/data \ -v ~/sdnu-data/uploads:/app/uploads \ -v ~/sdnu-data/config/config.toml:/app/config.toml:ro \ -v ~/sdnu-data/logs:/app/logs \ --restart unless-stopped \ sdnu-chronosync:latestMount notes:
~/sdnu-data/database:/app/data— database file directory (must be thedatabasesubdirectory)~/sdnu-data/uploads:/app/uploads— user upload directory~/sdnu-data/config/config.toml:/app/config.toml:ro— config file (read-only)~/sdnu-data/logs:/app/logs— application log directory
3. Data backup strategy
Section titled “3. Data backup strategy”PostgreSQL is backed up with pg_dump, which supports hot backups (no downtime):
# Create the backup scriptcat > backup.sh << 'EOF'#!/bin/bashset -euo pipefail
RUN_ID="$(date +%Y%m%d_%H%M%S)_$$"BACKUP_DIR="./backups/$RUN_ID"mkdir -p "$BACKUP_DIR"
# Back up PostgreSQL (custom format, supports selective restore)docker compose exec -T db pg_dump -U chronosync -d chronosync -Fc > "$BACKUP_DIR/chronosync.dump"
# Preserve uploads, config, env vars, and runtime logs; each run writes to its own directory.cp -a ./data/uploads "$BACKUP_DIR/uploads"cp -a ./data/config "$BACKUP_DIR/config"cp -a ./data/logs "$BACKUP_DIR/logs"if [ -f ./.env ]; then cp -a ./.env "$BACKUP_DIR/.env"fi
# Old backups are not auto-deleted; define a retention policy based on actual storage capacity.echo "Backup complete: $BACKUP_DIR"EOF
chmod +x backup.sh
# Schedule periodic backups (optional)# crontab -e# 0 2 * * * /path/to/backup.shRestoring a backup:
# Restore a PostgreSQL backupdocker compose exec db pg_restore -U chronosync -d chronosync backups/xxx/chronosync.dump
# Or restore to a new databasedocker compose exec db createdb -U chronosync chronosync_restoredocker compose exec db pg_restore -U chronosync -d chronosync_restore backups/xxx/chronosync.dumpImportant directory reference
Section titled “Important directory reference”| Directory / File | Purpose | Must persist? |
|---|---|---|
PostgreSQL named volume (postgres_data) | PostgreSQL data files | Must preserve |
.env | Database password, unified SECRET_KEY, and other environment variables | Must preserve and never regenerate |
/app/uploads/avatars/ | User avatar files | Must preserve original mount |
/app/config.toml | Application config file | Must preserve original file |
/app/logs/ | Application and Supervisor logs | Must preserve original directory; restarts append, old logs are not cleared |
⚙️ Environment configuration
Section titled “⚙️ Environment configuration”Config file reference
Section titled “Config file reference”Main config file: config.toml
[storage]provider = "local" # or "alist"
[storage.local]upload_path = "uploads/avatars"base_url = "/static/avatars"
[storage.alist]version = 3url = "https://your-alist-instance.com"upload_path = "your-upload-path"token = "your-token"username = "your-username"password = "your-password"Environment variables (Docker)
Section titled “Environment variables (Docker)”# .env file exampleNODE_ENV=productionPYTHONUNBUFFERED=1
# Strongly recommended for production (used to sign login tokens)APP_ENV=productionSECRET_KEY=<a long random string>
# PostgreSQL password (required, used by Docker Compose)POSTGRES_PASSWORD=<a long random password>
# Optional: database config (defaults to the PostgreSQL in Docker Compose)# DATABASE_URL=postgresql+psycopg://chronosync:password@db:5432/chronosync
# Optional: connection pool parameters# DB_POOL_SIZE=10# DB_MAX_OVERFLOW=20# DB_POOL_RECYCLE=1800
# Optional: CORS (comma-separated domains)# ALLOWED_ORIGINS=https://your-domain.com,http://localhost:4321
# Optional: admin initialization behavior# AUTO_CREATE_ADMIN=1# SHOW_INITIAL_ADMIN_PASSWORD=1# DEFAULT_ADMIN_PASSWORD=<set only if you know what you are doing># CREATE_SAMPLE_USERS=0
# Optional: external storage config# STORAGE_PROVIDER=alist# ALIST_URL=https://your-alist.com# ALIST_TOKEN=your-token
# Optional: allowed external hosts for controlled code injection (comma-separated domains)# CODE_INJECTION_ALLOWED_HOSTS=analytics.hxcn.dev,hm.baidu.com,www.googletagmanager.comCORS configuration
Section titled “CORS configuration”When the browser reports Access to fetch ... has been blocked by CORS policy, add the frontend’s actual Origin to ALLOWED_ORIGINS, comma-separated for multiple sources. This value is an environment variable, not configured in config.toml; you must restart the backend or rebuild the container after changing it.
# Local development: backend/.envALLOWED_ORIGINS=http://localhost:4321
# Docker single-containerdocker run ... \ -e ALLOWED_ORIGINS='https://your-domain.com,http://localhost:4321' \ ...Docker Compose configures it in the app service’s environment::
environment: - ALLOWED_ORIGINS=https://your-domain.com,http://localhost:4321Then run docker compose up -d. CORS only accepts full Origins (protocol, host, and port); do not include page paths, and do not loosen it to any origin for development convenience.
Backend stability and performance parameters
Section titled “Backend stability and performance parameters”| Variable | Default | Description |
|---|---|---|
DB_POOL_SIZE | 10 | Per-worker steady-state PostgreSQL business connections |
DB_MAX_OVERFLOW | 20 | Per-worker temporary overflow connections |
DB_POOL_TIMEOUT | 30 | Business pool wait seconds |
DB_POOL_RECYCLE | 1800 | PostgreSQL connection recycle seconds |
AUTH_RATE_LIMIT_WINDOW_SECONDS | 300 | Login failure counting window |
AUTH_RATE_LIMIT_MAX_ATTEMPTS | 8 | Max login failures per window |
AUTH_RATE_LIMIT_LOCKOUT_SECONDS | 600 | Login lockout seconds |
AUTH_RATE_LIMIT_CLEANUP_INTERVAL_SECONDS | 300 | Expired rate-limit record cleanup interval |
REGISTER_RATE_LIMIT_MAX_ATTEMPTS | 10 | Max registration attempts per IP per window |
REGISTER_RATE_LIMIT_WINDOW_SECONDS | 600 | Registration IP counting window |
JWXT_HTTP_TIMEOUT_GET | 10 | Academic affairs GET request timeout seconds |
JWXT_HTTP_TIMEOUT_POST | 15 | Academic affairs POST request timeout seconds |
JWXT_MAX_CONCURRENCY | 3 | Max concurrency for classroom-availability upstream requests |
CLASSROOM_SESSION_TTL_SECONDS | 600 | Classroom-availability auth session lifetime |
CLASSROOM_SESSION_MAX_ITEMS | 256 | Per-process classroom-availability session capacity |
CLASSROOM_SESSION_CLEANUP_INTERVAL_SECONDS | 60 | Session cleanup interval |
Login and registration rate limiting and email verification codes use shared database state, so multiple workers share them correctly. All workers must still use the same stable SECRET_KEY, otherwise JWTs and verification-code HMACs cannot be verified across processes. Each worker’s business connection cap is DB_POOL_SIZE + DB_MAX_OVERFLOW, plus one independent health-probe connection; account for this against PostgreSQL max_connections before adding workers.
Academic affairs connection addresses:
# The on-campus direct upstream only supports HTTP; do not force HTTPS.JWXT_DIRECT_BASE_URL=http://jwxt.sdnu.edu.cn/jwglxtJWXT_WEBVPN_BASE_URL=https://webvpn.sdnu.edu.cn:10443/http/<token>/jwglxtJWXT_WEBVPN_SSO_LOGIN_URL=https://webvpn.sdnu.edu.cn/enlink/sso/loginWebVPN and academic affairs system accounts can differ; credentials are entered by the user each time and must not be written to environment variables, config files, or logs.
🔐 Controlled code injection (analytics scripts / stylesheets / meta)
Section titled “🔐 Controlled code injection (analytics scripts / stylesheets / meta)”This project supports injecting third-party analytics or style resources into the page head / body, but to avoid XSS risk, “code injection” has been narrowed to a controlled whitelist mechanism.
Injection restrictions (summary)
Section titled “Injection restrictions (summary)”- Only
script[src]/link[rel=stylesheet,href]/meta[name|property,content]are allowed - Inline scripts and event attributes (e.g.
onclick) are forbidden - Same-origin scripts are limited to
/assets/*.js; same-origin styles are limited to/assets/*.css - External resources must be
https://and the domain must be inCODE_INJECTION_ALLOWED_HOSTS
How to configure the external-host whitelist (CODE_INJECTION_ALLOWED_HOSTS)
Section titled “How to configure the external-host whitelist (CODE_INJECTION_ALLOWED_HOSTS)”- Source deployment / direct run
- Preferred: add to
backend/.env:CODE_INJECTION_ALLOWED_HOSTS=analytics.hxcn.dev
- Or set a system environment variable before starting the backend.
- Preferred: add to
- systemd
- Set
Environment="CODE_INJECTION_ALLOWED_HOSTS=analytics.hxcn.dev"in the unit, or useEnvironmentFile=; after changing, runsystemctl daemon-reloadand restart the service.
- Set
- Docker Compose deployment
- Add to the
environment:block ofdocker-compose.yml:CODE_INJECTION_ALLOWED_HOSTS=analytics.hxcn.dev
- Add to the
- Docker single-container deployment
- Add at
docker runtime:-e CODE_INJECTION_ALLOWED_HOSTS=analytics.hxcn.dev
- Add at
Reverse proxy / CSP considerations
Section titled “Reverse proxy / CSP considerations”If you allow external analytics scripts, the reverse proxy layer must allow them too:
script-src/script-src-elem: allow the script domainsconnect-src: allow the analytics reporting domains (many analytics tools report viafetch)
The Nginx config used by Docker deployment is at docker/nginx.conf; if you change analytics domains, update the CSP at the same time.
Default accounts
Section titled “Default accounts”The system auto-creates an admin account when no student_id=admin exists in the database:
- Username:
admin - Password: randomly generated at first startup, printed only once in the initialization logs
- Role: admin
Test student accounts are not created by default. Sample users are only created when CREATE_SAMPLE_USERS=1 is explicitly set; do not enable this in production.
Retrieving the initial admin password
Section titled “Retrieving the initial admin password”When the system detects that no admin account exists in the database, it auto-creates the student_id=admin admin account and prints the password once in the backend startup logs:
[SECURITY] Password: <random_password>
Optional environment variables:
DEFAULT_ADMIN_PASSWORD: specify the initial admin password (a fixed weak password is not recommended in production)SHOW_INITIAL_ADMIN_PASSWORD=0: suppress printing the initial password in logs (prints<hidden>instead)AUTO_CREATE_ADMIN=0: disable auto-creating the admin (not recommended — may lock you out of the admin panel)
Docker / docker compose
Section titled “Docker / docker compose”Check the container logs immediately after first startup and find the [SECURITY] line:
docker compose logs --no-color sdnu-chronosync | grep "\[SECURITY\]"Direct run (plain install)
Section titled “Direct run (plain install)”If you start the backend directly with uv run python main.py / uv run uvicorn, the initial password prints to the current terminal. Save it at first startup.
systemd (plain install)
Section titled “systemd (plain install)”If you host the backend with systemd, use:
journalctl -u <your-service-name> -b --no-pager | grep "\[SECURITY\]"Security reminder: this initial password is printed only once; change the admin password immediately after first login.
🔧 Troubleshooting
Section titled “🔧 Troubleshooting”Common issues
Section titled “Common issues”1. Container fails to start
Section titled “1. Container fails to start”# Check port conflictsnetstat -tulpn | grep 1145
# Check container logsdocker compose logs sdnu-chronosync
# Check resource usagedocker stats2. Database connection or revision check failure
Section titled “2. Database connection or revision check failure”# Check PostgreSQL container status and logsdocker compose ps dbdocker compose logs dbdocker compose exec db pg_isready -U chronosync -d chronosync
# Check the Alembic current/head as seen by the app imagedocker compose run --rm --entrypoint uv sdnu-chronosync \ run alembic -c /app/alembic.ini currentdocker compose run --rm --entrypoint uv sdnu-chronosync \ run alembic -c /app/alembic.ini heads
# Only run the upgrade on a fresh empty database or one already on the live chaindocker compose run --rm --entrypoint uv sdnu-chronosync \ run alembic -c /app/alembic.ini upgrade headPostgreSQL forbids bypassing the version gate with Base.metadata.create_all() or legacy fix scripts. When business tables exist but there is no alembic_version, follow the restricted bootstrap section under PostgreSQL above; when switching from SQLite to PostgreSQL, follow the Migration Runbook for a fresh cutover.
Common mistake: database and config file not taking effect
If the database or config.toml is not loaded as expected, check:
# 1. Confirm the PostgreSQL container is runningdocker compose ps db
# 2. Verify the env file and required variables without echoing secretstest -f .envset -a. ./.envset +atest -n "${POSTGRES_PASSWORD:-}"test -n "${SECRET_KEY:-}"
# 3. Confirm the config file exists; only copy the template if missingtest -f /path/to/sdnu-data/config/config.toml || \ cp backend/config.toml /path/to/sdnu-data/config/config.toml
# 4. Restart the container for changes to take effectdocker compose restart3. File upload fails
Section titled “3. File upload fails”# Check upload directory permissionsls -la data/uploads/
# Fix permissionschmod -R 755 data/uploads/4. Frontend assets fail to load
Section titled “4. Frontend assets fail to load”# Rebuild the imagedocker compose up -d --build
# Check the nginx configdocker compose exec sdnu-chronosync nginx -t5. Access returns 502, logs report a --log-config path that does not exist
Section titled “5. Access returns 502, logs report a --log-config path that does not exist”An old image’s supervisord.conf may reference a non-existent Uvicorn log config. Prefer rebuilding the image from current source so the code, uvicorn_log_config.json, and Supervisor config stay consistent:
docker compose up -d --buildOnly when you truly cannot rebuild the old image, mount the corrected Supervisor config at /etc/supervisor/conf.d/supervisord.conf:ro. Once you have switched to the current image in production, remove that compatibility mount to prevent the old config from overriding the in-image config indefinitely.
Viewing logs
Section titled “Viewing logs”# Application logsdocker compose logs -f sdnu-chronosync
# Backend API logsdocker compose exec sdnu-chronosync tail -f logs/fastapi.log
# Nginx logsdocker compose exec sdnu-chronosync tail -f logs/nginx.log
# System monitoringdocker compose exec sdnu-chronosync topPerformance optimization
Section titled “Performance optimization”The default single worker is sufficient for light deployments. Before increasing Uvicorn workers, you must account for PostgreSQL connections and memory, and ensure all workers use the same SECRET_KEY:
# docker/supervisord.conf examplecommand=uv run uvicorn main:app --host 0.0.0.0 --port 8000 --workers 4Each worker uses at most DB_POOL_SIZE + DB_MAX_OVERFLOW business connections, plus one independent health-probe connection. The pool is tunable via environment variables:
DB_POOL_SIZE=10DB_MAX_OVERFLOW=20DB_POOL_TIMEOUT=30DB_POOL_RECYCLE=1800The health-probe connection pool is fixed at a single connection with a 2-second pool-wait and connect timeout, used to return not-ready quickly when the business pool is exhausted. Adjust PostgreSQL shared_buffers, effective_cache_size, and work_mem based on actual memory and query-load measurements rather than fixed template values.
📞 Support
Section titled “📞 Support”If you run into issues, check:
- Whether system resources are sufficient
- Whether the port is already in use
- Whether the data directory permissions are correct
- Whether the firewall is blocking access
For more help, see the project docs or file an Issue.
🎉 Once deployment is complete, visit https://localhost:1145 to get started!