Skip to main content
The FullStackHero .NET Starter Kit uses .NET’s built-in container publishing capabilities for seamless Docker deployments. No Dockerfile needed—just publish directly to a container image.

Container Publishing

.NET 10 includes native support for building container images without a Dockerfile. The project files are pre-configured for containerization:
<Project Sdk="Microsoft.NET.Sdk.Web">
  <PropertyGroup>
    <RootNamespace>FSH.Playground.Api</RootNamespace>
    <AssemblyName>FSH.Playground.Api</AssemblyName>
    
    <!-- Container configuration -->
    <ContainerPort>8080</ContainerPort>
    <ContainerUser>root</ContainerUser>
    <ContainerRepository>fsh-playground-api</ContainerRepository>
    <PublishProfile>DefaultContainer</PublishProfile>
  </PropertyGroup>
</Project>

Build Container Images

Publish the API and Blazor projects as container images:
1

Build the API Container

dotnet publish src/Playground/Playground.Api/Playground.Api.csproj \
  -c Release \
  -r linux-x64 \
  -p:PublishProfile=DefaultContainer \
  -p:ContainerImageTags='"latest;1.0.0"'
This creates a container image fsh-playground-api:latest using the Microsoft-provided ASP.NET runtime image.
2

Build the Blazor Container

dotnet publish src/Playground/Playground.Blazor/Playground.Blazor.csproj \
  -c Release \
  -r linux-x64 \
  -p:PublishProfile=DefaultContainer \
  -p:ContainerImageTags='"latest;1.0.0"'
This creates a container image fsh-playground-blazor:latest.
3

Verify Images

docker images | grep fsh-playground
You should see:
fsh-playground-api      latest    abc123...   10 seconds ago   250MB
fsh-playground-blazor   latest    def456...   15 seconds ago   180MB
The base image is automatically selected based on your runtime (linux-x64 → mcr.microsoft.com/dotnet/aspnet:10.0). No Dockerfile required.

Multi-Stage Build (Alternative with Dockerfile)

If you prefer a traditional Dockerfile for more control:
# Build stage
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
WORKDIR /src

# Copy project files and restore dependencies
COPY ["src/Playground/Playground.Api/Playground.Api.csproj", "Playground.Api/"]
COPY ["src/BuildingBlocks/", "BuildingBlocks/"]
COPY ["src/Modules/", "Modules/"]
COPY ["src/Playground/Migrations.PostgreSQL/", "Migrations.PostgreSQL/"]
COPY ["Directory.Build.props", "Directory.Packages.props", "./"]

RUN dotnet restore "Playground.Api/Playground.Api.csproj"

# Copy source code and build
COPY src/ .
WORKDIR "/src/Playground.Api"
RUN dotnet build "Playground.Api.csproj" -c Release -o /app/build

# Publish stage
FROM build AS publish
RUN dotnet publish "Playground.Api.csproj" -c Release -o /app/publish --no-restore

# Runtime stage
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS final
WORKDIR /app
EXPOSE 8080

COPY --from=publish /app/publish .
ENTRYPOINT ["dotnet", "FSH.Playground.Api.dll"]
Build with Docker:
docker build -f Dockerfile.api -t fsh-playground-api:latest .
docker build -f Dockerfile.blazor -t fsh-playground-blazor:latest .

Docker Compose Setup

Create a complete development stack with API, Blazor UI, PostgreSQL, Redis, and observability tools.

Basic Compose Configuration

version: '3.8'

