Production Grade Cloud-Ready .NET 10 Starter Kit (Web API + React Client) with Multitenancy Support, and Clean/Modular Architecture that saves roughly 200+ Development Hours! All Batteries Included.
docker pull library/postgres
docker run -d -p 8080:8080 --name dotnet-starter-kit postgres:18-alpine
docker compose up -d
# fullstackhero — production docker-compose.
# Run from this directory:
# cp .env.example .env && $EDITOR .env
# docker compose up -d --build
#
# Operator owns the edge (TLS / subdomain routing). This file publishes
# only the FSH services on host ports; the data plane (postgres/redis/
# minio) stays compose-internal unless you uncomment their ports blocks.
name: fsh
services:
postgres:
image: postgres:18-alpine
container_name: fsh-postgres
restart: unless-stopped
environment:
POSTGRES_DB: fsh
POSTGRES_USER: fsh
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:?POSTGRES_PASSWORD is required}
volumes:
- pg_data:/var/lib/postgresql/data
- ./postgres-init:/docker-entrypoint-initdb.d:ro
healthcheck:
test: ["CMD-SHELL", "pg_isready -U fsh -d fsh"]
interval: 5s
timeout: 3s
retries: 12
# Uncomment to expose for host psql / pg_dump access:
# ports:
# - "5432:5432"
redis:
image: valkey/valkey:9.1.0-alpine
container_name: fsh-redis
restart: unless-stopped
command:
- valkey-server
- --requirepass
- ${REDIS_PASSWORD:?REDIS_PASSWORD is required}
- --appendonly
- "yes"
volumes:
- redis_data:/data
healthcheck:
test: ["CMD-SHELL", "valkey-cli -a $$REDIS_PASSWORD PING | grep PONG"]
interval: 5s
timeout: 3s
retries: 12
environment:
REDIS_PASSWORD: ${REDIS_PASSWORD}
# Uncomment to expose for host redis-cli:
# ports:
# - "6379:6379"
minio:
image: minio/minio:latest
container_name: fsh-minio
restart: unless-stopped
command: ["server", "/data", "--console-address", ":9001"]
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER:?MINIO_ROOT_USER is required}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD:?MINIO_ROOT_PASSWORD is required}
volumes:
- minio_data:/data
healthcheck:
test: ["CMD", "mc", "ready", "local"]
interval: 10s
timeout: 3s
retries: 12
# Uncomment to expose the console (admin UI) + S3 API to the host:
# ports:
# - "9000:9000" # S3 API
# - "9001:9001" # Web console
# One-shot: create the bucket the Files module writes to. The app never
# auto-creates it (S3StorageService just PutObjects into Storage:S3:Bucket),
# so without this the first upload fails with NoSuchBucket. No anonymous
# policy is set — objects are served via the API / presigned URLs, not a
# public bucket.
minio-init:
image: minio/mc:latest
container_name: fsh-minio-init
restart: "no"
depends_on:
minio: { condition: service_healthy }
environment:
MINIO_ROOT_USER: ${MINIO_ROOT_USER}
MINIO_ROOT_PASSWORD: ${MINIO_ROOT_PASSWORD}
entrypoint: /bin/sh
command:
- -c
- |
mc alias set local http://minio:9000 "$$MINIO_ROOT_USER" "$$MINIO_ROOT_PASSWORD"
mc mb --ignore-existing local/fsh
migrator:
build:
context: ../..
dockerfile: src/Host/FSH.Starter.DbMigrator/Dockerfile
image: fsh/dbmigrator:local
container_name: fsh-migrator
restart: "no"
depends_on:
postgres: { condition: service_healthy }
environment:
DOTNET_ENVIRONMENT: Production
DatabaseOptions__ConnectionString: Host=postgres;Database=fsh;Username=fsh;Password=${POSTGRES_PASSWORD}
CachingOptions__Redis: redis,password=${REDIS_PASSWORD}
JwtOptions__SigningKey: ${JWT_SIGNING_KEY:?JWT_SIGNING_KEY is required}
Seed__DefaultAdminPassword: ${SEED_ADMIN_PASSWORD:?SEED_ADMIN_PASSWORD is required}
command: ["apply", "--seed"]
api:
build:
context: ../..
dockerfile: src/Host/FSH.Starter.Api/Dockerfile
image: fsh/api:local
container_name: fsh-api
restart: unless-stopped
depends_on:
postgres: { condition: service_healthy }
redis: { condition: service_healthy }
minio: { condition: service_healthy }
minio-init: { condition: service_completed_successfully }
migrator: { condition: service_completed_successfully }
environment:
DOTNET_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:8080
DatabaseOptions__ConnectionString: Host=postgres;Database=fsh;Username=fsh;Password=${POSTGRES_PASSWORD}
CachingOptions__Redis: redis,password=${REDIS_PASSWORD}
JwtOptions__SigningKey: ${JWT_SIGNING_KEY}
Seed__DefaultAdminPassword: ${SEED_ADMIN_PASSWORD}
Storage__Provider: s3
Storage__S3__ServiceUrl: http://minio:9000
Storage__S3__AccessKey: ${MINIO_ROOT_USER}
Storage__S3__SecretKey: ${MINIO_ROOT_PASSWORD}
Storage__S3__Bucket: fsh
Storage__S3__ForcePathStyle: "true"
AllowedHosts: "*"
HangfireOptions__UserName: ${HANGFIRE_USERNAME:?HANGFIRE_USERNAME is required}
HangfireOptions__Password: ${HANGFIRE_PASSWORD:?HANGFIRE_PASSWORD is required}
CorsOptions__AllowedOrigins__0: ${FSH_ADMIN_URL:?FSH_ADMIN_URL is required}
CorsOptions__AllowedOrigins__1: ${FSH_DASHBOARD_URL:?FSH_DASHBOARD_URL is required}
OpenTelemetryOptions__Exporter__Otlp__Endpoint: ${OTEL_EXPORTER_OTLP_ENDPOINT:-}
ports:
- "${FSH_API_PORT:-8080}:8080"
admin:
build:
context: ../../clients/admin
image: fsh/admin:local
container_name: fsh-admin
restart: unless-stopped
depends_on:
migrator: { condition: service_completed_successfully }
environment:
FSH_API_URL: ${FSH_API_URL:?FSH_API_URL is required}
FSH_DASHBOARD_URL: ${FSH_DASHBOARD_URL}
FSH_DEFAULT_TENANT: ${FSH_DEFAULT_TENANT:-root}
ports:
- "${FSH_ADMIN_PORT:-8081}:80"
dashboard:
build:
context: ../../clients/dashboard
image: fsh/dashboard:local
container_name: fsh-dashboard
restart: unless-stopped
depends_on:
migrator: { condition: service_completed_successfully }
environment:
FSH_API_URL: ${FSH_API_URL}
FSH_DEFAULT_TENANT: ${FSH_DEFAULT_TENANT:-root}
ports:
- "${FSH_DASHBOARD_PORT:-8082}:80"
volumes:
pg_data:
redis_data:
minio_data: