Files
the-biergarten-app/docs/web/docker.md

14 KiB

title, last-updated, tags
title last-updated tags
Docker deployment, configuration, and troubleshooting 2026-08-31
docker
docker-compose
deployment

This document covers Docker deployment, configuration, and troubleshooting for The Biergarten App.

Overview

The project uses Docker Compose to orchestrate multiple services:

  • SQL Server 2022 database
  • Database migrations runner (DbUp)
  • Database seeder
  • .NET API
  • React Router website (SSR frontend)
  • Test runners

See the deployment diagram for visual representation.

Docker Compose environments

1. Development (docker-compose.dev.yaml)

Purpose: Local development with persistent data

Features:

  • Persistent SQL Server volume
  • NuGet package cache volume (speeds up rebuilds)
  • Swagger UI enabled
  • Seed data included
  • Local Mailpit SMTP server for dev email testing
  • Local SeaweedFS S3-compatible object storage for photo uploads
  • CLEAR_DATABASE=true (drops and recreates schema)

Services:

sqlserver           # SQL Server 2022 (port 1434)
database.migrations # DbUp migrations
database.seed      # Seed initial data
api.core           # Web API (ports 8080, 8081)
frontend           # React Router website (port 3000)
mailpit            # Local dev SMTP server + UI (port 8025)
seaweedfs           # S3-compatible object storage (ports 8333, 8888, 9333, 23646)

Start Development Environment:

docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d

Access:

Stop Environment:

# Stop services (keep volumes)
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down

# Stop and remove volumes (fresh start)
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v

2. Testing (docker-compose.test.yaml)

Purpose: Automated CI/CD testing in an isolated environment

Features:

  • Fresh database each run
  • All test suites execute in parallel
  • Test results exported to ./test-results/
  • Containers auto-exit after completion
  • Fully isolated testnet network

Services:

sqlserver           # Test database
database.migrations # Fresh schema
database.seed      # Test data
api.specs          # Reqnroll BDD tests
unit.tests         # All Features.*.Tests and Shared.*.Tests unit test projects
frontend.tests     # Storybook Vitest + Playwright suites (own Playwright image)

Run Tests:

# Run all tests
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml up -d
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml wait api.specs unit.tests frontend.tests

# View results
ls -la test-results/
cat test-results/api-specs/results.trx
cat test-results/Features.Users.Tests.trx

# Clean up
docker compose --env-file web/.env.test -f web/docker-compose.test.yaml down -v

wait blocks until api.specs, unit.tests, and frontend.tests have all stopped, regardless of which one finishes first; up --abort-on-container-exit isn't used here because the one-shot database.migrations/database.seed jobs and frontend.tests (no database dependency) can exit before the other test containers, which would end the run prematurely. See Testing for details.

3. Production (docker-compose.prod.yaml)

Purpose: Production-ready deployment

Features:

  • Production logging levels
  • No database clearing
  • Optimized build configurations
  • Health checks enabled
  • Restart policies (unless-stopped)
  • Security hardening
  • Only frontend publishes a port to the host, bound to 127.0.0.1 so a reverse proxy can sit in front of it; sqlserver, api.core and seaweedfs are reachable only from other containers on prodnet
  • Seeded catalogue, with its AI-generated origin disclosed in the app. See Production data below.

Services:

sqlserver          # Production SQL Server (internal only)
database.migrations # Schema updates only
database.seed      # Loads the catalogue, runs once
api.core          # Production API (internal only, no published ports)
seaweedfs         # S3-compatible object storage (internal only)
frontend          # Production website (127.0.0.1:5656, the only host binding)

Deploy Production:

# One time, before the first `up`: the data volumes are external, so Compose
# expects them to exist already.
docker volume create biergartendata-prod
docker volume create seaweedfsdata-prod

docker compose --env-file web/.env.prod -f web/docker-compose.prod.yaml up -d

Docker seeds each volume with the ownership the image expects and labels it for SELinux, so the host needs no chown and no :z/:Z mount options on enforcing distributions such as RHEL, AlmaLinux and Fedora.

Production data

Production runs database.seed, so the catalogue it serves is the AI-generated fixture data produced by the pipeline. Per Pipeline ethics, bias, and known issues, anyone interacting with an application seeded from it has to be told upfront that the content is AI-generated.

The website meets that obligation with AiDisclosureDialog (web/frontend/app/features/disclosure/components/AiDisclosureDialog.tsx), which the root route renders on every full page load, whichever route the visitor lands on. It is part of the server-rendered HTML rather than mounted after hydration, so the disclosure is on screen with the first paint. Acknowledging it hides the dialog for the remainder of that page load; a new load shows it again.

Keep database.seed and the dialog together. A production deployment that seeds the catalogue without the disclosure does not satisfy the ethics documentation.

4. Database only (docker-compose.db.yaml)

Purpose: Run just the database for local backend development outside Docker (for example, dotnet run against a containerized SQL Server)

Services:

sqlserver           # SQL Server 2022 (port 1434)
database.migrations # DbUp migrations
database.seed      # Seed initial data

No api.core or mailpit container. Uses .env.dev, same as development.

Start:

docker compose --env-file web/.env.dev -f web/docker-compose.db.yaml up -d

5. Minimal (docker-compose.min.yaml)

Purpose: Local dev where the API runs outside Docker but the database and mail capture still run in containers

Services:

sqlserver           # SQL Server 2022 (port 1433)
database.migrations # DbUp migrations
database.seed      # Seed initial data
mailpit            # Local dev SMTP server + UI (port 8025)

No api.core container. Uses .env.local instead of .env.dev, and exposes SQL Server on the standard port 1433 (docker-compose.db.yaml and docker-compose.dev.yaml both use 1434).

