🐳 dotnet-starter-kit

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 Ready 8 Services Ports: 8080, 8081, 8082 6,606 stars
1. Pull Image docker pull library/postgres
2. 1-Click Launch Command docker run -d -p 8080:8080 --name dotnet-starter-kit postgres:18-alpine
3. Compose Deployment docker compose up -d
deploy/docker/docker-compose.yml
Download .yml
# 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: