The F1 Competition Platform is a modular solution designed to aggregate Formula 1 data and serve it via a modern RESTful API. Built with .NET 8 and following Clean Architecture principles, it automates data ingestion from external providers and serves it through a containerized infrastructure.
It consists of two main components:
- F1.DataSyncWorker (.NET Worker): A scheduled worker that fetches baseline data from Jolpica and seeds the live Postgres model (competitions, drivers, and races).
- F1.Api (ASP.NET Core): A RESTful API that serves the aggregated data to clients.
Legacy note: PopulateF1Database (Azure Functions/Cosmos path) is kept only for transition support and is no longer the canonical baseline data path.
The solution is built using .NET 8 and follows Clean Architecture principles, leveraging dependency injection, configuration management, and containerization.
The platform is hosted on a local Proxmox Virtualization Environment using Debian 12 LXC containers.
- Pull Request Validation: GitHub Actions runs formatting/analyzer checks, strict builds, and unit tests with coverage on pull requests.
- Registry (Release-Only on
main): Successful main-branch runs build and push release images to GitHub Container Registry (GHCR). - Automated Staging (
f1-test): The test environment runs Watchtower, which automatically pulls and restarts the containers whenever the moving:testimage aliases are updated. - Production Gate (
f1-prod): Deployment to production requires Manual Approval via GitHub Environments, ensuring a stable "human-in-the-loop" verification before live updates.
docker-build.yamlRuns a lightweight, path-filtered check on pull requests and runs heavy container build/push plus post-deploy E2E on main and manual dispatch, with changed-service gating to skip unaffected images.code-quality.ymlRuns on pull requests for formatting/analyzers, strict build validation, and API/Web unit tests with coverage.codeql.ymlRuns weekly by schedule and supports manual dispatch for urgent security investigations.docker-debug-on-demand.ymlManual workflow that builds/pushes debug images only when incident debugging is needed.dependency-review.ymlLightweight dependency risk check on pull requests.
sha-<shortsha>: immutable build artifact for traceability and rollback.test: moving alias for the current main-branch build used by the test environment.stable: moving alias for the manually approved production build.
Rollback process:
- Set
TAG=sha-<known-good-sha>on the target host. - Recreate the containers so Docker Compose uses that pinned image tag.
| Environment | Host IP (Internal) | Port | Deployment Logic |
|---|---|---|---|
| Test | 192.168.0.50 |
5000 |
Automated (Watchtower) |
| Production | 192.168.0.51 |
5000 |
Manual (Gatekeeper) |
The solution uses Cloudflare Tunnels to securely expose the services without opening ports on the router.
- Profile:
cloud(Enabled viaCOMPOSE_PROFILES=cloudor--profile cloud) - Token: Managed via
TUNNEL_TOKENin.env. - Domains:
- Test:
https://f1-test.philipwoulfe.com - API:
https://f1-api-test.philipwoulfe.com
- Test:
- Scheduled Data Sync Worker:
F1.DataSyncWorkeringests and upserts baseline competition, driver, and race data from Jolpica into Postgres. - ASP.NET Core API: A containerized Web API for data access.
- Persistence: API runtime persistence via Postgres.
- Containerized: Full Docker support for reproducible environments across Proxmox LXCs.
- Race-scoped selection endpoints: selection config, mine, and current snapshot routes require
raceIdin the path, and selection deadlines are read from the race record. - Additive selection rule config:
GET /selections/{raceId}/confignow includesbetOptions,earlyLockBetType,earlyLockLabel,finalSubmissionLabel,lockMessage, andlockedSelectionMessageso the generic page can render competition-specific lock semantics and labels without route-specific branching. - Additive metadata-driven selection copy:
GET /races/{raceId}/metadatamay include optionaldisplayTitleandlockMessagefields for web title/banner rendering; clients must gracefully fall back when absent. - Additive race-context resolver endpoints:
/races/context/{competitionSlug}/{season}/round/{round}and/races/context/{competitionSlug}/{season}/slug/{raceSlug}resolve generic route context to canonicalraceIdduring migration. - Race-scoped web selection routes: direct route
/selection/{raceId}accepts canonical race IDs only. Preferred defaults/navigation use context routes/selection/{competition}/{season}/round/{round}and/selection/{competition}/{season}/{raceSlug}.
.
├── .github/workflows/ # CI/CD Pipelines (Build, Test, Deploy)
├── src/
│ ├── F1.Api # ASP.NET Core Web API (Entry Point)
│ ├── F1.Core # Domain Entities & Interfaces
│ ├── F1.Infrastructure # Database & External Integrations
│ ├── F1.DataSyncWorker # Scheduled Postgres baseline seed worker
│ ├── F1.Services # Business Logic
│ └── PopulateF1... # Legacy Azure Function ingestion apps (deprecated path)
├── tests/
│ └── F1.Api.Tests # XUnit Test Suite
│ └── F1.Web.Tests # XUnit Test Suite
├── docker-compose.yml # Infrastructure Blueprint
└── Dockerfile # Multi-stage Docker Build
- .NET 8 SDK
- Docker & Docker Compose
- PostgreSQL 16 – provisioned automatically via Docker Compose (
postgres:16). No manual installation needed for Docker-based runs; for bare-metaldotnet runyou need a local PostgreSQL 16 instance. - entr (for live test-watching on Linux/WSL):
sudo apt-get update && sudo apt-get install entr
The solution uses .env for Docker Compose plus appsettings/environment variables for API and worker runtime settings.
Create a .env file in the root of the project. This file controls ports, URLs, and environment settings for the local Docker containers.
# Copy the example file to create your local configuration
cp .env.example .envThe default values in .env.example are configured for a standard local setup.
Required API values in .env:
POSTGRES_DB,POSTGRES_USER,POSTGRES_PASSWORD: used to buildConnectionStrings__Postgresforf1-api.CLOUDFLARE_AUDIENCE: mapped toCloudflareAccess__Audienceforf1-api.
For non-Docker local API runs (dotnet run), configure ConnectionStrings:Postgres via environment variable or user-secrets instead of committing credentials in appsettings files:
# Option A: environment variable
export ConnectionStrings__Postgres='Host=localhost;Port=5432;Database=f1competition;Username=<user>;Password=<password>'
# Option B: user secrets (from src/F1.Api)
dotnet user-secrets set "ConnectionStrings:Postgres" "Host=localhost;Port=5432;Database=f1competition;Username=<user>;Password=<password>"Note: src/F1.Api/appsettings.json and src/F1.Api/appsettings.Development.json intentionally keep ConnectionStrings:Postgres empty so missing configuration fails fast with a clear startup error.
Optional Postgres bootstrap value in .env:
DB_AUTO_MIGRATE: mapped toDatabase__AutoMigrateforf1-api. Whentrue, the API applies EF Core migrations on startup. Baseline competition/driver/race ingestion is handled byf1-data-sync-worker.- Default in
.env.example:true.
- Default in
Optional worker values in .env:
These DATA_SYNC_* values are consumed by docker-compose.yml and mapped to DataSyncWorker__* environment variables for the f1-data-sync-worker container.
DATA_SYNC_INTERVAL_MINUTES: mapped toDataSyncWorker__IntervalMinutesforf1-data-sync-worker.0means run once and exit.DATA_SYNC_AUTO_MIGRATE: mapped toDataSyncWorker__AutoMigrateforf1-data-sync-worker.- Default in
.env.example:falseto keep a single migration owner by default (the API viaDB_AUTO_MIGRATE=true). Set totrueonly if you intentionally want the worker to own migrations.
- Default in
DATA_SYNC_HTTP_RETRY_COUNT: mapped toDataSyncWorker__HttpRetryCountfor retry attempts against Jolpica.DATA_SYNC_HTTP_RETRY_DELAY_MS: mapped toDataSyncWorker__HttpRetryDelayMsfor retry backoff delay.DATA_SYNC_DEADLINE_MINUTES_BEFORE_START: mapped toDataSyncWorker__DeadlineMinutesBeforeStart; default placeholder policy is30.DATA_SYNC_JOLPICA_BASE_URL: mapped toDataSyncWorker__JolpicaBaseUrl.DATA_SYNC_CONTINUE_ON_ERROR: mapped toDataSyncWorker__ContinueOnError.MIGRATION_IMPORT_ENABLED: mapped toMigrationImport__Enabledfor one-time CSV migration mode.MIGRATION_IMPORT_SEASON: mapped toMigrationImport__Season; default is2025.MIGRATION_IMPORT_SOURCE_FILE_PATH: mapped toMigrationImport__SourceFilePath; default isdata/imports/phil-2025/PhilMigratedSelectionsAndScores.csv.MIGRATION_IMPORT_DRY_RUN: mapped toMigrationImport__DryRun; settruefor validation runs.MIGRATION_IMPORT_UNRESOLVED_TOKEN_FAIL_THRESHOLD: mapped toMigrationImport__UnresolvedTokenFailThreshold;0disables fail-fast and values greater than0fail the run when unresolved token count is greater than or equal to the threshold.MIGRATION_EXPECTED_VARIANCE_ENABLED: mapped toMigrationExpectedVariance__Enabled; enables expected variance ruleset loading.MIGRATION_EXPECTED_VARIANCE_RULE_MANIFEST_PATH: mapped toMigrationExpectedVariance__RuleManifestPath; default isdata/imports/phil-2025/expected-variance-rules.json.f1-data-sync-workerimage bundlesdata/imports/phil-2025under/app, so the default relative paths for migration source CSV and expected variance manifest resolve inside the container without manual host file edits.- To use host-managed files instead, keep
HOST_MIGRATION_UPLOAD_PATHmounted and point paths to/tmp/f1-imports/<file-name>.
Optional API values in .env:
ADMIN_GROUP_CLAIM_TYPE: mapped toCloudflareAccess__AdminGroupClaimTypeforf1-api. Sets the primary/custom claim used to read Cloudflare group membership; the middleware also falls back to common group claims (groups,group, andClaimTypes.GroupSid) when present.ADMIN_GROUPS: mapped toCloudflareAccess__AdminGroupsforf1-api. Any matching group value from the inspected claims grants theAdminrole.ADMIN_EMAILS: mapped toCloudflareAccess__AdminEmailsforf1-api. Any matching email value also grants theAdminrole, using case-insensitive matching.
Optional development toggle in .env:
DEV_MOCK_EMAIL: mapped toDevSettings__MockEmailforf1-api. Sets the mock user identity used when simulating Cloudflare locally.DEV_MOCK_GROUPS: mapped toDevSettings__MockGroupsforf1-api. Sets the mock group memberships used for local Admin/non-Admin testing.DEV_ENABLE_DEBUG_ENDPOINTS: mapped toDevSettings__EnableDebugEndpointsforf1-api. Whentrue, enables the test-only/api/users/debug/mediagnostics endpoint in allowed environments.HOST_MIGRATION_UPLOAD_PATH: shared host path bind-mounted to/tmp/f1-importsin bothf1-apiandf1-data-sync-workerso uploaded migration files are readable by the worker container. This directory must be pre-created on every environment host, includingtestandprod, beforedocker compose upor container restart. Ensure it is writable by the non-root container user, for example:mkdir -p /tmp/f1-migration-imports && chmod 1777 /tmp/f1-migration-imports.- If this directory is missing or left as
root:rootwith755, the API may fall back to a private temp path that the worker cannot see, and uploaded Dave package runs will fail during processing. - Recommended deployment practice: create or repair
HOST_MIGRATION_UPLOAD_PATHin host provisioning, cloud-init, Ansible, or a pre-start script rather than relying on a one-time manual fix.
Notes:
CloudflareAccess__Issueris currently set indocker-compose.yml.API_BASE_URLis set to/api/directly indocker-compose.ymlforf1-weband is not read from.env./api/users/debug/mereturns sanitized post-auth claims, groups, and role resolution data only whenDEV_ENABLE_DEBUG_ENDPOINTS=trueand the API is running inDevelopmentorTest.TAGapplies to API, Web, and Data Sync worker images. UseTAG=testfor the test host,TAG=stablefor production, andTAG=sha-<shortsha>for rollback or pinning a specific build.
Web post-login landing config:
src/F1.Web/wwwroot/appsettings.jsoncontains thePostLoginRoutingsection used by the web client after authentication.AdminLandingPathdefaults to/admin/migration-runs.AuthenticatedUserLandingPathdefaults to/results.FallbackPathis used when the user session is missing or a role-specific landing path is blank, and defaults to/results.- Environment-specific overrides can be added in
src/F1.Web/wwwroot/appsettings.Development.json,src/F1.Web/wwwroot/appsettings.Test.json, orsrc/F1.Web/wwwroot/appsettings.Production.json.
Web competition context config:
src/F1.Web/wwwroot/appsettings.jsonalso contains theSelectionContextsection used by the race-selection workspace selector.- Each entry defines
CompetitionSlug,CompetitionLabel,Season, andDefaultRound. - Visiting
/selectionrestores the last-used competition and season from browser storage when the saved context is still configured; otherwise the client falls back to the default configured context. - Removing a context from configuration automatically causes stored stale selections to fall back to a valid configured context on the next visit.
Competition leaderboard config:
src/F1.Api/appsettings.jsoncontains theCompetitionLeaderboardsection used by the standings endpoint.- Each context can be backed by a completed migration run or marked unavailable until a leaderboard source is approved.
- For migration-backed contexts,
MigrationSourcePathContainsselects the latest completed run used for leaderboard totals. - Official leaderboard totals currently use imported legacy scores for approved migrated contexts; admins can request recalculated comparison mode from the API/UI.
The worker reads ConnectionStrings:Postgres and the DataSyncWorker section. Default config includes the three baseline competitions for this epic:
- Philip 2025
- David 2025
- Main 2026
Run locally:
export ConnectionStrings__Postgres='Host=localhost;Port=5432;Database=f1competition;Username=<user>;Password=<password>'
dotnet run --project src/F1.DataSyncWorker/F1.DataSyncWorker.csprojSet DataSyncWorker__IntervalMinutes=0 for a one-shot run or any positive value for scheduled mode.
When running the worker directly (outside Docker Compose), set DataSyncWorker__* environment variables directly rather than DATA_SYNC_*.
For Docker Compose runtime, f1-data-sync-worker runs automatically and reads values from .env.
Migration import mode (Story #239 / Phil 2025 one-time load):
- Set
MigrationImport__Enabled=trueto switch worker execution from Jolpica sync mode to migration import mode. - Set
MigrationImport__Season=2025for the source season being imported. - Set
MigrationImport__SourceFilePathto the CSV file path (default:data/imports/phil-2025/PhilMigratedSelectionsAndScores.csv). - Set
MigrationImport__DryRun=trueto stage rows and run envelope metadata without writing domain competition/driver/race/selection entities. - Set
MigrationImport__UnresolvedTokenFailThresholdto control fail-fast behavior for unresolved tokens (0keeps complete-with-warnings mode). - Set
MigrationExpectedVariance__Enabled=trueto load rules from the manifest. - Set
MigrationExpectedVariance__RuleManifestPathto the promoted manifest location (default:data/imports/phil-2025/expected-variance-rules.json).
Expected variance ruleset promotion and audit:
- The canonical ruleset manifest is
data/imports/phil-2025/expected-variance-rules.jsonand is safe to reapply without duplication. - Rule entries can include
targetEnvironmentsto activate only in selected environments. - Each migration run logs ruleset metadata (
RuleSetId,RuleSetVersion,RuleSetChecksum, environment, and active rule count) for audit traceability.
When using Docker Compose, keep using .env MIGRATION_IMPORT_* variables because compose maps those to MigrationImport__* for the worker container.
Preseason migration runbook and sign-off checklist:
docs/runbooks/preseason-migration-signoff.md
Example one-shot dry-run:
export ConnectionStrings__Postgres='Host=localhost;Port=5432;Database=f1competition;Username=<user>;Password=<password>'
export MigrationImport__Enabled=true
export MigrationImport__Season=2025
export MigrationImport__SourceFilePath='data/imports/phil-2025/PhilMigratedSelectionsAndScores.csv'
export MigrationImport__DryRun=true
export MigrationImport__UnresolvedTokenFailThreshold=0
dotnet run --project src/F1.DataSyncWorker/F1.DataSyncWorker.csprojCLI entrypoint (M9):
--migration-importenables one-time migration mode.--source-file-path <path>(or--source <path>) overrides the CSV source path.--season <year>overrides the migration season.--dry-run(or--dry-run=<true|false>) controls write mode.--write-modeis shorthand to forceDryRun=false.
Example CLI run:
export ConnectionStrings__Postgres='Host=localhost;Port=5432;Database=f1competition;Username=<user>;Password=<password>'
dotnet run --project src/F1.DataSyncWorker/F1.DataSyncWorker.csproj -- \
--migration-import \
--source-file-path data/imports/phil-2025/PhilMigratedSelectionsAndScores.csv \
--season 2025 \
--dry-runEach run prints a migration summary line including rows parsed, rows rejected, unresolved token count, and total delta points.
This file configures the legacy Cosmos ingestion service and is no longer the canonical baseline seed path. It remains for migration fallback only.
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "UseDevelopmentStorage=true",
"FUNCTIONS_WORKER_RUNTIME": "dotnet-isolated",
"FUNCTIONS_INPROC_NET8_ENABLED": 1,
"Functions:Worker:HostEndpoint": "http://127.0.0.1:5001",
"UpdateDatabaseCronSchedule": "0 */1 * * * *",
"Environment": "Dev",
"CompetitionYear": "2025",
"CosmosDbConnectionString": "your-cosmos-db-connection-string",
"CosmosDbDatabaseId": "your-database-id",
"CosmosDbDriversContainer": "Drivers",
"CosmosDbPreSeasonQuestionsContainer": "PreSeasonQuestions",
"CosmosDbRacesContainer": "Races",
"CosmosDbResultsContainer": "Results",
"CosmosDbSprintsContainer": "Sprints",
"CosmosDbUsersContainer": "Users",
"JolpicaBaseUrl": "https://api.jolpi.ca/ergast/f1/",
"CosmosDbRetryCount": 1,
"CosmosDbRetryTime": 30,
"JolpicaRateLimitDelayMs": 500
}
}
The recommended way to run the application locally is using the provided build script. This workflow runs tests first and will abort the build if they fail, ensuring a stable environment.
# Make the script executable (only needs to be done once)
chmod +x build.shbuild.sh now supports dedicated quality and CI flows:
-
./build.sh
Runs API/Web tests with coverage and then builds/starts Docker containers. -
./build.sh --debug
Skips tests and runs debug Docker configuration. -
./build.sh --quality
Runs quality gate only (restore,dotnet format whitespace --verify-no-changes, and strict CI-style Release builds), then exits. -
./build.sh --ci
Runs quality gate and then tests with coverage, then exits (no Docker compose).
The Build and Push F1 Platform workflow includes a post-deploy Selenium gate:
build-and-pushpublishes immutable release:sha-<shortsha>images and updates the moving:testaliases used by the test environment.run-e2e-testexecutes Selenium flows against the deployed test environment.deploy-prodis blocked unlessrun-e2e-testsucceeds, then manually promotes the exact tested images to the moving:stablealiases.
For deep runtime debugging, use the manual Build Debug Images On Demand workflow (docker-debug-on-demand.yml) rather than building debug images on every main run.
Required GitHub Environment (test) secrets for the E2E job:
E2E_BASE_URL: base URL for the deployed test web app.E2E_API_BASE_URL: API base URL (optional ifE2E_BASE_URL + /apiworks for your routing).E2E_CF_CLIENT_ID: Cloudflare Access service token client ID.E2E_CF_CLIENT_SECRET: Cloudflare Access service token secret.
Optional E2E tuning:
E2E_TIMEOUT_SECONDS: defaults to 20 locally and 30 in CI.E2E_HEADLESS: defaults to true.E2E_COMPETITION_SLUG: defaults tomain.E2E_SEASON: defaults to2026.E2E_ROUND: defaults to1.E2E_RACE_ID: optional explicit canonical race ID override. When unset, E2E resolves race ID via/races/context/{competitionSlug}/{season}/round/{round}.E2E_STEP_TRACE_PATH: optional override for always-on Selenium step logs. Defaults toTestResults/e2e/step-traces.E2E_POSTGRES_CONNECTION_STRING: optional explicit connection string used by deterministic migration-runs E2E fixture seeding.POSTGRES_HOST: optional host override for migration-runs fixture seeding whenE2E_POSTGRES_CONNECTION_STRINGis not set. Defaults tolocalhost.
Test-only service-token fallback controls (use only when Cloudflare service-token JWTs omit email claims):
CLOUDFLARE_ENABLE_TEST_SERVICE_TOKEN_FALLBACK: defaultfalse; only active when API environment isTest.CLOUDFLARE_TEST_SERVICE_TOKEN_SUBJECT_ALLOWLIST: required allowlist of service-token identifiers (sub,nameidentifier, orcommon_name) permitted to use fallback identity.CLOUDFLARE_TEST_SERVICE_TOKEN_ADMIN_SUBJECT_ALLOWLIST: optional subset of allowlisted identifiers that should receive Admin role.CLOUDFLARE_TEST_SERVICE_TOKEN_EMAIL_DOMAIN: defaulttest.local; used for synthesized fallback email addresses.
GitHub Actions artifact behavior:
- CI writes E2E output to
TestResults/e2eon the runner. - Selenium step traces are always written to
TestResults/e2e/step-traces(orE2E_STEP_TRACE_PATHif set). - Failure screenshots and HTML are written to
TestResults/e2e/failure-artifactswhen capture runs. - The workflow uploads
TestResults/e2e/**andchromedriver.logas thee2e-resultsartifact. - The
e2e-resultsartifact is retained for 5 days to limit storage costs. - Runner-local files are discarded after the job completes, so GitHub Actions artifacts are the persisted copy for CI runs.
For SSH-based debugging, use the checked-in helper script:
./scripts/e2e-debug-vm.shThis script sets safe VM defaults and runs one focused E2E test by default:
E2E_BASE_URL=http://localhost:5001E2E_API_BASE_URL=http://localhost:5000E2E_HEADLESS=trueE2E_TIMEOUT_SECONDS=30
Run a specific test filter:
./scripts/e2e-debug-vm.sh "FullyQualifiedName~SubmitSelection_ShouldPersistServerSide"Run the full E2E project:
./scripts/e2e-debug-vm.sh allTo inspect the active Selenium Chrome target from your laptop, open an SSH tunnel first:
ssh -L 9222:localhost:9222 <user>@<vm>Then open:
http://localhost:9222/json/list
Notes:
- Keep Cloudflare service token values out of the script. Set
E2E_CF_CLIENT_IDandE2E_CF_CLIENT_SECRETin your shell when needed. - If API verification returns 404, verify
E2E_API_BASE_URLis correct for your route path.
The E2E test project also reads the repository root .env file when you run dotnet test tests/F1.E2E.Tests/F1.E2E.Tests.csproj locally.
Create a local .env (for example by copying .env.example) and point it at the local container ports:
E2E_BASE_URL=http://localhost:5001E2E_API_BASE_URL=http://localhost:5000E2E_POSTGRES_CONNECTION_STRING=Host=localhost;Port=5432;Database=f1competition;Username=f1;Password=f1
If you want the browser flows to execute locally, start the local stack first, then run the E2E project test command directly.
For preseason-focused checks, run the fast filter matrix documented in:
docs/testing/preseason-test-strategy.md
The matrix includes:
- Worker contract/scoring/lifecycle tests with infrastructure coverage output.
- Admin API preseason payload tests with API coverage output.
- Admin web preseason review tests with web coverage output.
- E2E migration runs preseason review flow filter.
Set deployment host .env files to match the image promotion flow:
- Test host:
TAG=test - Production host:
TAG=stable - Rollback or pinning a known build:
TAG=sha-<known-good-sha>
After changing TAG or any other deployment environment variable, recreate the containers. Watchtower updates images but does not apply changed container environment settings on its own.
For persistent API logs on Proxmox, keep test and production isolated with separate ZFS datasets and mount each dataset into its matching LXC.
On the Proxmox host:
zfs create tank/f1/test
zfs create tank/f1/test/logs
zfs create tank/f1/test/selenium
zfs create tank/f1/prod
zfs create tank/f1/prod/logs
zfs create tank/f1/prod/selenium
chown -R 101654:101654 /tank/f1/test /tank/f1/prod
chmod -R 0775 /tank/f1/test /tank/f1/prod
pct set 101 -mp0 /tank/f1/test/logs,mp=/mnt/f1-logs
pct set 101 -mp1 /tank/f1/test/selenium,mp=/mnt/f1-sel
pct set 103 -mp0 /tank/f1/prod/logs,mp=/mnt/f1-logs
pct set 103 -mp1 /tank/f1/prod/selenium,mp=/mnt/f1-selNotes:
101654:101654matches theappuser (UID/GID1654:1654) used by themcr.microsoft.com/dotnet/aspnet:8.0runtime image inside the current unprivileged LXCs (f1-testandf1-prod). Confirm withdocker exec <container> id.- Restart the containers after adding
mpmounts. - Inside each LXC, set
HOST_LOG_PATH=/mnt/f1-logsin the deployment.envbefore recreating the API container. - The
seleniummount is reserved for manual or self-hosted test runs. GitHub Actions still uploads CI artifacts to the workflow run rather than writing them to Proxmox storage.
After the mount is in place and the API container has been recreated, use the following commands inside f1-test or f1-prod.
List log files:
ls -lah /mnt/f1-logsFollow the newest log file:
tail -f "$(ls -1t /mnt/f1-logs | head -n 1 | sed 's#^#/mnt/f1-logs/#')"Filter auth failures by reason code:
grep 'reasonCode' /mnt/f1-logs/*.log | grep 'missing_jwt_header\|missing_email_claim\|token_invalid\|token_expired'Pretty-print compact JSON logs with jq:
jq -c 'select(.eventName == "auth_failure") | {time: .["@t"], reason: .reasonCode, path: .Path, statusCode: .StatusCode, requestId: .RequestId, traceId: .TraceId}' /mnt/f1-logs/*.logFind a single request by request ID:
grep 'YOUR_REQUEST_ID' /mnt/f1-logs/*.logInspect manual Selenium artifacts when they exist:
ls -lah /mnt/f1-sel
find /mnt/f1-sel -type f | sortQuality and analysis gates are intentionally scoped to F1 app code:
- Included:
src/F1.Api,src/F1.Core,src/F1.Infrastructure,src/F1.Services,src/F1.Web,tests/F1.Api.Tests,tests/F1.Web.Tests - Excluded from quality/build gates:
src/PopulateF1Database*
The PopulateF1Database projects remain in the repository for ingestion workflows but are not part of the F1 API/Web quality gate path.
Development & Running the App
This project supports two primary ways to run the application on a Linux VM via Docker. Both modes utilize the F1Competition.sln but handle code execution differently.
- Normal Mode (Testing/Standard Run)
Use this mode to run the app as a "release candidate." This uses the multi-stage Dockerfile and runs the compiled binaries. It is the closest representation of the production environment.
How to run: ./build.sh
Behavior: Compiles the app once during the build phase.
Environment: Defaults to Development (via docker-compose.yml) to enable Cloudflare header mocking.
Use Case: Verifying overall system stability, UI/UX testing, and preparing for deployment.
- Debug Mode (Active Development)
Use this mode when you need to set breakpoints, inspect variables, or use "Hot Reload." This mode mounts your source code directly into the container and runs dotnet watch.
How to run: ./build.sh --debug
Behavior: Uses dotnet watch run to restart the app whenever you save a file on the VM.
Debugger: Requires vsdbg (installed automatically via Dockerfile.debug).
Use Case: Fixing bugs, developing new features, and step-through debugging.
Then ctrl+shft+d (or f5) and .Net COre Docker Attach
Troubleshooting Tips
Container Crashing? Check the logs immediately: docker logs -f f1-local.
Environment Mismatch? If the logs say Hosting environment: Production, the identity mock is disabled. Ensure ASPNETCORE_ENVIRONMENT=Development is set in your docker-compose.yml.
Process Picker Empty? Ensure the C# Dev Kit and Docker extensions are installed on the Remote SSH host, not just your local machine.
Stale DLLs? If the app is acting like old code, the build.sh script handles the clean automatically, but you can run it manually:
find . -type d −name"bin"−o−name"obj" -exec rm -rf {} +
Once running, the services will be available at:
- API:
http://localhost:5000 - Web App:
http://localhost:5001
Symptom: You updated .env (for example CLOUDFLARE_AUDIENCE, Postgres settings, or TAG) but the app still uses the old value.
Cause: Docker Compose only reads .env when creating containers. Watchtower updates the image but reuses the existing container configuration.
Fix: Manually recreate the container to apply changes:
bash +docker-compose up -d
docker-compose.yml currently maps API ASPNETCORE_ENVIRONMENT from BLAZOR_ENVIRONMENT.
f1-api:ASPNETCORE_ENVIRONMENT=${BLAZOR_ENVIRONMENT:-Development}f1-web:BLAZOR_ENVIRONMENT=${BLAZOR_ENVIRONMENT:-Development}
If you need independent values, split these into separate env vars (for example API_ENVIRONMENT and BLAZOR_ENVIRONMENT).
This project is licensed under the Unlicense. See the LICENSE file for details.