Files
baya-monorepo/DEPLOY.md
T

7.2 KiB

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. 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 <UserSecretsId> 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.ymlapi.environment
Client build-time config (API URL, site origin) client/.env.production
Telegram relay config (bot token, chat ids, API key, proxy) docker-compose.ymlotp-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 / EncryptkeyStartupSecretsGuard 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:Providerkavenegar, 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.

docker network ls | grep caddy_net

2. Add the Balinyaar block to your Caddyfile

Copy from deploy/Caddyfile into the Caddyfile your Caddy container already loads:

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:

docker exec <caddy-container> 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

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

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

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.

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