# DriverVault — Docker (multi-container stack)

Three containers — **PocketBase**, **API Server**, **Web App** — on one compose
network. This is the deployment to use unless you specifically want everything
in a single image; for that see [`../Docker-AIO`](../Docker-AIO).

```
Browser ─► Web App BFF (:8090) ──/api/*──► API Server (:8080) ─► PocketBase (:8070)
```

Only the Web App port is meant to be public. The API Server and the PocketBase
admin UI are published for convenience and, in the prod file, bound to
`127.0.0.1` by default.

| File | Use |
|---|---|
| `docker-compose.yml` | **builds from source** in this repo — for development and local testing |
| `docker-compose.prod.yml` | **pulls prebuilt images** from the registry — for deployment |
| `.env.example` / `.env.prod.example` | copy to `.env` for the matching compose file |
| `pocketbase/` | the PocketBase image (official release binary on alpine) |

## Run it

```bash
cd Docker
cp .env.example .env        # then edit — PB_ADMIN_* have no safe defaults
docker compose up -d --build
```

Production, from the registry:

```bash
cp .env.prod.example .env   # then edit
docker compose -f docker-compose.prod.yml pull
docker compose -f docker-compose.prod.yml up -d
```

Then: web app on `http://host:8090/`, the API Server's superadmin panel on
`http://host:8080/`, PocketBase admin on `http://host:8070/_/`.

## First boot

Both steps are idempotent, so restarts and upgrades are safe:

1. **PocketBase** upserts its superuser from `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD`.
   This is the only way to create the first superuser — the REST API cannot
   bootstrap it. The API Server then authenticates with the same credentials.
2. **The API Server** creates any missing collections and reconciles existing
   ones, then creates the first app `superadmin` from
   `DRIVERVAULT_SUPERADMIN_EMAIL` / `_PASSWORD` if no such user exists.

> Leave `PB_BOOTSTRAP` at `true`, including across upgrades. A release can add
> collections or fields the server needs, and a stack that skipped the bootstrap
> never gets them. The API Server self-heals exactly one thing — `app_settings`,
> the collection holding the plugin settings, which it creates on demand because
> it cannot serve the plugin panel without it. Every other schema change still
> depends on this flag, so turn it off only for a database you know already
> matches the release you are running.

No manual `setup-pocketbase.mjs` step is needed here — the server runs the same
schema reconcile itself.

## Volumes

| Volume | Holds |
|---|---|
| `pb_data` | the PocketBase SQLite database and uploaded files — everything that persists |

One volume, because the API Server keeps no state on disk. Plugin enable-state
and global config live in the database like the rest of the settings, so backing
up `pb_data` backs up the whole stack. It defaults to a Docker-managed named
volume; set `PB_DATA` to an absolute host path in the prod file for a bind mount
instead. PocketBase runs as root, so a root-owned host directory is fine.

> One thing does **not** persist: the API Server panel's *Settings → PocketBase*
> and *Settings → Web App* screens apply immediately but only for the life of the
> container. Set `POCKETBASE_URL`, `PB_ADMIN_EMAIL` / `PB_ADMIN_PASSWORD`,
> `WEBAPP_URL` and `CORS_ALLOW_ORIGINS` in `.env` to change them permanently —
> in this stack the compose environment wins over anything the panel writes.

## Charger control (OCPP)

Chargers in own/proxy mode dial in to `/ocpp/{serial}` **on the API Server
port**, authenticating with a per-charger control token in an OCPP Basic-auth
header. A plaintext `ws://` would put that token on the wire in the clear, so
`OCPP_REQUIRE_TLS` defaults to `true` and non-TLS connections are rejected.

This stack serves plain HTTP, so to actually use charger control you need to
terminate TLS in a reverse proxy in front of it and set `OCPP_PUBLIC_URL` to the
public `wss://` base (behind a proxy, deriving it from request headers is
unreliable). `OCPP_REQUIRE_TLS=false` is for trusted networks only. You will
also need `API_BIND` set so the proxy can reach the port.

## Notes

- `docker-compose.yml` builds the API Server and Web App from `../API Server`
  and `../Web App`, so run it from this directory with the repo checked out.
- The Web App's Vue bundle is built with an empty `VITE_API_BASE`, so the
  browser uses same-origin `/api` and the BFF proxies it — no CORS in play.
- `CORS_ALLOW_ORIGINS` therefore only matters if a browser calls the API Server
  directly. Native mobile apps are not subject to CORS at all.