Start:

docker compose --env-file web/.env.local -f web/docker-compose.min.yaml up -d

Service dependencies

Docker Compose manages startup order using health checks:

graph TD
    A[sqlserver: health check passes] --> B[database.migrations: completes successfully]
    B --> C[database.seed: completes successfully]
    C --> D[api.core / tests: start when ready]
    E[seaweedfs: container started] --> D

In development, api.core also depends on seaweedfs (condition: service_started, not a health check) alongside database.seed.

Health Check Example (SQL Server):

healthcheck:
  test:
    [
      "CMD-SHELL",
      "sqlcmd -S localhost -U sa -P '${DB_PASSWORD}' -C -Q 'SELECT 1'",
    ]
  interval: 10s
  timeout: 5s
  retries: 12
  start_period: 30s

Dependency Configuration:

api.core:
  depends_on:
    database.seed:
      condition: service_completed_successfully

Volumes

Persistent volumes

Development:

  • sqlserverdata-dev - Database files persist between restarts
  • nuget-cache-dev - NuGet package cache (speeds up builds)
  • seaweedfsdata-dev - SeaweedFS object storage data persists between restarts

Testing:

  • sqlserverdata-test - Temporary, typically removed after tests

Production:

  • biergartendata-prod - Database files, external; survives down -v
  • seaweedfsdata-prod - SeaweedFS object storage, external; survives down -v
  • nuget-cache-prod - Production NuGet cache

See Production data volumes.

Mounted volumes

Test Results:

volumes:
  - ./test-results:/app/test-results

Test results are written to host filesystem for CI/CD integration.

Production data volumes

biergartendata-prod and seaweedfsdata-prod are declared external: true. Compose removes only the volumes it owns, so docker compose down -v tears the stack down and leaves the database and the uploaded photos in place. Deleting them takes an explicit docker volume rm.

The trade-off is that Compose creates neither one, and up fails with a clear error until both exist — hence the docker volume create step in Deploy Production. External volumes also carry their literal name rather than a <project>_ prefix, which is why docker volume ls shows biergartendata-prod next to web_nuget-cache-prod.

Both live under /var/lib/docker/volumes. To put them on a specific disk, point the daemon's data-root at it in /etc/docker/daemon.json, or mount the disk at /var/lib/docker.

Back up the database through SQL Server rather than by copying its files, since a file copy taken while the engine is running can be torn:

docker exec prod-env-sqlserver /opt/mssql-tools18/bin/sqlcmd \
  -S localhost -U sa -P '<db-password>' -C \
  -Q "BACKUP DATABASE [<db-name>] TO DISK = '/var/opt/mssql/data/backup.bak'"
docker cp prod-env-sqlserver:/var/opt/mssql/data/backup.bak .

Object storage can be archived directly, with the stack stopped:

docker run --rm -v seaweedfsdata-prod:/data -v "$PWD:/backup" \
  alpine tar czf /backup/seaweedfs.tar.gz -C /data .

Networks

Each environment uses isolated bridge networks:

  • devnet - Development network
  • testnet - Testing network (fully isolated)
  • prodnet - Production network

Environment variables

All containers are configured via environment variables from .env files:

env_file: ".env.dev" # or .env.test, .env.prod

environment:
  ASPNETCORE_ENVIRONMENT: "Development"
  DOTNET_RUNNING_IN_CONTAINER: "true"
  DB_SERVER: "${DB_SERVER}"
  DB_NAME: "${DB_NAME}"
  DB_USER: "${DB_USER}"
  DB_PASSWORD: "${DB_PASSWORD}"
  ACCESS_TOKEN_SECRET: "${ACCESS_TOKEN_SECRET}"
  REFRESH_TOKEN_SECRET: "${REFRESH_TOKEN_SECRET}"
  CONFIRMATION_TOKEN_SECRET: "${CONFIRMATION_TOKEN_SECRET}"
  SEAWEEDFS_SERVICE_URL: "${SEAWEEDFS_SERVICE_URL}"
  SEAWEEDFS_ACCESS_KEY_ID: "${SEAWEEDFS_ACCESS_KEY_ID}"
  SEAWEEDFS_SECRET_ACCESS_KEY: "${SEAWEEDFS_SECRET_ACCESS_KEY}"
  SEAWEEDFS_BUCKET: "${SEAWEEDFS_BUCKET}"

For complete list, see Environment Variables.

Common commands

View services

# Running services
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml ps

# All containers (including stopped)
docker ps -a

View logs

# All services
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs -f

# Specific service
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs -f api.core

# Last 100 lines
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml logs --tail=100 api.core

Execute commands in container

# Interactive shell
docker exec -it dev-env-api-core bash

# Run command (replace <db-password> with the value of DB_PASSWORD from .env.dev)
docker exec dev-env-sqlserver /opt/mssql-tools18/bin/sqlcmd -S localhost -U sa -P '<db-password>' -C

Restart services

# Restart all services
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml restart

# Restart specific service
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml restart api.core

# Rebuild and restart
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d --build api.core

Build images

# Build all images
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build

# Build specific service
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build api.core

# Build without cache
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml build --no-cache

Clean up

# Stop and remove containers
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down

# Remove containers and volumes
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v

# Remove containers, volumes, and images
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml down -v --rmi all

# System-wide cleanup
docker system prune -af --volumes

Dockerfile structure

Multi-stage build

# Stage 1: Build
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src
COPY ["Project/Project.csproj", "Project/"]
RUN dotnet restore
COPY . .
RUN dotnet build -c Release

# Stage 2: Runtime
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
COPY --from=build /app/build .
ENTRYPOINT ["dotnet", "Project.dll"]

Additional resources