Skip to main content

Published images

Open Wearables publishes two production images to Docker Hub: Images are published manually through a GitHub Actions workflow. Every publish pushes a short commit SHA tag (sha-<commit>); runs from main also update latest. Pin a SHA tag in deployments to get repeatable builds. The single backend image covers every backend role. The start command selects what the container runs:

What a deployment runs

A typical deployment consists of the application containers plus their backing services:
  • app - the API, from the backend image
  • celery-worker - background jobs, from the backend image
  • celery-beat - schedules recurring syncs, from the backend image
  • frontend - the web UI, from the frontend image
  • PostgreSQL - primary database
  • Redis - Celery broker and cache
  • flower (optional) - Celery monitoring, from the backend image
  • svix-server (optional) - only needed when outgoing webhooks are enabled; requires its own svix database
The backend reads all of its configuration from environment variables at runtime; 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.
The docker-compose.yml in the repository root is a development setup: it builds images from local source, hardcodes database credentials, and wires up hot reload via docker compose watch. Do not use it as a production template. For deployment, run the published images with your own orchestration—a Compose file you maintain, Kubernetes, or a managed platform such as Railway.

Example: Compose deployment

A baseline Compose stack using the published images. It uses the latest tag to stay copy-pasteable; pin sha-<commit> tags for real deployments.
docker-compose.yml

Configuration

Copy backend/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

The app container applies database migrations and seed scripts before the API starts.
3

Check service status

All services should report Up, with db as Up (healthy).
4

Verify the API and frontend

Prints 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.
In practice you will put the API and frontend behind a reverse proxy with TLS instead of exposing the container ports directly. To add Flower, run another backend container with 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.

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. Set VITE_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:
The same image with a different value points at a different backend—no rebuild:
If 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:
  1. The Nitro server reads process.env.VITE_API_URL at request time.
  2. It injects window.__APP_CONFIG__ = { apiUrl: "..." } into the HTML <head> before the app hydrates.
  3. The API client reads that value. (Resolution order: injected runtime value → VITE_API_URL baked at build → http://localhost:8000.)
The logic lives in frontend/src/lib/api/runtime-config.ts.