Skip to content

Deployment

MrBeanDev edited this page Aug 23, 2026 · 1 revision

Deployment

Hosting the whole app yourself, frontend and backend, rather than using the hosted frontend with a local backend.

Face Gallery has no authentication. Anyone who can reach the backend can read, upload, and delete every photo on it. Do not put it on the public internet without putting an authenticating proxy in front.

Build the frontend

cd face-detection-webapp/frontend
npm ci
npm run build

Static files land in dist/. Serve them with nginx, Caddy, or anything else.

The build needs 1 to 2 GB of memory. On a small VPS, build locally and upload dist/ rather than building on the box.

Run the backend

cd face-detection-webapp/backend
FACE_GALLERY_ALLOWED_ORIGINS="https://photos.example.com" \
FACE_GALLERY_DATA_DIR=/var/lib/face-gallery \
  .venv/bin/python -m uvicorn main:app --host 127.0.0.1 --port 8000

Set FACE_GALLERY_ALLOWED_ORIGINS to the origin serving your frontend, or every browser request will be blocked. See Configuration.

Use exactly one worker

Do not run multiple workers. Running processes such as gunicorn -w 4 will break the app.

The registry of running jobs is an in-memory dictionary, and the processing settings are module-level globals. With more than one worker, a pause or stop request often lands on a process that knows nothing about the job and returns 404, WebSocket connections attach to the wrong process, and a settings change applies to whichever worker served it. One worker is the only correct configuration today.

The workload is CPU-bound inside dlib, so extra workers would not buy much anyway.

Surviving a restart

The job registry is in memory, so a restart orphans anything mid-run: it stays processing forever and cannot be resumed. Delete such jobs and start over.

Under systemd, a unit with Restart=on-failure keeps the service up but does not fix orphaned jobs.

Reverse proxy

Point the proxy at port 8000 and serve frontend/dist/ as static files. A single-page app needs a fallback so client-side routes work on a refresh:

location / {
    try_files $uri $uri/ /index.html;
}

location /assets/ {
    expires 1y;
    add_header Cache-Control "public, max-age=31536000, immutable";
}

Without the fallback, reloading on /results/<id> returns 404. Asset filenames are content-hashed, so they are safe to cache indefinitely; index.html must not be, or a deploy will not reach browsers.

Also proxy /api, /ws, /thumb, and /static to the backend, and make sure /ws carries the WebSocket upgrade headers.

Resource use

Measured on 1080p and 12 MP photos:

Situation Peak memory
Idle, dlib models loaded 216 MB
Processing a 2 MP photo 245 MB
Processing a 12 MP photo 388 MB
20 concurrent thumbnail requests, 12 MP 1.3 GB
40 concurrent thumbnail requests, 12 MP 2.2 GB

Memory is not the constraint; the worst case comes from thumbnail generation on a cold cache, not from the ML. Around 2.5 GB is a realistic ceiling, so 4 GB is comfortable and 8 GB is plenty.

CPU is what actually limits throughput. dlib is entirely CPU-bound, so a shared or burstable vCPU processes photos noticeably slower than the core count suggests.

Storage grows quickly: uploads are kept at full resolution, alongside cropped face images and a thumbnail cache.

Clone this wiki locally