services:
  # PostgreSQL Database
  postgres:
    image: postgres:16-alpine
    container_name: fsh-postgres
    environment:
      POSTGRES_DB: fsh
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: password
    ports:
      - "5432:5432"
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - fsh-network

  # Redis Cache
  redis:
    image: redis:7.1-alpine
    container_name: fsh-redis
    command: redis-server --requirepass password --appendonly yes
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    healthcheck:
      test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5
    networks:
      - fsh-network

  # API Service
  api:
    image: fsh-playground-api:latest
    container_name: fsh-api
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:8080
      - DatabaseOptions__Provider=POSTGRESQL
      - DatabaseOptions__ConnectionString=Host=postgres;Port=5432;Database=fsh;Username=postgres;Password=password;Pooling=true
      - DatabaseOptions__MigrationsAssembly=FSH.Playground.Migrations.PostgreSQL
      - CachingOptions__Redis=redis:6379,password=password
      - OriginOptions__OriginUrl=http://localhost:7140
      - CorsOptions__AllowAll=true
      - JwtOptions__SigningKey=your-secret-key-min-32-characters-long
      - JwtOptions__Issuer=fsh.local
      - JwtOptions__Audience=fsh.clients
      - Storage__Provider=local
    ports:
      - "5285:8080"
    volumes:
      - api-uploads:/app/wwwroot/uploads
    networks:
      - fsh-network

  # Blazor UI
  blazor:
    image: fsh-playground-blazor:latest
    container_name: fsh-blazor
    depends_on:
      - api
    environment:
      - ASPNETCORE_ENVIRONMENT=Development
      - ASPNETCORE_URLS=http://+:8080
      - Api__BaseUrl=http://localhost:5285
    ports:
      - "7140:8080"
    networks:
      - fsh-network

networks:
  fsh-network:
    driver: bridge

volumes:
  postgres-data:
  redis-data:
  api-uploads:

Run the Stack

1

Build Container Images

Build both API and Blazor images (if not already built):
dotnet publish src/Playground/Playground.Api/Playground.Api.csproj -c Release -r linux-x64 -p:PublishProfile=DefaultContainer
dotnet publish src/Playground/Playground.Blazor/Playground.Blazor.csproj -c Release -r linux-x64 -p:PublishProfile=DefaultContainer
2

Start Services

docker compose up -d
This starts Postgres, Redis, API, and Blazor in detached mode.
3

View Logs

docker compose logs -f api
Watch for successful database migrations and service startup.
4

Access Applications

Production Compose Configuration

For production deployments, use explicit secrets and resource limits:
docker-compose.prod.yml
version: '3.8'

services:
  postgres:
    environment:
      POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
    secrets:
      - postgres_password
    deploy:
      resources:
        limits:
          cpus: '2'
          memory: 2G
        reservations:
          cpus: '1'
          memory: 1G

  redis:
    command: redis-server --requirepass /run/secrets/redis_password --appendonly yes
    secrets:
      - redis_password
    deploy:
      resources:
        limits:
          cpus: '1'
          memory: 512M

  api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - DatabaseOptions__ConnectionString=Host=postgres;Port=5432;Database=fsh;Username=postgres;Password=/run/secrets/postgres_password;Pooling=true;SSL Mode=Require
      - JwtOptions__SigningKey=/run/secrets/jwt_signing_key
      - OpenApiOptions__Enabled=false
      - RateLimitingOptions__Enabled=true
    secrets:
      - postgres_password
      - jwt_signing_key
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '0.5'
          memory: 512M
      restart_policy:
        condition: on-failure
        max_attempts: 3

  blazor:
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
    deploy:
      replicas: 2
      resources:
        limits:
          cpus: '0.5'
          memory: 512M

secrets:
  postgres_password:
    file: ./secrets/postgres_password.txt
  redis_password:
    file: ./secrets/redis_password.txt
  jwt_signing_key:
    file: ./secrets/jwt_signing_key.txt
Run production stack:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d

Container Image Tags

The CI/CD pipeline automatically builds and tags container images:
Images are tagged with dev-{SHA} and dev-latest:
ghcr.io/fullstackhero/fsh-playground-api:dev-abc1234
ghcr.io/fullstackhero/fsh-playground-api:dev-latest
GitHub Actions Workflow:
- name: Publish API container image
  run: |
    dotnet publish src/Playground/Playground.Api/Playground.Api.csproj \
      -c Release -r linux-x64 \
      -p:PublishProfile=DefaultContainer \
      -p:ContainerRepository=ghcr.io/${{ github.repository_owner }}/fsh-playground-api \
      -p:ContainerImageTags='"dev-${{ github.sha }};dev-latest"'

Container Registries

GitHub Container Registry (GHCR)

