mirror of
https://github.com/aaronpo97/the-biergarten-app.git
synced 2026-10-14 17:54:42 +00:00
559 lines
18 KiB
Markdown
559 lines
18 KiB
Markdown
---
|
|
title: Docker deployment, configuration, and troubleshooting
|
|
last-updated: 2026-08-31
|
|
tags:
|
|
- 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](diagrams/deployment.md) 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**:
|
|
|
|
```yaml
|
|
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**:
|
|
|
|
```bash
|
|
docker compose --env-file web/.env.dev -f web/docker-compose.dev.yaml up -d
|
|
```
|
|
|
|
**Access**:
|
|
|
|
- Website: http://localhost:3000
|
|
- API Swagger: http://localhost:8080/swagger
|
|
- Health Check: http://localhost:8080/health
|
|
- Mailpit UI: http://localhost:8025
|
|
- SQL Server: localhost:1434 (sa credentials from .env.dev)
|
|
- SeaweedFS S3 API: http://localhost:8333 (credentials from
|
|
`SEAWEEDFS_ACCESS_KEY_ID`/`SEAWEEDFS_SECRET_ACCESS_KEY` in .env.dev)
|
|
- SeaweedFS Filer UI: http://localhost:8888
|
|
- SeaweedFS Master UI: http://localhost:9333
|
|
|
|
**Stop Environment**:
|
|
|
|
```bash
|
|
# 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**:
|
|
|
|
```yaml
|
|
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**:
|
|
|
|
```bash
|
|
# 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](testing.md#running-tests-with-docker-recommended) for details.
|
|
|
|
### 3. Production (`docker-compose.prod.yaml`)
|
|
|
|
**Purpose**: Production-ready deployment
|
|
|
|
**Features**:
|
|
|
|
- Production logging levels
|
|
- No database clearing
|
|
- Optimized build configurations
|
|
- Health checks on every long-running service
|
|
- Restart policies that survive a reboot (see
|
|
[Restart tolerance](#restart-tolerance))
|
|
- 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` (named `biergarten-prodnet`
|
|
so the seed job can join it from its own Compose project)
|
|
- Seeding is a separate, explicitly run compose file
|
|
(`docker-compose.prod.seed.yaml`), so an `up` or a restart leaves the existing
|
|
catalogue as it is. See [Production data](#production-data) below.
|
|
|
|
**Services**:
|
|
|
|
```yaml
|
|
sqlserver # Production SQL Server (internal only)
|
|
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)
|
|
```
|
|
|
|
Migrating and seeding are run-once jobs kept outside this file, so an `up` or a
|
|
restart touches neither the schema nor the catalogue. Run them explicitly at
|
|
deploy time: see [Migrating production](#migrating-production) and
|
|
[Seeding production](#seeding-production-docker-composeprodseedyaml).
|
|
|
|
**Deploy Production**:
|
|
|
|
```bash
|
|
# 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.
|
|
|
|
#### Migrating production
|
|
|
|
`database.migrations` is a run-once job rather than a service in the prod stack,
|
|
so the schema is migrated when an operator asks for it instead of on every boot.
|
|
Build the image and run it against the network the prod stack creates:
|
|
|
|
```bash
|
|
docker build -t database.migrations \
|
|
-f web/backend/Database/Database.Migrations/Dockerfile \
|
|
--build-arg BUILD_CONFIGURATION=Release --build-arg APP_UID=1000 \
|
|
web/backend
|
|
|
|
docker run --rm --env-file web/.env.prod --network biergarten-prodnet \
|
|
-e DOTNET_RUNNING_IN_CONTAINER=true database.migrations
|
|
```
|
|
|
|
DbUp records every script it has applied, so a re-run applies only what is new.
|
|
Run this with `sqlserver` already up and healthy, and before the seed job.
|
|
|
|
#### Restart tolerance
|
|
|
|
`sqlserver`, `api.core`, `seaweedfs` and `frontend` run under
|
|
`restart: unless-stopped`, so the stack returns on its own after a container
|
|
crash, a Docker daemon restart or a host reboot, provided Docker itself starts at
|
|
boot (`systemctl enable docker`). The migration and seed jobs stay out of the
|
|
stack, so a reboot replays neither.
|
|
|
|
The daemon replays restart policies without replaying the `depends_on` graph, so
|
|
after a reboot the containers come back in arbitrary order and each one tolerates
|
|
a dependency that arrives late. `api.core` opens its database and S3 connections
|
|
per request rather than at startup, and `frontend` answers with its own error
|
|
states while `api.core` is still coming up.
|
|
|
|
Health checks report when the stack has converged:
|
|
|
|
| Service | Probe |
|
|
| --- | --- |
|
|
| `sqlserver` | `sqlcmd -Q 'SELECT 1'`, with a 30s start period and 12 retries |
|
|
| `api.core` | `GET /health` spoken over bash's `/dev/tcp` — the .NET runtime image carries bash and no HTTP client |
|
|
| `seaweedfs` | `curl http://localhost:8333/status`, the S3 gateway, which serves once the master, volume and filer servers are up |
|
|
| `frontend` | `wget --spider http://127.0.0.1:5656/healthz`, answered by Express alone and so independent of the API |
|
|
|
|
`sqlserver` also carries `stop_grace_period: 60s`, giving a checkpoint time to
|
|
finish on shutdown instead of being recovered on the next start.
|
|
|
|
Deploy and wait for the stack to report healthy, or check convergence after a
|
|
reboot:
|
|
|
|
```bash
|
|
docker compose --env-file web/.env.prod -f web/docker-compose.prod.yaml up -d --wait
|
|
docker compose --env-file web/.env.prod -f web/docker-compose.prod.yaml ps
|
|
```
|
|
|
|
#### Seeding production (`docker-compose.prod.seed.yaml`)
|
|
|
|
`database.seed` lives in its own compose file, run on demand once the prod stack
|
|
is up, `sqlserver` is healthy, and the schema is migrated:
|
|
|
|
```bash
|
|
docker compose --env-file web/.env.prod -f web/docker-compose.prod.seed.yaml \
|
|
run --rm --build database.seed
|
|
```
|
|
|
|
The job runs as the `biergarten-prod-seed` Compose project and attaches to the
|
|
external `biergarten-prodnet` network, which the main prod stack creates, so it
|
|
reaches `sqlserver` and `seaweedfs` under the same hostnames the API uses. It
|
|
exits when seeding finishes; `--rm` removes the container. Bring the prod stack
|
|
up first, because the seed project has no visibility into the other project's
|
|
health checks, so a run started before SQL Server is accepting connections fails.
|
|
|
|
#### Production data
|
|
|
|
A production deployment seeded this way serves the AI-generated fixture data
|
|
produced by the pipeline. Per
|
|
[Pipeline ethics, bias, and known issues](../pipeline/ETHICS-AND-KNOWN-ISSUES.md),
|
|
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/home/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 the seed job and the dialog together: any deployment that runs
|
|
`docker-compose.prod.seed.yaml` needs the disclosure in place to 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**:
|
|
|
|
```yaml
|
|
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**:
|
|
|
|
```bash
|
|
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**:
|
|
|
|
```yaml
|
|
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**:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```mermaid
|
|
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`.
|
|
|
|
Production has a shorter chain, because the migration and seed jobs run outside
|
|
the stack: `api.core` waits for `seaweedfs` to pass its health check, and
|
|
`frontend` waits for `api.core` to report healthy. This ordering applies to
|
|
`docker compose up`; see [Restart tolerance](#restart-tolerance) for what happens
|
|
on a host reboot.
|
|
|
|
**Health Check Example** (SQL Server):
|
|
|
|
```yaml
|
|
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**:
|
|
|
|
```yaml
|
|
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`
|
|
|
|
See [Production data volumes](#production-data-volumes).
|
|
|
|
### Mounted volumes
|
|
|
|
**Test Results**:
|
|
|
|
```yaml
|
|
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](#3-production-docker-composeprodyaml). External volumes also
|
|
carry their literal name rather than a `<project>_` prefix, so `docker volume ls`
|
|
shows `biergartendata-prod` and `seaweedfsdata-prod` unprefixed, unlike the
|
|
`web_`-prefixed volumes of the dev and test stacks.
|
|
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```bash
|
|
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:
|
|
|
|
```yaml
|
|
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](environment-variables.md).
|
|
|
|
## Common commands
|
|
|
|
### View services
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```bash
|
|
# 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
|
|
|
|
```dockerfile
|
|
# 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
|
|
|
|
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
|
- [.NET Docker Images](https://hub.docker.com/_/microsoft-dotnet)
|
|
- [SQL Server Docker Images](https://hub.docker.com/_/microsoft-mssql-server)
|