💡 The full stack — MongoDB or PostgreSQL / SQL Server, Redis, Kafka, the API microservice, and the background worker — ships in one product. Run the pieces you need; MongoDB single-process is the simplest default.
Building
Local Build
# Build the entire solutiondotnet build BlazorBlueprint.sln --configuration Release# Run testsdotnet test BlazorBlueprint.sln --configuration Release# Publish for deploymentdotnet publish Web/BlazorBlueprint.Web/BlazorBlueprint.Web.csproj --configuration Release --output ./publish/webDocker Build
# Build all images with Docker Compose (build context = repo root)docker compose -f .azure/devops/docker-compose.yml build# Or build an individual imagedocker build -f Web/BlazorBlueprint.Web/Dockerfile -t blazorblueprint-web .docker build -f Services/BlazorBlueprint.ApplicationService/Dockerfile -t blazorblueprint-applicationservice .Deployment
Quick Deployment (Docker Compose)
# Render secrets first (see "Manual Deployment" below), then:# Only when your images come from a registry — set WEB_IMAGE / APP_IMAGE /# WORKER_IMAGE / MIGRATOR_IMAGE first. Skip this if you built them locally.docker compose -f .azure/devops/docker-compose.yml pull# Gated migration, then start the stackdocker compose -f .azure/devops/docker-compose.yml run --rm blazorblueprint-dbmigratordocker compose -f .azure/devops/docker-compose.yml up -d# View logsdocker compose -f .azure/devops/docker-compose.yml logs -f# Stop services (NEVER add -v in prod — it deletes the key ring + data)docker compose -f .azure/devops/docker-compose.yml down✅ What the stack includes: the complete production stack — MongoDB, Redis, Kafka, Web, API, Background Worker, and a gated DbMigrator — with persistent named volumes and an isolated network. (.NET Aspire development does not include MongoDB — you provide that separately.)
Manual Deployment (no CI/CD)
You can run the full production stack (MongoDB, Redis, Kafka, Application Service, Background Worker, and Web) on a single host without a pipeline. Build the images, render the secrets file, then bring the stack up — the same steps the pipeline automates.
# 1. Build the images locally (or pull them from your registry)docker compose -f .azure/devops/docker-compose.yml build# 2. Export your secrets, then render the git-ignored deploy.env the stack readsexport WEBAPP_CLIENT_SECRET=... JWT_SECRET_KEY=... KAFKA_SIGNING_KEY=...bash scripts/render-deploy-env.sh .azure/devops/deploy.env# 3. Run the DB migrator (gated), then start the stackdocker compose -f .azure/devops/docker-compose.yml run --rm blazorblueprint-dbmigratordocker compose -f .azure/devops/docker-compose.yml up -d⚠️ Set strong, unique secrets before going to production. scripts/render-deploy-env.sh fails closed if any required key is empty, and the app refuses to boot on the shipped dev-only-change-me-… placeholders:
Kafka:MessageSigningKey— HMAC key that signs inter-service Kafka messages. If shared or default, any process that knows it can forge messages between Web, ApplicationService, and BackgroundWorker. Must be identical on Web and BackgroundWorker.Security:Jwt:SecretKey— signing key for the JWT the Web app uses for ApplicationService API calls.Security:WebApp:ClientSecret— client credential the Web app presents when requesting that JWT.- External auth provider secrets (
Authentication:Facebook:AppSecret,Authentication:Google:ClientSecret,Authentication:Microsoft:ClientSecret) — from each provider's dashboard.
Generate fresh values with openssl rand -base64 48 (or equivalent) and inject them via environment variables or your secret store — never commit real secrets to source control. The rendered deploy.env is git-ignored.
Production Considerations
- Persistent Storage: Docker volumes preserve data across deployments
- Environment Variables: Configure production settings via environment
- SSL/TLS: Add reverse proxy (nginx/Apache) for HTTPS
- Monitoring: Use built-in health checks and infrastructure monitor
- Backups: Regular MongoDB backups and configuration backups
- Scaling: single-node by default; run multiple instances behind a load balancer with a shared data tier, sticky sessions, and a single worker
- Background Processing: .NET Worker Service handles background tasks including push notifications, data cleanup, and AI workflows
CI/CD Setup
How the pipeline works
One multi-stage pipeline builds, tests, and deploys — no separate release step. The flow is identical on Azure DevOps and GitHub Actions, and the container registry is a setting you choose:
- Test — unit + relational integration tests and migration guards.
- Build — builds the four images (Web, ApplicationService, BackgroundWorker, DbMigrator) and pushes them to your registry with a readable version tag from the repo-root
VERSIONfile plus the CI build number — e.g.1.0.0.2517(immutable, what Deploy pins), alongside floating1.0.0andlatesttags. BumpVERSIONfor a release. - Deploy — renders secrets into a git-ignored
deploy.env, pulls the exact image tag, runs the DbMigrator as a gated one-shot (a failure blocks the deploy before the app is touched), thendocker compose upand a/healthcheck. Rollback = re-deploy a prior image tag.
Where the stages run: the pipeline ships with Test + Build on a self-hosted Default agent pool and Deploy on your server (an Environment VM resource). To run Test/Build on free Microsoft-hosted Linux agents instead, set their pool: to vmImage: 'ubuntu-latest'. The shell steps are cross-platform (a Git-Bash guard handles Windows), so a Windows or Linux self-hosted agent works either way — each just needs Docker, and its account authorised for Docker (LocalSystem or the docker-users group on Windows; the docker group on Linux).
📋 What You Need: a container registry — Docker Hub, Azure Container Registry, GHCR, or a private one (your choice; keep the repositories private, since these images contain your compiled app) — and a server with Docker installed, registered as a self-hosted agent.
Secrets — one variable set, both platforms
Deploy-time secrets live in an Azure DevOps variable group (or GitHub Actions secrets), never in the pipeline files. The Deploy stage maps them into scripts/render-deploy-env.sh, which writes the git-ignored deploy.env that docker compose reads. Required (fail-closed) keys:
WEBAPP_CLIENT_SECRET # Web ⇄ ApplicationService client credentialJWT_SECRET_KEY # signs the internal API JWTKAFKA_SIGNING_KEY # HMAC key — identical on Web + BackgroundWorkerOptional keys (OAuth, email, OpenAI, OpenRouter, N8N, domain-verification) fall back to appsettings.json when omitted — see scripts/render-deploy-env.sh for the full list.
Pick your database backend
The stack runs on MongoDB (default), PostgreSQL, or SQL Server. Set DATABASE_TYPE in the variable group / Actions variables to postgresql or sqlserver and the Deploy stage picks the matching compose file (all three are equally pipeline-ready: gated migrator, secrets, pinned volumes). A relational backend also needs a database-password secret — POSTGRES_PASSWORD or MSSQL_SA_PASSWORD. The app images are backend-agnostic (the backend is chosen at runtime), so switching needs no rebuild — but switching a live deployment is a data migration, not just a setting change.
Choosing your container registry
The registry is a setting, not baked into the pipeline. Create the matching service connection (Azure DevOps) or set the REGISTRY_* variables (GitHub Actions), then point REGISTRY_BASE at the image prefix — the pipeline appends /<image-name>:<tag> to it:
ACR myregistry.azurecr.io → myregistry.azurecr.io/blazorblueprint-web:1234GHCR ghcr.io/your-org → ghcr.io/your-org/blazorblueprint-web:1234Docker Hub docker.io/your-user → docker.io/your-user/blazorblueprint-web:1234- ACR takes no owner segment — the registry itself is the namespace. GHCR and Docker Hub both need the org/username segment.
- No trailing slash, and GHCR must be lowercase —
ghcr.io/myorg, neverghcr.io/MyOrg, or the push fails. - Azure DevOps → GHCR uses a service connection of type Others: URL
https://ghcr.io, your GitHub username, and a PAT withwrite:packages+read:packages. - You don't pre-create repositories — the four images appear on first push. GHCR packages default to private; on Docker Hub check the visibility yourself, since its free tier allows only one private repository and this pipeline pushes four.
Azure DevOps setup
- Service connection: Project Settings → Service connections → New → Docker Registry, choosing your registry type — Docker Hub, Azure Container Registry, or Others (for GHCR and anything else). Name it exactly
registry-connection— the pipeline references that literal name, because Azure DevOps resolves service connections at compile time and a$(variable)there fails validation. The name is just a label: re-point this one connection to switch registries. - Environment — not a Deployment Group: Pipelines → Environments → New →
BlazorBlueprint-Production→ Add resource → Virtual machines, then run the generated script on your server (a few minutes, mostly downloading the agent). YAML pipelines cannot target a classic deployment group — if you are migrating from a classic Release, leave the old deployment group in place; this installs its own agent alongside it. Add an approval check here for a manual gate. - Variable group: Pipelines → Library →
BlazorBlueprint-ProductionwithREGISTRY_BASE(the image prefix, e.g.docker.io/<user>ormyacr.azurecr.io), plus the secrets above (lock the secret ones). - Authorize the connection: on the first run Azure DevOps asks you to permit the pipeline to use
registry-connection— approve it, or pre-grant access from the connection's Security tab. - Pipeline: New pipeline → Existing Azure Pipelines YAML →
.azure/devops/build-pipeline.yml. Push tomainto build + deploy.
⚠️ Windows agent + Docker Desktop — the most common first-deploy failure. The VM registration script runs the agent as a service, defaulting to NT AUTHORITY\NETWORK SERVICE, which normally cannot reach Docker Desktop (it is per-user, exposed over the \\.\pipe\docker_engine named pipe). After registering, open services.msc → vstsagent.* → Properties → Log On, set an account belonging to the docker-users group, and restart the service. Skip it and the deploy runs fine until the compose step, then fails with cannot connect to the Docker daemon — which looks like a pipeline bug but is purely the service account.
💡 Define only the variables you use. Unused optional variables can be left out of the group entirely — the render script treats an undefined $(VAR) macro as unset, so the app falls back to appsettings.json. The three required secrets fail the deploy immediately if missing.
GitHub Actions setup
- No registry setup needed — images push to
ghcr.io/<your-org>using the built-inGITHUB_TOKEN. To use Docker Hub or ACR instead, set theREGISTRY_SERVER,REGISTRY_USERNAMEandREGISTRY_BASEvariables plus aREGISTRY_PASSWORDsecret. - Self-hosted runner: Settings → Actions → Runners → New self-hosted runner, installed on your server (with Docker + Git Bash). Open ports 8080 (Web) and 8081 (API).
- Environment: Settings → Environments →
production(add a required reviewer for a manual gate). - Secrets & variables: Settings → Secrets and variables → Actions — add the secret keys above; put the non-secret config (client IDs, email/OpenAI toggles) under Variables.
- Deploy: push to
main, or use "Run workflow" with animage_tagto roll back to a prior build.
What gets deployed
- Web Application: Blazor Server app on port 8080
- API Service: Application service on port 8081
- Background Worker: .NET Worker for async tasks and queue processing
- DbMigrator: one-shot schema/index runner, gated ahead of the app
- Kafka: message queue for background job processing
- MongoDB: database with persistent storage — internal to the compose network, not published to the host
- Redis: cache + Data Protection key ring, persistent storage — internal-only (never exposed to the host, since it holds the key ring)
- Networking: isolated Docker network; data preserved in pinned named volumes across deploys
Production Environment
Infrastructure Requirements
- Server: 4+ CPU cores, 8GB+ RAM, 20GB+ storage
- Docker: Docker Engine 20.10+ or Docker Desktop, with Docker Compose v2.24+ (required for the
env_filehandling the deploy relies on) - Network: Ports 8080 (Web), 8081 (API), 80/443 (HTTP/HTTPS)
- SSL Certificate: For HTTPS (Let's Encrypt recommended)
Security Configuration
- Environment Variables: Store secrets in environment, not appsettings.json
- Database Security: Enable MongoDB authentication and encryption
- Redis Security: Configure Redis AUTH and disable dangerous commands
- HTTPS Only: Force HTTPS redirects and secure cookies
- CORS: Configure appropriate CORS policies
Monitoring & Maintenance
- Health Checks: Monitor /health endpoint
- Infrastructure Monitor: Use built-in dashboard
- Logging: Configure structured logging and log aggregation
- Backups: Automated MongoDB backups and disaster recovery
- Updates: Regular security updates and dependency updates
Performance Optimization
- Caching: Redis distributed caching is enabled by default
- CDN: Use CDN for static assets and images
- Database: Create appropriate MongoDB indexes
- Scaling: Compose-based single-node by default; Kubernetes-ready if you supply your own manifests (none ship in the box)
- Background Processing: Queue-based background processing enables horizontal scaling of worker services
- Compression: Enable gzip compression in reverse proxy
Ready to Download?
Deploy your SaaS application with confidence. Download the full product free — use it, including commercially, while you're under £100k/year revenue; a commercial licence (from £499) applies above that.
🔓 Source-available on GitHub • Free under £100k/yr revenue • Commercial licence (from £499) above that • Full source code included