Deployment: Docker & CI/CD — Blazor Blueprint
Login Register

💡 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 solution
dotnet build BlazorBlueprint.sln --configuration Release

# Run tests
dotnet test BlazorBlueprint.sln --configuration Release

# Publish for deployment
dotnet publish Web/BlazorBlueprint.Web/BlazorBlueprint.Web.csproj --configuration Release --output ./publish/web

Docker Build

# Build all images with Docker Compose (build context = repo root)
docker compose -f .azure/devops/docker-compose.yml build

# Or build an individual image
docker 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 stack
docker compose -f .azure/devops/docker-compose.yml run --rm blazorblueprint-dbmigrator
docker compose -f .azure/devops/docker-compose.yml up -d

# View logs
docker 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 reads
export 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 stack
docker compose -f .azure/devops/docker-compose.yml run --rm blazorblueprint-dbmigrator
docker 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 VERSION file plus the CI build number — e.g. 1.0.0.2517 (immutable, what Deploy pins), alongside floating 1.0.0 and latest tags. Bump VERSION for 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), then docker compose up and a /health check. 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 credential
JWT_SECRET_KEY # signs the internal API JWT
KAFKA_SIGNING_KEY # HMAC key — identical on Web + BackgroundWorker

Optional 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:1234
GHCR       ghcr.io/your-org       → ghcr.io/your-org/blazorblueprint-web:1234
Docker 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 lowercaseghcr.io/myorg, never ghcr.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 with write: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

  1. 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.
  2. 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.
  3. Variable group: Pipelines → Library → BlazorBlueprint-Production with REGISTRY_BASE (the image prefix, e.g. docker.io/<user> or myacr.azurecr.io), plus the secrets above (lock the secret ones).
  4. 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.
  5. Pipeline: New pipeline → Existing Azure Pipelines YAML → .azure/devops/build-pipeline.yml. Push to main to 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.mscvstsagent.*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

  1. No registry setup needed — images push to ghcr.io/<your-org> using the built-in GITHUB_TOKEN. To use Docker Hub or ACR instead, set the REGISTRY_SERVER, REGISTRY_USERNAME and REGISTRY_BASE variables plus a REGISTRY_PASSWORD secret.
  2. 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).
  3. Environment: Settings → Environments → production (add a required reviewer for a manual gate).
  4. Secrets & variables: Settings → Secrets and variables → Actions — add the secret keys above; put the non-secret config (client IDs, email/OpenAI toggles) under Variables.
  5. Deploy: push to main, or use "Run workflow" with an image_tag to 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_file handling 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

Welcome! How can we help you today?
An unhandled error has occurred. Reload