Published images
Open Wearables publishes two production images to Docker Hub:
The single backend image covers every backend role. The start command selects what the container runs:
Image tags
Both images share the same tag scheme. Choose a tag based on how much stability you need:
Tags are produced by
.github/workflows/publish-images.yml:
- Nightly - a scheduled run at
02:00UTC buildsmainand pushesnightlyplus an immutablenightly-<sha>. - Release - publishing a GitHub release builds its git tag, pushes the version tags (
0.7.0,0.7), and moveslatest. Pre-releases are published under their version tag but do not movelatest. - Manual - the workflow can also be run by hand from the GitHub Actions UI. It requires a tag input and publishes that tag only - no
nightlytags. Reserved names -latest,nightly, the wholenightly-*namespace, and release versions like0.7.0/0.7- are rejected, so a manual run can never move or overwrite them.
0.7.0) rather than latest or nightly, so deployments are reproducible and upgrades are intentional.
What a deployment runs
A typical deployment consists of the application containers plus their backing services:app- the API, from the backend imagecelery-worker- background jobs, from the backend imagecelery-beat- schedules recurring syncs, from the backend imagefrontend- the web UI, from the frontend image- PostgreSQL - primary database
- Redis - Celery broker and cache
flower(optional) - Celery monitoring, from the backend imagesvix-server(optional) - only needed when outgoing webhooks are enabled; requires its ownsvixdatabase
backend/config/.env.example documents them. The frontend takes its API URL at runtime as well (see below). Neither image needs a per-environment rebuild.
Example: Compose deployment
A baseline Compose stack using the published images. It uses thelatest tag to stay copy-pasteable; pin an exact version tag like 0.7.0 for real deployments (see Image tags).
docker-compose.yml
Configuration
Copybackend/config/.env.example from the repository to .env next to the compose file. Compose reads the same file for variable interpolation, so DB_PASSWORD is defined once and shared by Postgres and the backend.
The defaults in .env.example are development values. Override at least these for production:
VITE_API_URL is a frontend variable and is not part of .env.example—add it to the same .env. For a local trial of this stack, set it to http://localhost:8000; in production it is typically the same value as API_BASE_URL.
Running it
1
Pull the images
2
Start the stack
app container applies database migrations and seed scripts before the API starts.3
Check service status
Up, with db as Up (healthy).4
Verify the API and frontend
OK once the API is up (migrations can take a moment on first start). Then open http://localhost:3000 in a browser—the login page should load.command: scripts/start/flower.sh and publish port 5555. To enable outgoing webhooks, add a svix-server service and a svix database; the development docker-compose.yml shows the wiring.
Logs
The backend writes its application logs to stdout (inlegacy, uvicorn’s own lines go to stderr, as before). LOG_FORMAT selects their format:
Structured app log lines keep the same fields in
legacy and json: timestamp, level, message, provider, then the attributes of the event (user_id, action, …). Other lines in json carry timestamp, level, message, logger and, for errors, exception.
LOG_LEVEL (DEBUG, INFO, WARNING, ERROR, CRITICAL, case-insensitive) drops log lines below that level, structured app logs included. It cannot lower the level of libraries that are kept quieter on purpose (httpx and httpcore stay at WARNING). When it is not set, other logs start at INFO and structured app logs are not filtered.
LOG_LEVEL also changes what Sentry sees from standard-library log records (logger.error(...) and similar) of app, Celery and uvicorn code: with CRITICAL, their errors no longer create Sentry events, and above INFO their INFO records no longer become breadcrumbs. Libraries with a level of their own (httpx, httpcore) still report to Sentry as before. log_structured lines never reach Sentry, whatever the level, and exceptions captured explicitly (capture_exception) or left unhandled always do. WARN and FATAL are accepted as aliases. Any other value, for example one your platform sets for another tool, is ignored with a warning at startup, as if LOG_LEVEL were not set.
The default will change from
legacy to text in a future release. Set LOG_FORMAT explicitly if you depend on the current output.- startup banners of the FastAPI CLI, Celery workers and Celery beat, and Celery’s
worker: Warm shutdownlines; - uvicorn’s first lines (
Started server process,Waiting for application startup.) and, withENVIRONMENT=local, its reloader messages; - what the start scripts print before the service starts: shell traces, Alembic migrations and the seed scripts;
- raw payloads printed with
RAW_PAYLOAD_STORAGE=log, which ignoreLOG_FORMATandLOG_LEVEL; - Python warnings (
warnings.warn), printed to stderr.
Configuring the frontend API URL at runtime
The frontend resolves its API URL at runtime, so the same published image works against any backend without rebuilding. SetVITE_API_URL as an environment variable on the frontend container. The Nitro server reads it at request time and injects it into the served HTML before the app loads:
VITE_API_URL is not set, it falls back to http://localhost:8000.
VITE_API_URL is a single variable used for both build-time (Vite inlines import.meta.env) and runtime configuration. When the same variable is set both in an .env file and via the container’s environment:, the container environment wins—so runtime always overrides any baked-in value.How it works
Because the frontend is server-rendered (TanStack Start on Nitro), the value travels from the container env to the browser like this:- The Nitro server reads
process.env.VITE_API_URLat request time. - It injects
window.__APP_CONFIG__ = { apiUrl: "..." }into the HTML<head>before the app hydrates. - The API client reads that value. (Resolution order: injected runtime value →
VITE_API_URLbaked at build →http://localhost:8000.)
frontend/src/lib/api/runtime-config.ts.
Related Guides
- Deploy to Railway — One-click managed deployment
- Raw Payloads Storage — Backend runtime configuration example

