> ## Documentation Index
> Fetch the complete documentation index at: https://openwearables.io/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# Deploying with Docker

> Run Open Wearables in production from the published Docker images

## Published images

Open Wearables publishes two production images to Docker Hub:

| Image                                                                                                 | Service                                          | Port   |
| ----------------------------------------------------------------------------------------------------- | ------------------------------------------------ | ------ |
| [`themomentum/open-wearables-backend`](https://hub.docker.com/r/themomentum/open-wearables-backend)   | FastAPI API + Celery worker/beat/flower          | `8000` |
| [`themomentum/open-wearables-frontend`](https://hub.docker.com/r/themomentum/open-wearables-frontend) | React frontend (TanStack Start, served by Nitro) | `3000` |

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:

| Command                   | Role                                                                     |
| ------------------------- | ------------------------------------------------------------------------ |
| `scripts/start/app.sh`    | API server. Also applies database migrations and seed scripts on startup |
| `scripts/start/worker.sh` | Celery worker (data syncs, background jobs)                              |
| `scripts/start/beat.sh`   | Celery beat (scheduler)                                                  |
| `scripts/start/flower.sh` | Flower (Celery monitoring UI, port `5555`)                               |

## 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](/docs/api-reference/guides/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.

<Warning>
  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](/docs/deployment/railway).
</Warning>

## 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.

```yaml docker-compose.yml theme={null}
services:
  db:
    image: postgres:18
    environment:
      POSTGRES_DB: ${DB_NAME:-open-wearables}
      POSTGRES_USER: ${DB_USER:-open-wearables}
      POSTGRES_PASSWORD: ${DB_PASSWORD:?Set DB_PASSWORD in .env}
    volumes:
      - postgres_data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 5s
      timeout: 5s
      retries: 5
    restart: unless-stopped

  redis:
    image: redis:8
    command: ["redis-server", "--appendonly", "yes"]
    volumes:
      - redis_data:/data
    restart: unless-stopped

  app:
    image: themomentum/open-wearables-backend:latest
    command: scripts/start/app.sh
    env_file:
      - ./.env
    environment:
      - DB_HOST=db
      - REDIS_HOST=redis
    ports:
      - "8000:8000"
    depends_on:
      db:
        condition: service_healthy
      redis:
        condition: service_started
    restart: unless-stopped

  celery-worker:
    image: themomentum/open-wearables-backend:latest
    command: scripts/start/worker.sh
    env_file:
      - ./.env
    environment:
      - DB_HOST=db
      - REDIS_HOST=redis
    depends_on:
      - app
    restart: unless-stopped

  celery-beat:
    image: themomentum/open-wearables-backend:latest
    command: scripts/start/beat.sh
    env_file:
      - ./.env
    environment:
      - DB_HOST=db
      - REDIS_HOST=redis
    depends_on:
      - app
    restart: unless-stopped

  frontend:
    image: themomentum/open-wearables-frontend:latest
    environment:
      - VITE_API_URL=${VITE_API_URL:?Set VITE_API_URL in .env}
    ports:
      - "3000:3000"
    restart: unless-stopped

volumes:
  postgres_data:
  redis_data:
```

### 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:

| Variable         | Set to                                                       |
| ---------------- | ------------------------------------------------------------ |
| `ENVIRONMENT`    | `production`                                                 |
| `DB_PASSWORD`    | a strong database password                                   |
| `SECRET_KEY`     | a long random secret, used to sign tokens                    |
| `ADMIN_PASSWORD` | password for the admin account seeded on first start         |
| `CORS_ORIGINS`   | your frontend origin, e.g. `["https://app.example.com"]`     |
| `FRONTEND_URL`   | public URL of the frontend                                   |
| `API_BASE_URL`   | public URL of the API, used for provider OAuth redirect URIs |
| `VITE_API_URL`   | URL of the API as reached from the browser                   |

`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

<Steps>
  <Step title="Pull the images">
    ```bash theme={null}
    docker compose pull
    ```
  </Step>

  <Step title="Start the stack">
    ```bash theme={null}
    docker compose up -d
    ```

    The `app` container applies database migrations and seed scripts before the API starts.
  </Step>

  <Step title="Check service status">
    ```bash theme={null}
    docker compose ps
    ```

    All services should report `Up`, with `db` as `Up (healthy)`.
  </Step>

  <Step title="Verify the API and frontend">
    ```bash theme={null}
    curl -fsS http://localhost:8000/openapi.json > /dev/null && echo OK
    ```

    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.
  </Step>
</Steps>

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:

```bash theme={null}
docker run -p 3000:3000 \
  -e VITE_API_URL=https://api.client1.com \
  themomentum/open-wearables-frontend:latest
```

The same image with a different value points at a different backend—no rebuild:

```bash theme={null}
docker run -p 3000:3000 \
  -e VITE_API_URL=https://api.client2.com \
  themomentum/open-wearables-frontend:latest
```

If `VITE_API_URL` is not set, it falls back to `http://localhost:8000`.

<Note>
  `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.
</Note>

### 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`.

## Related Guides

* [Deploy to Railway](/docs/deployment/railway) — One-click managed deployment
* [Raw Payloads Storage](/docs/dev-guides/raw-payload-storage) — Backend runtime configuration example
