Update docs for microservices architecture

This commit is contained in:
Alexa
2026-06-15 23:35:30 +03:00
parent 6704505dae
commit c6a4357c6f
6 changed files with 363 additions and 261 deletions
+5
View File
@@ -2,6 +2,11 @@
Native Android/iOS admin app for DorkNet, built with .NET MAUI. 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.<domain>`. Cloudflare and
`DorkNet.Gateway` keep the public URL stable while admin API requests are
routed to the owning backend slice.
## Authentication ## Authentication
The app stores sensitive values in platform secure storage: The app stores sensitive values in platform secure storage:
+6 -5
View File
@@ -6,8 +6,9 @@
(generated by `dotnet ef migrations add Initial` against the (generated by `dotnet ef migrations add Initial` against the
current entity model — captures all 67 `DbSet<>` tables). current entity model — captures all 67 `DbSet<>` tables).
- **`LegacyUpgrades.cs`** holds idempotent data transforms that can't - **`LegacyUpgrades.cs`** holds idempotent data transforms that can't
be derived from the entity model. Runs once per server boot, after be derived from the entity model. Runs during host boot for the
EF migrations complete and before `RoomService.SeedAsync()`. standalone server and for service-slice containers, after schema
bootstrap completes and before `RoomService.SeedAsync()`.
## What changed in the consolidation ## What changed in the consolidation
@@ -37,8 +38,8 @@ Both were collapsed in 2026-05-25:
## Production cutover (one-time SQL) ## Production cutover (one-time SQL)
Existing prod databases need a one-time fixup to tell EF "the new 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 Initial is already applied here" so the next standalone or service-slice
`CREATE TABLE` over existing data. boot doesn't try to `CREATE TABLE` over existing data.
### Postgres path ### 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 Postgres continues to use `EnsureCreated()` post-consolidation, so
there's *nothing to do* — `EnsureCreated()` reads the entity model, there's *nothing to do* — `EnsureCreated()` reads the entity model,
sees the schema already matches, and no-ops. Just deploy the new 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 If you ever switch the Postgres path to `Migrate()`, run this once
before the cutover deploy: before the cutover deploy:
+11 -7
View File
@@ -23,7 +23,7 @@ main also auto-detects your install.
| **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` | | **Version key** (sent as `X-DorkNet-Version` header) | `december_2020_12_18` |
| **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` | | **Version plugin** | `DorkNet.Server/Versions/Late2020/Late2020VersionPlugin.cs` |
| **Schema** | One EF `Initial` migration; SQLite + Postgres both work | | **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 | | **Diverges from `march-2020-03-10` by** | ~170 files |
Subsystems present on this branch that the March branch doesn't have: 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:SecretKey` | `S3__SecretKey` | production | S3 secret key |
| `S3:Region` | `S3__Region` | no | defaults to `garage`; use `auto` for R2 | | `S3:Region` | `S3__Region` | no | defaults to `garage`; use `auto` for R2 |
**4. Boot:** **4. Boot a local standalone server:**
```pwsh ```pwsh
dotnet run --project DorkNet.Server 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` `Application started.` in the log; `curl http://localhost:8080/healthz`
returns 200. 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). [docs/deploy.md](docs/deploy.md).
--- ---
@@ -199,7 +200,7 @@ Default `0` (off).
| Folder | Purpose | | 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.Models/` | DTOs shared between server and client mod |
| `DorkNet.Contracts/` | Shared service names, route ownership, service-map options, and health/probe contracts | | `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 | | `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.Services.*` | Gateway-fronted service hosts for identity, rooms, notify, content, social, commerce, platform, moderation, and web |
| `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher | | `DorkNet.ClientMod/` | MelonLoader IL2CPP mod — the client patcher |
| `tools/` | Installers (`install-melon.ps1`, `install-legacy-client.ps1`, `remove-eac.ps1`), Cloudflare tunnel templates | | `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 See [docs/architecture.md](docs/architecture.md) for the full mental
model (project layout, request lifecycle, watch-mirror controller model (project layout, request lifecycle, watch-mirror controller
@@ -215,6 +216,7 @@ pattern, where to start contributing).
### Microservices ### Microservices
The production topology is the gateway-fronted compose stack.
`docker-compose.microservices.yml` starts the gateway, identity, rooms, `docker-compose.microservices.yml` starts the gateway, identity, rooms,
notify, content, social, commerce, platform, moderation, web, and notify, content, social, commerce, platform, moderation, web, and
monolith fallback service hosts plus Compose-managed Postgres and Redis 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. domain rows.
The gateway routes owned host/path slices to the dedicated service hosts. 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 The monolith fallback is still present for unknown route families, so
public client URLs remain stable while the split continues. 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 docker compose -f docker-compose.microservices.yml up --build
``` ```
For deployment details, including Dokploy env vars and the current For deployment details, including Dokploy env vars, Cloudflare Tunnel
monolith-vs-microservices split, see [docs/deploy.md](docs/deploy.md). routing, external Postgres, and S3 setup, see
[docs/deploy.md](docs/deploy.md).
--- ---
+32 -23
View File
@@ -14,7 +14,7 @@ files in this folder.
``` ```
DorkNet.sln 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.Models Shared DTO + serialization types
├── DorkNet.Contracts Shared service contracts for the service split ├── DorkNet.Contracts Shared service contracts for the service split
├── DorkNet.ServiceDefaults Shared service-host setup ├── DorkNet.ServiceDefaults Shared service-host setup
@@ -33,7 +33,7 @@ DorkNet.sln
| Project | Responsibility | Target | | 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.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.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 | | `DorkNet.ServiceDefaults` | Common service health checks, JSON defaults, HTTP clients, service identity, route guard | .NET 9 |
@@ -78,12 +78,13 @@ DorkNet.Server/
## Runtime topology ## Runtime topology
`DorkNet.Server` remains the compatibility fallback and full standalone The production runtime is the gateway-fronted service split.
runtime. It can still run by itself, serving the full public `DorkNet.Server` remains available as the shared compatibility runtime
Rec Room-compatible HTTP surface, SignalR hub, static hosts, database and as a standalone debug/fallback image. In the compose stack it runs as
bootstrap, Redis-backed ephemeral state, and S3-backed blob storage. `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.Gateway` reverse-proxies public requests by host/path.
- `DorkNet.Services.Identity`, `DorkNet.Services.Rooms`, - `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 `DorkNet.Services.Web` run the shared server stack behind a route
ownership guard. That keeps response shapes identical while moving ownership guard. That keeps response shapes identical while moving
traffic by domain/path slice. 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 - `DorkNet.Gateway` exposes `/internal/services` and
`/internal/services/health`, plus `/internal/routes` for the active `/internal/services/health`, plus `/internal/routes` for the active
proxy table. proxy table.
@@ -108,12 +106,18 @@ The microservice shape is a gateway-fronted split:
`docker-compose.microservices.yml` starts gateway, all dedicated service `docker-compose.microservices.yml` starts gateway, all dedicated service
slices, the monolith fallback, and Compose-managed Postgres and Redis for slices, the monolith fallback, and Compose-managed Postgres and Redis for
local testing. local testing. `docker-compose.microservices.dokploy.yml` is the Dokploy
`docker-compose.microservices.dokploy.yml` is the Dokploy shape: it adds production shape: it adds a `cloudflared` sidecar that joins the same
a `cloudflared` sidecar that joins the same Compose network and forwards Compose network and forwards Cloudflare Tunnel traffic to
Cloudflare Tunnel traffic to `http://gateway:8080`. Object storage is `http://gateway:8080`. Object storage is external S3-compatible storage
external S3-compatible storage configured with `DORKNET_S3_*` configured with `DORKNET_S3_*` environment variables; the compose files
environment variables; the compose files do not run MinIO or Garage. 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 ### Controllers/ groupings
@@ -254,11 +258,14 @@ Two providers, switched by `Database:Provider` config:
- **SQLite** — default for local dev. File at `bin/<config>/data/dorknet.db`. - **SQLite** — default for local dev. File at `bin/<config>/data/dorknet.db`.
- **Postgres** — production. Connection string from `ConnectionStrings:Default`. - **Postgres** — production. Connection string from `ConnectionStrings:Default`.
Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`. EF Schema lives in `Data/Entities/*` and `Data/DorkNetDbContext.cs`.
migrations under `Data/Migrations/` apply via `Database.Migrate()` on SQLite dev DBs apply EF migrations under `Data/Migrations/` via
boot. Data transforms that can't be expressed as schema migrations `Database.Migrate()`. The production Postgres path still uses
(seed renames, computed backfills) go in `Data/LegacyUpgrades.cs` as `EnsureCreated()` plus idempotent bootstrap work under the same
idempotent methods registered in `RunAsync()`. See 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 [`Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md) for the
discipline: one release = one migration; everything else is discipline: one release = one migration; everything else is
`LegacyUpgrades`. `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 4. Look at one **`Cpp2IL`-cited line** in a controller to see how
wire shapes are pinned to decompiled provenance. 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. `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.
--- ---
+267 -204
View File
@@ -1,78 +1,154 @@
# DorkNet Deployment # 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 Object storage is always a separate S3-compatible service. The compose
`DorkNet.Server`. This is still the runtime that serves every public files start Postgres and Redis, but they do not start MinIO, Garage, or
Rec Room-compatible endpoint. any other object-storage emulator.
- **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.
--- ---
## 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**: | Service | Purpose |
| Field | Value |
|---|---| |---|---|
| Source | This GitHub repo / branch | | `gateway` | Edge reverse proxy, service map, route table, and service health probes |
| Build type | Dockerfile | | `identity` | Auth, accounts, platform login, JWT issuance |
| Dockerfile path | `Dockerfile` | | `rooms` | Rooms, room keys, playlists, matchmaking, discovery |
| Context | `.` | | `notify` | Notify, messages/chat, SignalR, notification fan-out |
| Container port | `8080` | | `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 ```env
ASPNETCORE_ENVIRONMENT=Production
ASPNETCORE_URLS=http://0.0.0.0:8080
DOTNET_RUNNING_IN_CONTAINER=true
DORKNET_DOMAIN=yourdomain.com DORKNET_DOMAIN=yourdomain.com
DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters DORKNET_JWT_SECRET=replace-with-at-least-64-random-characters
CLOUDFLARE_TUNNEL_TOKEN=<token from Cloudflare Zero Trust tunnel>
Database__Provider=postgres # Bundled compose Postgres. Optional when DORKNET_POSTGRES_CONNECTION_STRING is set.
ConnectionStrings__Default=Host=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<password> POSTGRES_PASSWORD=replace-with-a-random-db-password
ConnectionStrings__Redis=redis://<redis-host>:6379
S3__Endpoint=https://your-s3-api-endpoint # External object storage. Required for production.
S3__AccessKey=your-access-key DORKNET_S3_ENDPOINT=https://your-s3-api-endpoint
S3__SecretKey=your-secret-key DORKNET_S3_ACCESS_KEY=your-access-key
S3__Region=garage DORKNET_S3_SECRET_KEY=your-secret-key
DORKNET_S3_REGION=garage
# Photon Cloud.
Photon__AppId=your-photon-realtime-app-id Photon__AppId=your-photon-realtime-app-id
Photon__VoiceAppId=your-photon-voice-app-id Photon__VoiceAppId=your-photon-voice-app-id
Photon__CloudRegion=eu Photon__CloudRegion=eu
DorkNet__DefaultClientVersion=december_2020_12_18
DorkNet__SupportedVersions__0=december_2020_12_18 # December 2020 client version gate.
DorkNet__BuildIdToVersionKey__20201210=december_2020_12_18 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, The compose file maps the `DORKNET_S3_*` values into `S3__*` for every
use the internal service hostnames Dokploy gives you. For a Compose-style backend service, and maps the version variables into `DorkNet__*`
network the values usually look like: configuration keys. `Photon__*` is passed directly to each service.
### Database Options
By default, service containers connect to the bundled compose Postgres:
```env ```env
ConnectionStrings__Default=Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=dorknet_dev Host=postgres;Port=5432;Database=dorknet;Username=dorknet;Password=${POSTGRES_PASSWORD}
ConnectionStrings__Redis=redis://redis:6379
``` ```
### 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 ```env
application. Every host routes to container port `8080`. DORKNET_POSTGRES_CONNECTION_STRING=Host=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<password>
```
Add these hosts for full client compatibility: For hosted Postgres that requires TLS:
```env
DORKNET_POSTGRES_CONNECTION_STRING=Host=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<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 <web-or-platform-container> getent hosts <postgres-host>
```
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 ```text
yourdomain.com yourdomain.com
@@ -116,150 +192,154 @@ thorn.yourdomain.com
videos.yourdomain.com videos.yourdomain.com
``` ```
All of these point to the same `DorkNet.Server` application/container With Cloudflare Tunnel, the apex and wildcard hostname rules cover the
port today. `admin` and `feed` serve static hosts; the rest are the list. If you deploy without a tunnel, every host must route to the
service URLs returned to the client by `ConfigService`. gateway container on port `8080`.
--- ### Smoke Checks
## Dokploy: microservices After Dokploy finishes rebuilding, verify the gateway and service map:
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=<token from Cloudflare Zero Trust tunnel>
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=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<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 <postgres-host>` 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=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<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:
```bash ```bash
curl https://api.yourdomain.com/healthz curl https://api.yourdomain.com/healthz
curl https://api.yourdomain.com/internal/services curl https://api.yourdomain.com/internal/services
curl https://api.yourdomain.com/internal/services/health curl https://api.yourdomain.com/internal/services/health
curl https://api.yourdomain.com/internal/routes 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 <web-container> 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 `/internal/routes` shows which host/path patterns route to each service
slice or the monolith fallback. `/internal/services/health` reports slice or to the monolith fallback. `/internal/services/health` reports
gateway-visible health for every backend service. gateway-visible health for each backend service.
### Useful Logs
```bash
docker logs <stack>-gateway-1 --tail=200
docker logs <stack>-web-1 --tail=200
docker logs <stack>-platform-1 --tail=200
docker logs <stack>-cloudflared-1 --tail=200
```
If `admin.<domain>` 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, For local service-split testing without Dokploy:
camera photos, and CDN/image pipeline backing data. Create these buckets
before production cutover: ```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=<postgres-host>;Port=5432;Database=dorknet;Username=<user>;Password=<password>
ConnectionStrings__Redis=redis://<redis-host>: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 ```text
profile-images profile-images
@@ -287,41 +367,24 @@ S3__MaxErrorRetry=1
S3__TimeoutSeconds=300 S3__TimeoutSeconds=300
``` ```
For the microservices compose file, use the `DORKNET_S3_*` equivalents; For the microservices compose files, use the `DORKNET_S3_*`
Compose maps them into `S3__*` for the service containers. 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.<domain>` or `feed.<domain>` 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.
--- ---
## First-time cutover from SQLite ## First-Time Cutover From SQLite
1. Stop the local server so the SQLite source stops accepting writes. 1. Stop the local server so the SQLite source stops accepting writes.
2. Snapshot the SQLite database. 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 4. Run the migration/import tooling against the new Postgres and S3
targets. targets.
5. Deploy the full `DorkNet.Server` application. 5. Deploy the microservices compose stack.
6. Verify `/healthz` returns 200 before sending players to the new host. 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 The database bootstrap path uses advisory locking so multiple service
multiple replicas can start without racing schema/bootstrap work. containers can start without racing schema/bootstrap work.
Rollback before players write to Postgres is simple: stop the deploy and Rollback before players write to Postgres is simple: stop the deploy and
return to the SQLite snapshot. After players write to Postgres, rollback 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 SQLite dev DBs apply EF migrations. The production Postgres path is still
based on `EnsureCreated()` plus idempotent bootstrap steps. Until that is 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. 1. Add or update the entity model.
2. Add the migration for SQLite/dev. 2. Add the migration for SQLite/dev.
3. Add any needed idempotent Postgres bootstrap SQL. 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) See [`../DorkNet.Server/Data/MIGRATIONS.md`](../DorkNet.Server/Data/MIGRATIONS.md)
for the detailed migration discipline. for the detailed migration discipline.
+42 -22
View File
@@ -1,11 +1,19 @@
# Cloudflare Tunnel for `localhost` # Cloudflare Tunnel
Stand up a Cloudflare Tunnel that exposes your local DorkNet server Cloudflare Tunnel is used in two DorkNet workflows:
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.
## 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 - 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 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 1. **`localhost` is on a Cloudflare account you control.** The DNS
nameservers for the domain need to be Cloudflare's. Check at nameservers for the domain need to be Cloudflare's. Check at
<https://dash.cloudflare.com/> — the zone should be Active. <https://dash.cloudflare.com/> — 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 that covers `*.localhost` (re-run `tools/patch-client.ps1` if your
cert is still the `+1` rec.net-only one — it'll regenerate as cert is still the `+1` rec.net-only one — it'll regenerate as
`+3` covering both domains). `+3` covering both domains).
@@ -59,7 +67,7 @@ Or install as a Windows service so it survives reboots:
cloudflared service install cloudflared service install
``` ```
## Dokploy sidecar ## Dokploy Microservices Sidecar
For Dokploy microservices, use For Dokploy microservices, use
`docker-compose.microservices.dokploy.yml`. It starts `cloudflared` in `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 Do not add these domains in Dokploy's domain modal for this compose
stack; Cloudflare owns the public hostname routing. 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 visitor -> https://api.localhost/api/versioncheck/v4
→ Cloudflare edge (real LE cert, validated) -> Cloudflare edge
→ cloudflared tunnel -> cloudflared tunnel
→ cloudflared (running on your machine) -> cloudflared running on your machine
→ http://gateway:8080 (Host preserved) -> https://localhost:443 (Host preserved)
→ DorkNet.Gateway reverse proxy -> DorkNet.Server
→ dedicated service slice / monolith fallback
``` ```
The leg from cloudflared → localhost has `noTLSVerify: true` because Dokploy microservices tunnel:
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 visitor -> https://api.yourdomain.com/api/versioncheck/v4
— it's just acknowledging that "127.0.0.1 with self-trusted cert" is -> Cloudflare edge
where the trust chain ends. -> 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 ## Tearing down