From c6a4357c6fd72aba537c3d8fd42e567e6109e737 Mon Sep 17 00:00:00 2001 From: Alexa <53471801+Alexa-RR@users.noreply.github.com> Date: Mon, 15 Jun 2026 23:35:30 +0300 Subject: [PATCH] Update docs for microservices architecture --- DorkNet.AdminMobile/README.md | 5 + DorkNet.Server/Data/MIGRATIONS.md | 11 +- README.md | 18 +- docs/architecture.md | 55 ++-- docs/deploy.md | 471 +++++++++++++++++------------- tools/cloudflared.README.md | 64 ++-- 6 files changed, 363 insertions(+), 261 deletions(-) diff --git a/DorkNet.AdminMobile/README.md b/DorkNet.AdminMobile/README.md index 240490a..dabe05a 100644 --- a/DorkNet.AdminMobile/README.md +++ b/DorkNet.AdminMobile/README.md @@ -2,6 +2,11 @@ Native Android/iOS admin app for DorkNet, built with .NET MAUI. +In the microservices deployment, point the app at the public admin host +for the stack, usually `https://admin.`. Cloudflare and +`DorkNet.Gateway` keep the public URL stable while admin API requests are +routed to the owning backend slice. + ## Authentication The app stores sensitive values in platform secure storage: diff --git a/DorkNet.Server/Data/MIGRATIONS.md b/DorkNet.Server/Data/MIGRATIONS.md index 8edf389..ebc7fd8 100644 --- a/DorkNet.Server/Data/MIGRATIONS.md +++ b/DorkNet.Server/Data/MIGRATIONS.md @@ -6,8 +6,9 @@ (generated by `dotnet ef migrations add Initial` against the current entity model — captures all 67 `DbSet<>` tables). - **`LegacyUpgrades.cs`** holds idempotent data transforms that can't - be derived from the entity model. Runs once per server boot, after - EF migrations complete and before `RoomService.SeedAsync()`. + be derived from the entity model. Runs during host boot for the + standalone server and for service-slice containers, after schema + bootstrap completes and before `RoomService.SeedAsync()`. ## What changed in the consolidation @@ -37,8 +38,8 @@ Both were collapsed in 2026-05-25: ## Production cutover (one-time SQL) Existing prod databases need a one-time fixup to tell EF "the new -Initial is already applied here" so the next boot doesn't try to -`CREATE TABLE` over existing data. +Initial is already applied here" so the next standalone or service-slice +boot doesn't try to `CREATE TABLE` over existing data. ### Postgres path @@ -48,7 +49,7 @@ table exists** because EF Core only creates it when `Migrate()` runs. Postgres continues to use `EnsureCreated()` post-consolidation, so there's *nothing to do* — `EnsureCreated()` reads the entity model, sees the schema already matches, and no-ops. Just deploy the new -build. +standalone image or microservices compose stack. If you ever switch the Postgres path to `Migrate()`, run this once before the cutover deploy: diff --git a/README.md b/README.md index 3055f4b..121d1d5 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,7 @@ main also auto-detects your install. | **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` | | **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` | | **Schema** | One EF `Initial` migration; SQLite + Postgres both work | -| **Runtime topology** | Standalone `DorkNet.Server`, plus gateway-fronted service slices with monolith fallback | +| **Runtime topology** | Gateway-fronted microservices with dedicated service slices and a monolith fallback | | **Diverges from `march-2020-03-10` by** | ~170 files | Subsystems present on this branch that the March branch doesn't have: @@ -72,7 +72,7 @@ cd DorkNet | `S3:SecretKey` | `S3__SecretKey` | production | S3 secret key | | `S3:Region` | `S3__Region` | no | defaults to `garage`; use `auto` for R2 | -**4. Boot:** +**4. Boot a local standalone server:** ```pwsh dotnet run --project DorkNet.Server @@ -84,7 +84,8 @@ categories / playlists, and starts listening. You should see `Application started.` in the log; `curl http://localhost:8080/healthz` returns 200. -For Docker, Dokploy, and microservices deploy notes, see +For the production microservices stack, Docker, and Dokploy deploy +notes, see [docs/deploy.md](docs/deploy.md). --- @@ -199,7 +200,7 @@ Default `0` (off). | Folder | Purpose | |---|---| -| `DorkNet.Server/` | ASP.NET Core backend (controllers, services, data, hubs, middleware) | +| `DorkNet.Server/` | Shared ASP.NET Core controller/service stack, standalone fallback, monolith fallback | | `DorkNet.Models/` | DTOs shared between server and client mod | | `DorkNet.Contracts/` | Shared service names, route ownership, service-map options, and health/probe contracts | | `DorkNet.ServiceDefaults/` | Shared service health, JSON, HTTP client, service identity, and route-guard setup | @@ -207,7 +208,7 @@ Default `0` (off). | `DorkNet.Services.*` | Gateway-fronted service hosts for identity, rooms, notify, content, social, commerce, platform, moderation, and web | | `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher | | `tools/` | Installers (`install-melon.ps1`, `install-legacy-client.ps1`, `remove-eac.ps1`), Cloudflare tunnel templates | -| `Dockerfile`, `Dockerfile.service`, `docker-compose.microservices*.yml` | Full-server, local microservices, and Dokploy microservices deployment entrypoints | +| `Dockerfile`, `Dockerfile.service`, `docker-compose.microservices*.yml` | Standalone fallback image, reusable service image, and local/Dokploy microservices entrypoints | See [docs/architecture.md](docs/architecture.md) for the full mental model (project layout, request lifecycle, watch-mirror controller @@ -215,6 +216,7 @@ pattern, where to start contributing). ### Microservices +The production topology is the gateway-fronted compose stack. `docker-compose.microservices.yml` starts the gateway, identity, rooms, notify, content, social, commerce, platform, moderation, web, and monolith fallback service hosts plus Compose-managed Postgres and Redis @@ -224,6 +226,7 @@ can route the apex and wildcard hostnames to the gateway without Dokploy domain rows. The gateway routes owned host/path slices to the dedicated service hosts. +The `web` service owns the apex, `www`, `admin`, and `feed` static hosts. The monolith fallback is still present for unknown route families, so public client URLs remain stable while the split continues. @@ -238,8 +241,9 @@ $env:DORKNET_S3_REGION="garage" # or auto for R2 docker compose -f docker-compose.microservices.yml up --build ``` -For deployment details, including Dokploy env vars and the current -monolith-vs-microservices split, see [docs/deploy.md](docs/deploy.md). +For deployment details, including Dokploy env vars, Cloudflare Tunnel +routing, external Postgres, and S3 setup, see +[docs/deploy.md](docs/deploy.md). --- diff --git a/docs/architecture.md b/docs/architecture.md index c9c1b0b..d254f5d 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -14,7 +14,7 @@ files in this folder. ``` DorkNet.sln -├── DorkNet.Server ASP.NET Core 9 backend — full public API today +├── DorkNet.Server Shared ASP.NET Core backend + monolith fallback ├── DorkNet.Models Shared DTO + serialization types ├── DorkNet.Contracts Shared service contracts for the service split ├── DorkNet.ServiceDefaults Shared service-host setup @@ -33,7 +33,7 @@ DorkNet.sln | Project | Responsibility | Target | |---|---|---| -| `DorkNet.Server` | REST API, SignalR hub, admin SPA host, static-site host, image CDN, matchmaking, persistence | .NET 9 | +| `DorkNet.Server` | Shared REST/controller/service stack, database bootstrap, standalone fallback, monolith fallback | .NET 9 | | `DorkNet.Models` | DTOs the wire protocol speaks. No project dependencies. | .NET 9 | | `DorkNet.Contracts` | Service names, route ownership, service-map options, health responses, probe responses | .NET 9 | | `DorkNet.ServiceDefaults` | Common service health checks, JSON defaults, HTTP clients, service identity, route guard | .NET 9 | @@ -78,12 +78,13 @@ DorkNet.Server/ ## Runtime topology -`DorkNet.Server` remains the compatibility fallback and full standalone -runtime. It can still run by itself, serving the full public -Rec Room-compatible HTTP surface, SignalR hub, static hosts, database -bootstrap, Redis-backed ephemeral state, and S3-backed blob storage. +The production runtime is the gateway-fronted service split. +`DorkNet.Server` remains available as the shared compatibility runtime +and as a standalone debug/fallback image. In the compose stack it runs as +`monolith` behind the gateway for route families that have not been +assigned to a dedicated service yet. -The microservice shape is a gateway-fronted split: +The service split works like this: - `DorkNet.Gateway` reverse-proxies public requests by host/path. - `DorkNet.Services.Identity`, `DorkNet.Services.Rooms`, @@ -93,9 +94,6 @@ The microservice shape is a gateway-fronted split: `DorkNet.Services.Web` run the shared server stack behind a route ownership guard. That keeps response shapes identical while moving traffic by domain/path slice. -- `DorkNet.Server` runs as `monolith` behind the gateway for unknown - route families that are not assigned to a dedicated service yet. - - `DorkNet.Gateway` exposes `/internal/services` and `/internal/services/health`, plus `/internal/routes` for the active proxy table. @@ -108,12 +106,18 @@ The microservice shape is a gateway-fronted split: `docker-compose.microservices.yml` starts gateway, all dedicated service slices, the monolith fallback, and Compose-managed Postgres and Redis for -local testing. -`docker-compose.microservices.dokploy.yml` is the Dokploy shape: it adds -a `cloudflared` sidecar that joins the same Compose network and forwards -Cloudflare Tunnel traffic to `http://gateway:8080`. Object storage is -external S3-compatible storage configured with `DORKNET_S3_*` -environment variables; the compose files do not run MinIO or Garage. +local testing. `docker-compose.microservices.dokploy.yml` is the Dokploy +production shape: it adds a `cloudflared` sidecar that joins the same +Compose network and forwards Cloudflare Tunnel traffic to +`http://gateway:8080`. Object storage is external S3-compatible storage +configured with `DORKNET_S3_*` environment variables; the compose files +do not run MinIO or Garage. + +`Dockerfile.service` is the reusable service image. It accepts +`PROJECT_PATH` and `APP_DLL` build args, rebuilds the admin/site Vite +SPAs for service images that need static assets, publishes the selected +.NET project, and runs that DLL in the runtime image. The root +`Dockerfile` builds only the standalone/fallback `DorkNet.Server` image. ### Controllers/ groupings @@ -254,11 +258,14 @@ Two providers, switched by `Database:Provider` config: - **SQLite** — default for local dev. File at `bin//data/dorknet.db`. - **Postgres** — production. Connection string from `ConnectionStrings:Default`. -Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`. EF -migrations under `Data/Migrations/` apply via `Database.Migrate()` on -boot. Data transforms that can't be expressed as schema migrations -(seed renames, computed backfills) go in `Data/LegacyUpgrades.cs` as -idempotent methods registered in `RunAsync()`. See +Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`. +SQLite dev DBs apply EF migrations under `Data/Migrations/` via +`Database.Migrate()`. The production Postgres path still uses +`EnsureCreated()` plus idempotent bootstrap work under the same +advisory-lock discipline. Data transforms that can't be expressed as +schema migrations (seed renames, computed backfills) go in +`Data/LegacyUpgrades.cs` as idempotent methods registered in `RunAsync()`. +See [`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md) for the discipline: one release = one migration; everything else is `LegacyUpgrades`. @@ -280,9 +287,11 @@ If you've never touched this codebase, the most approachable path: 4. Look at one **`Cpp2IL`-cited line** in a controller to see how wire shapes are pinned to decompiled provenance. -When in doubt: open `DorkNet.Server/Program.cs` and the +When in doubt: open `DorkNet.Contracts/DorkNetRouteOwnership.cs`, +`DorkNet.Gateway/Program.cs`, `DorkNet.Server/Program.cs`, and the `Versions/Late2020/Late2020VersionPlugin.cs` summary doc-comment. -Between them they explain 80% of the cross-cutting decisions. +Together they explain route ownership, proxying, the shared server +pipeline, and version compatibility. --- diff --git a/docs/deploy.md b/docs/deploy.md index a16547d..51a44fb 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -1,78 +1,154 @@ # DorkNet Deployment -This branch has two deployable shapes: +The recommended production shape is the gateway-fronted microservices +compose stack. The standalone `DorkNet.Server` image still exists for +local debugging and emergency single-container fallback, but new Dokploy +deployments should use `docker-compose.microservices.dokploy.yml`. -- **Production/full API:** build the root `Dockerfile`, which runs - `DorkNet.Server`. This is still the runtime that serves every public - Rec Room-compatible endpoint. -- **Microservices:** deploy `docker-compose.microservices.yml` - for local testing or `docker-compose.microservices.dokploy.yml` on - Dokploy. Both start `DorkNet.Gateway`, the dedicated service slices, - the `DorkNet.Server` monolith fallback, Postgres, and Redis. The - gateway routes public traffic to owned service slices and sends - unknown route families to the fallback. - -Object storage is always a **separate S3-compatible instance**. The -microservices compose files do not start MinIO/Garage. +Object storage is always a separate S3-compatible service. The compose +files start Postgres and Redis, but they do not start MinIO, Garage, or +any other object-storage emulator. --- -## Dokploy: full server +## Production Topology -Use this for a real server today. +Public traffic enters at `DorkNet.Gateway`. The gateway preserves the +original Host header, routes owned host/path families to dedicated +service hosts, and sends unassigned route families to the `monolith` +fallback. That lets the 2020 client keep using the same public URLs +while route slices move out of the fallback server. -Create a Dokploy **Application**: - -| Field | Value | +| Service | Purpose | |---|---| -| Source | This GitHub repo / branch | -| Build type | Dockerfile | -| Dockerfile path | `Dockerfile` | -| Context | `.` | -| Container port | `8080` | +| `gateway` | Edge reverse proxy, service map, route table, and service health probes | +| `identity` | Auth, accounts, platform login, JWT issuance | +| `rooms` | Rooms, room keys, playlists, matchmaking, discovery | +| `notify` | Notify, messages/chat, SignalR, notification fan-out | +| `content` | CDN paths, uploads, images, photos, room blobs, storage APIs | +| `social` | Clubs, groups, announcements, player events, subscriptions | +| `commerce` | Catalog, storefronts, econ, inventory, inventions | +| `platform` | Service directory, config, version checks, geo, strings | +| `moderation` | Bug reports, player reports, sanitize, admin API, testcase routes | +| `web` | Apex/www/admin/feed static hosts and site API | +| `monolith` | Fallback shared server for route families not split yet | +| `postgres` | Bundled Postgres for the compose network | +| `redis` | Bundled Redis for ephemeral state and fan-out | +| `cloudflared` | Cloudflare Tunnel sidecar in the Dokploy compose file | -Set application environment variables: +Every backend service uses the same database, Redis, S3, Photon, domain, +JWT, and version-gate settings. The dedicated slices currently reuse the +shared `DorkNet.Server` controller/service stack behind route ownership +guards, so response contracts stay identical while the architecture is +split. + +--- + +## Dokploy Microservices + +Create a Dokploy **Compose** service from: + +```text +docker-compose.microservices.dokploy.yml +``` + +Use the GitHub repo/branch as the source and keep the build context at +the repository root. Do not add Dokploy domain rows for this compose +stack when using the Cloudflare Tunnel sidecar; public hostname routing +belongs to Cloudflare. + +Set these Dokploy Compose environment variables: ```env -ASPNETCORE_ENVIRONMENT=Production -ASPNETCORE_URLS=http://0.0.0.0:8080 -DOTNET_RUNNING_IN_CONTAINER=true - DORKNET_DOMAIN=yourdomain.com DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters +CLOUDFLARE_TUNNEL_TOKEN= -Database__Provider=postgres -ConnectionStrings__Default=Host=;Port=5432;Database=dorknet;Username=;Password= -ConnectionStrings__Redis=redis://:6379 +# Bundled compose Postgres. Optional when DORKNET_POSTGRES_CONNECTION_STRING is set. +POSTGRES_PASSWORD=replace-with-a-random-db-password -S3__Endpoint=https://your-s3-api-endpoint -S3__AccessKey=your-access-key -S3__SecretKey=your-secret-key -S3__Region=garage +# External object storage. Required for production. +DORKNET_S3_ENDPOINT=https://your-s3-api-endpoint +DORKNET_S3_ACCESS_KEY=your-access-key +DORKNET_S3_SECRET_KEY=your-secret-key +DORKNET_S3_REGION=garage +# Photon Cloud. Photon__AppId=your-photon-realtime-app-id Photon__VoiceAppId=your-photon-voice-app-id Photon__CloudRegion=eu -DorkNet__DefaultClientVersion=december_2020_12_18 -DorkNet__SupportedVersions__0=december_2020_12_18 -DorkNet__BuildIdToVersionKey__20201210=december_2020_12_18 + +# December 2020 client version gate. +DORKNET_DEFAULT_CLIENT_VERSION=december_2020_12_18 +DORKNET_SUPPORTED_VERSION=december_2020_12_18 +DORKNET_DECEMBER_BUILD_VERSION_KEY=december_2020_12_18 ``` -If Postgres and Redis are Dokploy-managed services in the same project, -use the internal service hostnames Dokploy gives you. For a Compose-style -network the values usually look like: +The compose file maps the `DORKNET_S3_*` values into `S3__*` for every +backend service, and maps the version variables into `DorkNet__*` +configuration keys. `Photon__*` is passed directly to each service. + +### Database Options + +By default, service containers connect to the bundled compose Postgres: ```env -ConnectionStrings__Default=Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=dorknet_dev -ConnectionStrings__Redis=redis://redis:6379 +Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD} ``` -### Domains +To use a Dokploy-managed or external Postgres instead, set a full +connection string: -Point DNS at the Dokploy server, then add domains to the same -application. Every host routes to container port `8080`. +```env +DORKNET_POSTGRES_CONNECTION_STRING=Host=;Port=5432;Database=dorknet;Username=;Password= +``` -Add these hosts for full client compatibility: +For hosted Postgres that requires TLS: + +```env +DORKNET_POSTGRES_CONNECTION_STRING=Host=;Port=5432;Database=dorknet;Username=;Password=;SSL Mode=Require;Trust Server Certificate=true +``` + +`docker-compose.microservices.dokploy.yml` attaches every backend service +that opens database connections to both the stack's default network and +Dokploy's external `dokploy-network`. If you use another Dokploy-managed +database service, the hostname in `DORKNET_POSTGRES_CONNECTION_STRING` +must resolve to a Docker-network address from inside a DorkNet service: + +```bash +docker exec getent hosts +``` + +That should return a container/network IP, not `127.0.0.1`. + +If you use the bundled compose Postgres, `POSTGRES_PASSWORD` must match +the password that initialized the existing Postgres volume. Changing +`POSTGRES_PASSWORD` later does not update the already-created `dorknet` +database user. Restore the original password, reset it inside Postgres, +or recreate the Postgres volume. + +### Cloudflare Tunnel + +In Cloudflare Zero Trust, configure two public hostnames: + +```text +yourdomain.com -> http://gateway:8080 +*.yourdomain.com -> http://gateway:8080 +``` + +The wildcard covers `api`, `auth`, `rooms`, `notify`, `cdn`, `clubs`, +`commerce`, and the other client-facing subdomains. The apex rule is +separate because wildcard DNS does not cover the root domain. Let +Cloudflare create the tunnel DNS records, or create CNAME records to the +tunnel target Cloudflare gives you. + +Do not point the tunnel at individual service containers. All public +traffic should go to `http://gateway:8080`. + +### Public Hosts + +The client service map and the browser surfaces expect these hosts to +reach the gateway: ```text yourdomain.com @@ -116,150 +192,154 @@ thorn.yourdomain.com videos.yourdomain.com ``` -All of these point to the same `DorkNet.Server` application/container -port today. `admin` and `feed` serve static hosts; the rest are the -service URLs returned to the client by `ConfigService`. +With Cloudflare Tunnel, the apex and wildcard hostname rules cover the +list. If you deploy without a tunnel, every host must route to the +gateway container on port `8080`. ---- +### Smoke Checks -## Dokploy: microservices - -Use this to run the gateway/service-host layout. - -Create a Dokploy **Compose** service from one of these files: - -| Compose file | Use | -|---|---| -| `docker-compose.microservices.yml` | Local/dev-style compose | -| `docker-compose.microservices.dokploy.yml` | Dokploy compose with a Cloudflare Tunnel sidecar | - -Both compose files provide: - -| Service | Purpose | -|---|---| -| `gateway` | Edge reverse proxy; service map, route table, and service health probes | -| `identity` | Auth, accounts, and platform-login route slice | -| `rooms` | Rooms, room keys, playlists, matchmaking, and discovery route slice | -| `notify` | Notify, messages/chat, SignalR, and notification route slice | -| `content` | CDN paths, uploads, images, photos, room blobs, and storage route slice | -| `social` | Clubs, groups, announcements, player events, subscriptions route slice | -| `commerce` | Catalog, storefronts, econ, inventory, inventions route slice | -| `platform` | Service directory, config, version checks, geo, strings, and platform route slice | -| `moderation` | Bug reporting, player reporting, sanitize, admin API, and testcase route slice | -| `web` | Apex/www/admin/feed static hosts and site API route slice | -| `monolith` | Fallback full server for unknown route families not assigned to a service yet | -| `postgres` | Internal Postgres for the service network | -| `redis` | Internal Redis for ephemeral state and fan-out | -| `cloudflared` | Cloudflare Tunnel sidecar; Dokploy file only | - -### Cloudflare Tunnel domains - -Use `docker-compose.microservices.dokploy.yml` with Cloudflare Tunnel. -Do not add Dokploy domain rows for this compose stack; public traffic -enters through the `cloudflared` sidecar instead of Dokploy's Traefik -router. - -Set these Dokploy Compose environment variables: - -```env -DORKNET_DOMAIN=yourdomain.com -DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters -POSTGRES_PASSWORD=replace-with-a-random-db-password -CLOUDFLARE_TUNNEL_TOKEN= -Photon__AppId=your-photon-realtime-app-id -Photon__VoiceAppId=your-photon-voice-app-id -Photon__CloudRegion=eu -DORKNET_DEFAULT_CLIENT_VERSION=december_2020_12_18 -DORKNET_SUPPORTED_VERSION=december_2020_12_18 -DORKNET_DECEMBER_BUILD_VERSION_KEY=december_2020_12_18 -``` - -By default the service containers connect to the bundled compose -Postgres service: - -```env -Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD} -``` - -To use a Dokploy-managed or external Postgres instead, set a full -connection string and the compose services will use it instead of the -bundled `postgres` hostname: - -```env -DORKNET_POSTGRES_CONNECTION_STRING=Host=;Port=5432;Database=dorknet;Username=;Password= -``` - -`docker-compose.microservices.dokploy.yml` attaches every backend -service that opens database connections to both the stack's default -network and Dokploy's external `dokploy-network`. Use the managed -Postgres service's real internal Docker/Dokploy hostname in -`DORKNET_POSTGRES_CONNECTION_STRING`; inside a DorkNet service container, -`getent hosts ` should return a Docker IP, not -`127.0.0.1`. - -For hosted Postgres that requires TLS, include the Npgsql SSL options: - -```env -DORKNET_POSTGRES_CONNECTION_STRING=Host=;Port=5432;Database=dorknet;Username=;Password=;SSL Mode=Require;Trust Server Certificate=true -``` - -If you use the bundled compose Postgres, `POSTGRES_PASSWORD` must match -the password that initialized the existing Postgres volume. Changing -`POSTGRES_PASSWORD` later does not change the already-created `dorknet` -database user; either restore the original password, reset it inside -Postgres, or recreate the Postgres volume. - -In Cloudflare Zero Trust, configure the tunnel with two public hostnames: - -```text -yourdomain.com -> http://gateway:8080 -*.yourdomain.com -> http://gateway:8080 -``` - -The wildcard covers `api`, `auth`, `rooms`, `notify`, `cdn`, `clubs`, -`commerce`, and the other client-facing subdomains. The apex rule is -separate because wildcard DNS does not cover the root domain. Let -Cloudflare create the tunnel DNS records, or create CNAME records to the -tunnel target Cloudflare gives you. - -Every public DorkNet domain listed above should point to the gateway. -The gateway preserves the original Host header so service code still sees -`auth.yourdomain.com`, `rooms.yourdomain.com`, and the other client -hosts. - -Set these Compose environment variables in Dokploy: - -```env -DORKNET_S3_ENDPOINT=https://your-s3-api-endpoint -DORKNET_S3_ACCESS_KEY=your-access-key -DORKNET_S3_SECRET_KEY=your-secret-key -DORKNET_S3_REGION=garage -``` - -The compose services already refer to Postgres as `postgres` and Redis as -`redis` on the internal Compose network. Do not add an `object-storage` -service unless you intentionally want a local dev-only S3 emulator. - -Useful smoke checks after deploy: +After Dokploy finishes rebuilding, verify the gateway and service map: ```bash curl https://api.yourdomain.com/healthz curl https://api.yourdomain.com/internal/services curl https://api.yourdomain.com/internal/services/health curl https://api.yourdomain.com/internal/routes +curl https://api.yourdomain.com/api/versioncheck/v4?v=20201210 +``` + +The version check should return: + +```json +{"VersionStatus":0} +``` + +Browser hosts should route to `web` and return static files: + +```bash +curl -I https://yourdomain.com/ +curl -I https://admin.yourdomain.com/ +curl -I https://feed.yourdomain.com/ +``` + +Inside the running web container, these files must exist: + +```bash +docker exec sh -lc 'ls -la /app/wwwroot/admin /app/wwwroot/site /app/wwwroot/feed' +``` + +Expected key files: + +```text +/app/wwwroot/admin/index.html +/app/wwwroot/site/index.html +/app/wwwroot/feed/index.html ``` `/internal/routes` shows which host/path patterns route to each service -slice or the monolith fallback. `/internal/services/health` reports -gateway-visible health for every backend service. +slice or to the monolith fallback. `/internal/services/health` reports +gateway-visible health for each backend service. + +### Useful Logs + +```bash +docker logs -gateway-1 --tail=200 +docker logs -web-1 --tail=200 +docker logs -platform-1 --tail=200 +docker logs -cloudflared-1 --tail=200 +``` + +If `admin.` or the apex returns 404, check the `web` logs for +static-host probes such as: + +```text +probe="/app/wwwroot/admin/index.html" exists=False +``` + +That means the `web` image did not include the built SPA assets or the +old container is still running. Force a clean Dokploy rebuild on the +latest branch commit. --- -## S3 storage +## Local Microservices -DorkNet uses S3-compatible storage for room blobs, profile images, -camera photos, and CDN/image pipeline backing data. Create these buckets -before production cutover: +For local service-split testing without Dokploy: + +```bash +docker compose -f docker-compose.microservices.yml up --build +``` + +This starts the gateway on host port `8080`, all service slices, the +monolith fallback, Postgres, and Redis. The local compose file does not +start `cloudflared`; direct local testing uses Host headers: + +```bash +curl -H 'Host: api.localhost' http://localhost:8080/healthz +curl -H 'Host: admin.localhost' http://localhost:8080/ +curl -H 'Host: localhost' http://localhost:8080/ +``` + +Set `DORKNET_S3_*` if you need blob/image features locally. Leaving S3 +blank is fine for API smoke tests that do not touch object storage. + +--- + +## Standalone Server Fallback + +The root `Dockerfile` builds the standalone `DorkNet.Server` image. Use +it for local debugging, narrow rollback tests, or a temporary +single-container deployment. The current production docs and Dokploy +workflow assume the microservices compose stack instead. + +Create a Dokploy **Application** only if you intentionally want the +single-container fallback: + +| Field | Value | +|---|---| +| Source | This GitHub repo / branch | +| Build type | Dockerfile | +| Dockerfile path | `Dockerfile` | +| Context | `.` | +| Container port | `8080` | + +Standalone environment variables use the direct .NET configuration keys: + +```env +ASPNETCORE_ENVIRONMENT=Production +ASPNETCORE_URLS=http://0.0.0.0:8080 +DOTNET_RUNNING_IN_CONTAINER=true + +DORKNET_DOMAIN=yourdomain.com +DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters + +Database__Provider=postgres +ConnectionStrings__Default=Host=;Port=5432;Database=dorknet;Username=;Password= +ConnectionStrings__Redis=redis://:6379 + +S3__Endpoint=https://your-s3-api-endpoint +S3__AccessKey=your-access-key +S3__SecretKey=your-secret-key +S3__Region=garage + +Photon__AppId=your-photon-realtime-app-id +Photon__VoiceAppId=your-photon-voice-app-id +Photon__CloudRegion=eu +DorkNet__DefaultClientVersion=december_2020_12_18 +DorkNet__SupportedVersions__0=december_2020_12_18 +DorkNet__BuildIdToVersionKey__20201210=december_2020_12_18 +``` + +Every public host listed above must route to this single container when +running standalone. + +--- + +## S3 Storage + +DorkNet uses S3-compatible storage for room blobs, profile images, camera +photos, and CDN/image pipeline backing data. Create these buckets before +production cutover: ```text profile-images @@ -287,41 +367,24 @@ S3__MaxErrorRetry=1 S3__TimeoutSeconds=300 ``` -For the microservices compose file, use the `DORKNET_S3_*` equivalents; -Compose maps them into `S3__*` for the service containers. - -The microservices compose files also map `Photon__AppId`, -`Photon__VoiceAppId`, and `Photon__CloudRegion` into every backend -service. Missing or wrong Photon values show up in the 2020 client as -NameServer/region connection failures after the boot version check. - -They also map the December 2020 build ID into the version gate. A quick -check for the 2020 client should return `{"VersionStatus":0}`: - -```bash -curl https://api.yourdomain.com/api/versioncheck/v4?v=20201210 -``` - -If `admin.` or `feed.` returns the NameServer JSON -instead of the static site, verify the `web` container contains -`/app/wwwroot/admin/index.html` and `/app/wwwroot/feed/index.html`. -Those hosts should resolve through the gateway to the `web` service, not -the `ns` service or monolith fallback. +For the microservices compose files, use the `DORKNET_S3_*` +equivalents; compose maps them into `S3__*` for the service containers. --- -## First-time cutover from SQLite +## First-Time Cutover From SQLite 1. Stop the local server so the SQLite source stops accepting writes. 2. Snapshot the SQLite database. -3. Provision Postgres, Redis, and the separate S3-compatible storage. +3. Provision Postgres, Redis, and separate S3-compatible storage. 4. Run the migration/import tooling against the new Postgres and S3 targets. -5. Deploy the full `DorkNet.Server` application. -6. Verify `/healthz` returns 200 before sending players to the new host. +5. Deploy the microservices compose stack. +6. Verify `/healthz`, `/internal/services/health`, versioncheck, and + the browser static hosts before sending players to the new host. -The server uses an advisory-lock guarded startup path for Postgres so -multiple replicas can start without racing schema/bootstrap work. +The database bootstrap path uses advisory locking so multiple service +containers can start without racing schema/bootstrap work. Rollback before players write to Postgres is simple: stop the deploy and return to the SQLite snapshot. After players write to Postgres, rollback @@ -352,7 +415,7 @@ is updated to send `userid` and `LoginLock`. --- -## Future schema changes +## Future Schema Changes SQLite dev DBs apply EF migrations. The production Postgres path is still based on `EnsureCreated()` plus idempotent bootstrap steps. Until that is @@ -362,7 +425,7 @@ schema changes as explicit deployment work: 1. Add or update the entity model. 2. Add the migration for SQLite/dev. 3. Add any needed idempotent Postgres bootstrap SQL. -4. Deploy and verify `/healthz`. +4. Deploy and verify `/healthz` and `/internal/services/health`. See [`../DorkNet.Server/Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md) for the detailed migration discipline. diff --git a/tools/cloudflared.README.md b/tools/cloudflared.README.md index 904acbb..48deb83 100644 --- a/tools/cloudflared.README.md +++ b/tools/cloudflared.README.md @@ -1,11 +1,19 @@ -# Cloudflare Tunnel for `localhost` +# Cloudflare Tunnel -Stand up a Cloudflare Tunnel that exposes your local DorkNet server -on `*.localhost` for public testing — without opening port 443 on your -firewall, and using Cloudflare's real Let's-Encrypt-equivalent cert -edge-side so visitors don't need your mkcert CA installed. +Cloudflare Tunnel is used in two DorkNet workflows: -## When to use this +- Local standalone testing, where cloudflared forwards `*.localhost` + traffic to a locally running `DorkNet.Server`. +- Dokploy production microservices, where the compose sidecar forwards + the apex and wildcard hostnames to `http://gateway:8080`. + +Both avoid opening ports directly on the host and let Cloudflare serve a +public TLS certificate at the edge. + +## Local Standalone Tunnel + +Use this when you are running the standalone server locally and want +friends to reach it through Cloudflare. - You want a friend to play the patched 2020 Rec Room client against your server, but they're not on your LAN and don't want to install @@ -18,7 +26,7 @@ edge-side so visitors don't need your mkcert CA installed. 1. **`localhost` is on a Cloudflare account you control.** The DNS nameservers for the domain need to be Cloudflare's. Check at — the zone should be Active. -2. **Local server running** on `0.0.0.0:443` with the wildcard cert +2. **Local standalone server running** on `0.0.0.0:443` with the wildcard cert that covers `*.localhost` (re-run `tools/patch-client.ps1` if your cert is still the `+1` rec.net-only one — it'll regenerate as `+3` covering both domains). @@ -59,7 +67,7 @@ Or install as a Windows service so it survives reboots: cloudflared service install ``` -## Dokploy sidecar +## Dokploy Microservices Sidecar For Dokploy microservices, use `docker-compose.microservices.dokploy.yml`. It starts `cloudflared` in @@ -80,24 +88,36 @@ yourdomain.com -> http://gateway:8080 Do not add these domains in Dokploy's domain modal for this compose stack; Cloudflare owns the public hostname routing. -## What happens at request time +## What Happens At Request Time + +Local standalone tunnel: ``` -visitor → https://api.localhost/api/versioncheck/v4 - → Cloudflare edge (real LE cert, validated) - → cloudflared tunnel - → cloudflared (running on your machine) - → http://gateway:8080 (Host preserved) - → DorkNet.Gateway reverse proxy - → dedicated service slice / monolith fallback +visitor -> https://api.localhost/api/versioncheck/v4 + -> Cloudflare edge + -> cloudflared tunnel + -> cloudflared running on your machine + -> https://localhost:443 (Host preserved) + -> DorkNet.Server ``` -The leg from cloudflared → localhost has `noTLSVerify: true` because -the local server's cert is from mkcert (private CA only trusted on -your machine). The Cloudflare-edge → cloudflared leg is still -TLS-encrypted with their cert, so this isn't a security weakening -— it's just acknowledging that "127.0.0.1 with self-trusted cert" is -where the trust chain ends. +Dokploy microservices tunnel: + +``` +visitor -> https://api.yourdomain.com/api/versioncheck/v4 + -> Cloudflare edge + -> cloudflared sidecar + -> http://gateway:8080 (Host preserved) + -> DorkNet.Gateway reverse proxy + -> dedicated service slice or monolith fallback +``` + +For the local standalone template, the leg from cloudflared to localhost +has `noTLSVerify: true` because the local server's cert is from mkcert +(private CA only trusted on your machine). The Cloudflare-edge to +cloudflared leg is still TLS-encrypted with Cloudflare's cert. In the +Dokploy microservices stack, the tunnel sidecar uses plain HTTP on the +private Compose network and sends everything to the gateway. ## Tearing down