Deployment
Docker Compose (recommended)
git clone https://github.com/sleep1223/fast-soy-admin
cd fast-soy-admin
just docker-db-init
just up # == docker compose up -dRun initdb only once for a fresh database. The final just up starts the full stack and lets the app write default users, menus, roles, APIs, and business seed data.
| Service | Port | Purpose |
|---|---|---|
| nginx | 1880 | Frontend + API reverse proxy |
| app | 9999 | FastAPI backend |
| redis | 6379 | Cache |
Logs
just logs # == docker compose logs -f --tail=100
just logs app # == docker compose logs -f --tail=100 app
just logs app 200 # == docker compose logs -f --tail=200 appUpdate
git pull
just down && just rebuild # == docker compose down && docker compose up -d --buildManual deployment
Backend
uv sync --no-dev
# Granian (recommended; matches docker setup)
uv run granian --interface asgi --host 0.0.0.0 --port 9999 --workers 4 app:app
# Or uvicorn
uv run uvicorn app:app --host 0.0.0.0 --port 9999 --workers 4Behind a reverse proxy
Set PROXY_HEADERS_ENABLED=true and TRUSTED_HOSTS so granian reconciles X-Forwarded-For / X-Forwarded-Proto and the real client IP reaches fastapi-guard's rate limiting. Otherwise every request looks like it comes from the proxy IP and gets banned.
Frontend
cd web && pnpm install && pnpm build
# Deploy dist/ to your web serverNginx
server {
listen 80;
root /path/to/web/dist;
location / {
try_files $uri $uri/ /index.html;
}
location /api/ {
proxy_pass http://127.0.0.1:9999;
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;
}
}Key production checklist
- [ ]
.envSECRET_KEYrotated to a secure random value (note: this also invalidates historical sqids and JWTs) - [ ]
APP_DEBUG=false(hides/openapi.json/ Swagger) - [ ]
DB_URLswitched to PostgreSQL or MySQL (with appropriate driver installed) - [ ]
REDIS_URLset with strong password - [ ]
PROXY_HEADERS_ENABLED=true+TRUSTED_HOSTSif behind nginx / gateway - [ ]
CORS_ORIGINSrestricted to actual frontend origins - [ ]
RADAR_ENABLED=falseunless Radar is explicitly required; when enabled, verify onlyR_ADMIN/R_SUPERcan access/__radar/api/* - [ ] Logs rotated / shipped (default goes to
logs/, retention 30 days) - [ ] Migrations applied:
just mmafter deploy - [ ] Multi-worker setup verified: only the leader writes seeds (check
app:init_donein Redis)
Radar response when upgrading an affected deployment
Radar is disabled by default. Confirm RADAR_ENABLED in .env / .env.docker after upgrading, and enable it only when the diagnostic need is understood. If enabled, anonymous and regular-user calls to /__radar/api/* must fail; only R_ADMIN and R_SUPER should succeed.
If an older public deployment ran with Radar enabled:
- review Nginx, gateway, or WAF logs for historical
/__radar/apiaccess; - invalidate potentially exposed tokens and reset passwords, API keys, or other credentials first; if signing-key exposure is suspected or global invalidation is required, assess the Sqids impact before rotating
SECRET_KEY; - remember that the fix sanitizes old rows when read but does not rewrite existing plaintext in the database;
- Radar tables live in the database selected by
DB_URLand are covered by its normal backup / restore process. Before removingradar_requestsand its cascadingradar_queries/radar_user_logs, take a backup, define the exact scope, and obtain separate approval for the destructive operation. The patch performs no automatic cleanup or schema change.
See Radar sensitive-data boundaries and historical-data handling.
See also
- Configuration — env vars
- Switching DB — drivers
- Monitoring — Radar / Guard tuning