The project is configured to publish to GHCR by default:
# Login to GHCR
echo $GITHUB_TOKEN | docker login ghcr.io -u USERNAME --password-stdin

# Tag and push
docker tag fsh-playground-api:latest ghcr.io/yourorg/fsh-playground-api:1.0.0
docker push ghcr.io/yourorg/fsh-playground-api:1.0.0

Amazon ECR

For AWS deployments, push to ECR:
# Authenticate to ECR
aws ecr get-login-password --region us-east-1 | \
  docker login --username AWS --password-stdin 123456789012.dkr.ecr.us-east-1.amazonaws.com

# Create repositories (first time only)
aws ecr create-repository --repository-name fsh-playground-api --region us-east-1
aws ecr create-repository --repository-name fsh-playground-blazor --region us-east-1

# Tag and push
docker tag fsh-playground-api:latest 123456789012.dkr.ecr.us-east-1.amazonaws.com/fsh-playground-api:1.0.0
docker push 123456789012.dkr.ecr.us-east-1.amazonaws.com/fsh-playground-api:1.0.0
Update Terraform variables:
container_registry  = "123456789012.dkr.ecr.us-east-1.amazonaws.com"
container_image_tag = "1.0.0"

Docker Hub

# Login
docker login

# Tag and push
docker tag fsh-playground-api:latest yourorg/fsh-playground-api:1.0.0
docker push yourorg/fsh-playground-api:1.0.0

Observability Stack

Add Jaeger, Prometheus, and Grafana for local observability:
docker-compose.observability.yml
version: '3.8'

services:
  # Jaeger for distributed tracing
  jaeger:
    image: jaegertracing/all-in-one:1.53
    container_name: fsh-jaeger
    environment:
      - COLLECTOR_OTLP_ENABLED=true
    ports:
      - "4317:4317"   # OTLP gRPC
      - "4318:4318"   # OTLP HTTP
      - "16686:16686" # Jaeger UI
    networks:
      - fsh-network

  # Prometheus for metrics
  prometheus:
    image: prom/prometheus:latest
    container_name: fsh-prometheus
    command:
      - '--config.file=/etc/prometheus/prometheus.yml'
      - '--storage.tsdb.path=/prometheus'
    ports:
      - "9090:9090"
    volumes:
      - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    networks:
      - fsh-network

  # Grafana for dashboards
  grafana:
    image: grafana/grafana:latest
    container_name: fsh-grafana
    environment:
      - GF_SECURITY_ADMIN_PASSWORD=admin
    ports:
      - "3000:3000"
    volumes:
      - grafana-data:/var/lib/grafana
    networks:
      - fsh-network

volumes:
  prometheus-data:
  grafana-data:
Update API configuration:
api:
  environment:
    - OpenTelemetryOptions__Exporter__Otlp__Enabled=true
    - OpenTelemetryOptions__Exporter__Otlp__Endpoint=http://jaeger:4317
Start observability stack:
docker compose -f docker-compose.yml -f docker-compose.observability.yml up -d
Access:

Troubleshooting

Error: The remote certificate is invalid according to the validation procedureSolution: Add Trust Server Certificate=true to connection string (dev only) or configure proper SSL certificates.
DatabaseOptions__ConnectionString="Host=postgres;...;SSL Mode=Require;Trust Server Certificate=true"
Error: NOAUTH Authentication requiredSolution: Include password in Redis connection string:
CachingOptions__Redis="redis:6379,password=your-password"
Cause: API is starting but health checks are failing.Debug:
docker compose logs api
docker compose exec api curl http://localhost:8080/health/live
Check for database migration errors or missing configuration.
Cause: Incorrect Api__BaseUrl configuration.Solution: Blazor needs the external API URL (from browser perspective):
blazor:
  environment:
    - Api__BaseUrl=http://localhost:5285  # NOT http://api:8080

Next Steps

AWS Deployment

Deploy to production using ECS Fargate with Terraform automation

Configuration Management

Master environment-specific configuration and secrets management

Deployment Overview

Review production considerations and deployment checklist

GitHub Actions

Set up automated CI/CD pipeline for builds and deployments

Build docs developers (and LLMs) love