# Deploying Balinyaar A first, shareable deployment of the whole stack under **balinyaar.ir**, in Docker, behind an existing Caddy reverse proxy that terminates TLS. | Host | Serves | Container | | --- | --- | --- | | `balinyaar.ir`, `www.balinyaar.ir` | Next.js web client | `balinyaar-web:3000` | | `api.balinyaar.ir` | ASP.NET Core API | `balinyaar-api:8080` | | *(internal only)* | Telegram OTP relay | `balinyaar-otp-relay:5010` | The **database is not containerised** — it is the remote SQL Server already configured in [server/src/API/Baya.Web.Api/appsettings.Development.json](server/src/API/Baya.Web.Api/appsettings.Development.json). Nothing needs to be provisioned for it; the API just needs network reach to `87.107.152.16:1433`. --- ## Configuration model **There is no `dotnet user-secrets` any more.** The `` was removed from `Baya.Web.Api.csproj`, so the API no longer reads that store at all — a stale `secrets.json` on a dev machine is now inert and can be deleted. Every value lives in a file in the repo: | What | Where | | --- | --- | | API config + secrets (DB, JWE keys, field-encryption keys, Telegram key, CORS, trusted proxies) | `server/src/API/Baya.Web.Api/appsettings.Development.json` | | The two values that differ between a laptop and the container network | `docker-compose.yml` → `api.environment` | | Client build-time config (API URL, site origin) | `client/.env.production` | | Telegram relay config (bot token, chat ids, API key, proxy) | `docker-compose.yml` → `otp-relay.environment` | The API runs as **`ASPNETCORE_ENVIRONMENT=Development`**, so `appsettings.Development.json` is the file that actually loads. An `appsettings.Production.json` would be ignored — put changes in the Development file, or change the environment name first. The relay's shared secret appears twice and the two must match: `Seams:Sms:Telegram:ApiKey` in the appsettings file and `API_KEY` in the compose file. It was rotated away from the value in `telegram-otp-bot/.env.example`, which is published in git and in that project's README — `TelegramSmsSender` now refuses to authenticate with it. **If you run the relay locally**, copy the appsettings value into your own `telegram-otp-bot/.env`. > ⚠️ **`Seams:FieldEncryption:Key` and `:HashKey` must never change.** Every encrypted column in that > database — phone numbers, addresses, IBANs, clinical notes — was written with those exact values, and > `users.PhoneHash`, which every login looks up, is derived from `HashKey`. Rotating either makes the > existing data unreadable and locks every account out. The JWE keys (`IdentitySettings:SecretKey` / > `Encryptkey`) are safe to rotate; doing so only signs everyone out. --- ## What running as Development means This was a deliberate choice so the demo and lifecycle seeders populate the shared database and the screens aren't empty. It has real consequences, all of which are fine for a pre-launch demo among people you trust, and none of which are acceptable once strangers can reach the site: - **The developer exception page is public.** Any unhandled 500 on `api.balinyaar.ir` returns a stack trace and configuration detail to the caller. - **`GET /api/v1/dev/last_otp/{phone}` is live.** Anyone who knows a registered phone number can read its login code and sign in as that user. This is the single biggest exposure. - **Swagger is served** at `api.balinyaar.ir/swagger`. - **The seeders re-run on every container boot** (idempotent, so this is safe — they no-op on data that already exists) and **migrations auto-apply on boot** rather than as a separate step. - **gRPC reflection is enabled**, and the demo `bookings/convert` payment-capture simulator is wired. ### Going to Production later 1. Set `ASPNETCORE_ENVIRONMENT: Production` in `docker-compose.yml`. 2. Create `appsettings.Production.json` with the same content as the Development file, but with **real** `IdentitySettings:SecretKey` / `Encryptkey` — `StartupSecretsGuard` rejects anything containing `not-for-production` outside Development, so the current dev keys will refuse to boot (by design). Keep `Seams:FieldEncryption` byte-identical. 3. Run migrations as a one-shot instead of on boot: `docker compose run --rm api dotnet Baya.Web.Api.dll migrate` 4. Swap the OTP rail: `Seams:Sms:Provider` → `kavenegar`, with `Seams:Sms:ApiKey`/`Sender` filled in. The Telegram relay broadcasts every code to a fixed recipient list, which stops being acceptable the moment someone outside that list can request one. --- ## First deploy ### 1. Confirm the Caddy network exists The compose file joins `caddy_net` as an **external** network — it does not create it. ```bash docker network ls | grep caddy_net ``` ### 2. Add the Balinyaar block to your Caddyfile Copy from [deploy/Caddyfile](deploy/Caddyfile) into the Caddyfile your Caddy container already loads: ```caddyfile balinyaar.ir, www.balinyaar.ir { encode zstd gzip reverse_proxy balinyaar-web:3000 } api.balinyaar.ir { encode zstd gzip reverse_proxy balinyaar-api:8080 } ``` Caddy obtains and renews the certificates for both hostnames itself. Reload it: ```bash docker exec caddy reload --config /etc/caddy/Caddyfile ``` ### 3. Point DNS at the host `balinyaar.ir`, `www.balinyaar.ir` and `api.balinyaar.ir` all need an A record on the server's public IP **before** Caddy can complete the ACME challenge. ### 4. Confirm the proxy container is up The relay's hop to `api.telegram.org` is filtered in Iran and goes out through the proxy already on `caddy_net`, configured as `TELEGRAM_PROXY_URL: http://hysteria-client:8081`. If that container has a different name or port, change it in `docker-compose.yml` — a wrong value fails the relay at boot with a clear message rather than silently per-OTP. ### 5. Build and start ```bash docker compose up -d --build docker compose ps docker compose logs -f api ``` The API's first boot applies any pending migrations and runs the seeders against the remote database, so it takes noticeably longer than later ones. --- ## Verifying ```bash curl https://api.balinyaar.ir/healthz/live # process is up curl https://api.balinyaar.ir/healthz/ready # + database and object storage reachable curl -I https://balinyaar.ir # the public landing page docker compose logs otp-relay | head # should print the bot's @username and the proxy label ``` A full login round-trip is the real check: request an OTP from the site and confirm the code arrives in the Telegram chat. If it doesn't, `docker compose logs otp-relay` names the failing hop — a proxy error and a Telegram API rejection look different. --- ## Redeploying ```bash git pull docker compose up -d --build ``` Rebuild the client whenever a `NEXT_PUBLIC_*` value in `client/.env.production` changes — those are compiled into the browser bundle, so restarting the container alone changes nothing. ## Known wrinkle: the client lockfile is Windows-generated `client/package-lock.json` is produced on Windows, where npm filters out wasm32-only optional packages and therefore never records their transitive dependencies (`@emnapi/core`, `@emnapi/runtime`). On Linux npm *does* want them, so a bare `npm ci` fails with: ``` npm error `npm ci` can only install packages when your package.json and package-lock.json ... are in sync. npm error Missing: @emnapi/runtime@1.11.3 from lock file ``` The client Dockerfile works around this by completing the lock inside the image before installing. To fix it permanently, regenerate the lock **on Linux** once and commit the result: ```bash cd client docker run --rm -v "$PWD:/app" -w /app node:24-alpine npm install --package-lock-only --no-audit --no-fund ``` Then drop the `npm install --package-lock-only` line from `client/Dockerfile`, leaving just `npm ci`. Note `--omit=optional` is **not** a valid shortcut here: Turbopack resolves `@parcel/watcher`'s native binary through `optionalDependencies`, so omitting them breaks `next build` with `No prebuild or local build of @parcel/watcher found`. ## Persisted state Two named volumes survive rebuilds. Uploaded verification documents live in the first one; losing it means the admin verification queue shows broken documents. | Volume | Holds | | --- | --- | | `api-object-storage` | Uploaded verification documents (local-disk `IObjectStorage` seam) | | `api-logs` | Serilog JSON file sink |