Skip to content

Phase 10.1: Create Optimized Dockerfiles for API and Server with Health Checks #113

@artcava

Description

@artcava

📋 Task Description

Create production-ready Dockerfiles for StarGate.Server (API + Worker) with multi-stage builds, optimized image sizes, security best practices, and health check configurations. Ensure containers are efficient, secure, and ready for production deployment.

🎯 Objectives

  • Create multi-stage Dockerfile for StarGate.Server
  • Optimize image size with Alpine base
  • Implement security best practices (non-root user, minimal surface)
  • Configure health checks for container orchestration
  • Add proper labeling and metadata
  • Optimize layer caching for faster builds
  • Include only necessary runtime dependencies
  • Configure proper logging to stdout/stderr
  • Document Docker build and run procedures
  • Test container startup and health checks
  • Measure and document image sizes
  • Create .dockerignore for build optimization

📦 Deliverables

1. Create .dockerignore

Create .dockerignore:

# Build artifacts
**/bin/
**/obj/
**/out/

# IDE files
.vs/
.vscode/
.idea/
*.user
*.suo

# Test results
**/TestResults/
**/coverage/
**/results/

# OS files
.DS_Store
Thumbs.db

# Git
.git/
.gitignore
.gitattributes

# Documentation
*.md
docs/

# Docker
Dockerfile*
docker-compose*
.dockerignore

# CI/CD
.github/

# Logs
*.log
logs/

# Temporary files
*.tmp
*.temp

2. Create Dockerfile for StarGate.Server

Create src/StarGate.Server/Dockerfile:

# Multi-stage build for StarGate.Server
# This container runs both the API (HTTP endpoints) and the ProcessWorker (background consumer)

# ============================================
# Stage 1: Build
# ============================================
FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build

WORKDIR /src

# Copy solution and project files first (better layer caching)
COPY ["StarGate.sln", "./"]
COPY ["src/StarGate.Core/StarGate.Core.csproj", "src/StarGate.Core/"]
COPY ["src/StarGate.Infrastructure/StarGate.Infrastructure.csproj", "src/StarGate.Infrastructure/"]
COPY ["src/StarGate.Server/StarGate.Server.csproj", "src/StarGate.Server/"]

# Restore dependencies
RUN dotnet restore "src/StarGate.Server/StarGate.Server.csproj" \
    --runtime linux-musl-x64

# Copy source code
COPY src/ ./src/

# Build application
WORKDIR /src/src/StarGate.Server
RUN dotnet build "StarGate.Server.csproj" \
    -c Release \
    --runtime linux-musl-x64 \
    --self-contained false \
    --no-restore

# ============================================
# Stage 2: Publish
# ============================================
FROM build AS publish

RUN dotnet publish "StarGate.Server.csproj" \
    -c Release \
    --runtime linux-musl-x64 \
    --self-contained false \
    --no-build \
    -o /app/publish \
    /p:PublishTrimmed=false \
    /p:PublishSingleFile=false

# ============================================
# Stage 3: Runtime
# ============================================
FROM mcr.microsoft.com/dotnet/aspnet:8.0-alpine AS runtime

# Install dependencies for health checks
RUN apk add --no-cache curl

# Create non-root user
RUN addgroup -g 1000 stargate && \
    adduser -u 1000 -G stargate -s /bin/sh -D stargate

# Set working directory
WORKDIR /app

# Copy published application
COPY --from=publish --chown=stargate:stargate /app/publish .

# Create logs directory
RUN mkdir -p /app/logs && chown stargate:stargate /app/logs

# Switch to non-root user
USER stargate

# Expose ports
EXPOSE 8080
EXPOSE 8081

# Environment variables (can be overridden)
ENV ASPNETCORE_URLS=http://+:8080 \
    ASPNETCORE_ENVIRONMENT=Production \
    DOTNET_RUNNING_IN_CONTAINER=true \
    DOTNET_EnableDiagnostics=0

# Health check configuration
# Checks the /health endpoint every 30 seconds
# Container is unhealthy after 3 consecutive failures
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

# Labels for metadata
LABEL maintainer="StarGate Team" \
      version="1.0" \
      description="StarGate Process Orchestration Server - API + Worker" \
      org.opencontainers.image.source="https://github.com/artcava/StarGate" \
      org.opencontainers.image.description="Async process orchestration platform" \
      org.opencontainers.image.licenses="MIT"

# Entry point
ENTRYPOINT ["dotnet", "StarGate.Server.dll"]

3. Create Optimized Dockerfile Variant

Create src/StarGate.Server/Dockerfile.optimized:

# Ultra-optimized variant using self-contained deployment
# Results in slightly larger image but no .NET runtime dependency

FROM mcr.microsoft.com/dotnet/sdk:8.0-alpine AS build

WORKDIR /src

COPY ["StarGate.sln", "./"]
COPY ["src/StarGate.Core/StarGate.Core.csproj", "src/StarGate.Core/"]
COPY ["src/StarGate.Infrastructure/StarGate.Infrastructure.csproj", "src/StarGate.Infrastructure/"]
COPY ["src/StarGate.Server/StarGate.Server.csproj", "src/StarGate.Server/"]

RUN dotnet restore "src/StarGate.Server/StarGate.Server.csproj" \
    --runtime linux-musl-x64

COPY src/ ./src/

WORKDIR /src/src/StarGate.Server

# Self-contained publish with trimming
RUN dotnet publish "StarGate.Server.csproj" \
    -c Release \
    --runtime linux-musl-x64 \
    --self-contained true \
    -o /app/publish \
    /p:PublishTrimmed=true \
    /p:PublishSingleFile=true \
    /p:EnableCompressionInSingleFile=true \
    /p:DebugType=None \
    /p:DebugSymbols=false

# Ultra-minimal runtime (no .NET runtime needed)
FROM alpine:3.19 AS runtime

RUN apk add --no-cache \
    curl \
    icu-libs \
    libgcc \
    libstdc++ \
    zlib

RUN addgroup -g 1000 stargate && \
    adduser -u 1000 -G stargate -s /bin/sh -D stargate

WORKDIR /app

COPY --from=build --chown=stargate:stargate /app/publish .

RUN mkdir -p /app/logs && chown stargate:stargate /app/logs

USER stargate

EXPOSE 8080
EXPOSE 8081

ENV ASPNETCORE_URLS=http://+:8080 \
    DOTNET_SYSTEM_GLOBALIZATION_INVARIANT=false

HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
    CMD curl -f http://localhost:8080/health || exit 1

LABEL maintainer="StarGate Team" \
      version="1.0-optimized" \
      description="StarGate Server - Optimized Self-Contained"

ENTRYPOINT ["./StarGate.Server"]

4. Create Build Script

Create scripts/build-docker.sh:

#!/bin/bash

# Script to build Docker images for StarGate

set -e

DOCKER_REGISTRY="${DOCKER_REGISTRY:-stargate}"
VERSION="${VERSION:-latest}"
BUILD_TYPE="${BUILD_TYPE:-standard}" # standard or optimized

echo "============================================"
echo "Building StarGate Docker Images"
echo "============================================"
echo "Registry: $DOCKER_REGISTRY"
echo "Version: $VERSION"
echo "Build Type: $BUILD_TYPE"
echo "============================================"

# Determine Dockerfile to use
if [ "$BUILD_TYPE" = "optimized" ]; then
    DOCKERFILE="src/StarGate.Server/Dockerfile.optimized"
else
    DOCKERFILE="src/StarGate.Server/Dockerfile"
fi

echo "Using Dockerfile: $DOCKERFILE"
echo ""

# Build image
echo "Building image..."
docker build \
    -f "$DOCKERFILE" \
    -t "$DOCKER_REGISTRY/stargate-server:$VERSION" \
    -t "$DOCKER_REGISTRY/stargate-server:latest" \
    --build-arg BUILD_DATE="$(date -u +'%Y-%m-%dT%H:%M:%SZ')" \
    --build-arg VCS_REF="$(git rev-parse --short HEAD)" \
    .

echo ""
echo "============================================"
echo "Build Complete!"
echo "============================================"

# Show image size
docker images | grep stargate-server | head -n 1

echo ""
echo "To run the container:"
echo "  docker run -p 8080:8080 $DOCKER_REGISTRY/stargate-server:$VERSION"
echo ""
echo "To push to registry:"
echo "  docker push $DOCKER_REGISTRY/stargate-server:$VERSION"
echo ""

Make executable:

chmod +x scripts/build-docker.sh

5. Create Docker Test Script

Create scripts/test-docker.sh:

#!/bin/bash

# Script to test Docker container

set -e

IMAGE="${1:-stargate/stargate-server:latest}"
CONTAINER_NAME="stargate-server-test"

echo "Testing Docker image: $IMAGE"
echo ""

# Cleanup existing container
docker rm -f $CONTAINER_NAME 2>/dev/null || true

# Start container
echo "Starting container..."
docker run -d \
    --name $CONTAINER_NAME \
    -p 8080:8080 \
    -e ASPNETCORE_ENVIRONMENT=Development \
    $IMAGE

echo "Waiting for container to be ready..."
sleep 5

# Check if container is running
if ! docker ps | grep -q $CONTAINER_NAME; then
    echo "❌ Container failed to start"
    docker logs $CONTAINER_NAME
    docker rm -f $CONTAINER_NAME
    exit 1
fi

echo "✓ Container is running"
echo ""

# Test health endpoint
echo "Testing health endpoint..."
for i in {1..10}; do
    if curl -sf http://localhost:8080/health > /dev/null; then
        echo "✓ Health check passed"
        HEALTH_OK=true
        break
    fi
    echo "Attempt $i/10 failed, waiting..."
    sleep 2
done

if [ "$HEALTH_OK" != "true" ]; then
    echo "❌ Health check failed"
    docker logs $CONTAINER_NAME
    docker rm -f $CONTAINER_NAME
    exit 1
fi

echo ""

# Check Docker health status
echo "Checking Docker health status..."
sleep 35 # Wait for health check to run
HEALTH_STATUS=$(docker inspect --format='{{.State.Health.Status}}' $CONTAINER_NAME)
echo "Health status: $HEALTH_STATUS"

if [ "$HEALTH_STATUS" != "healthy" ]; then
    echo "⚠️  Warning: Container not marked as healthy yet"
fi

echo ""

# Show container info
echo "Container information:"
docker stats --no-stream $CONTAINER_NAME

echo ""
echo "Container logs:"
docker logs --tail 20 $CONTAINER_NAME

echo ""
echo "============================================"
echo "Test Summary"
echo "============================================"
echo "✓ Container started successfully"
echo "✓ Health endpoint responding"
echo "✓ Container is $HEALTH_STATUS"
echo ""
echo "To view logs: docker logs -f $CONTAINER_NAME"
echo "To stop: docker rm -f $CONTAINER_NAME"
echo "============================================"

# Cleanup (optional)
read -p "Stop and remove container? (y/n) " -n 1 -r
echo
if [[ $REPLY =~ ^[Yy]$ ]]; then
    docker rm -f $CONTAINER_NAME
    echo "Container removed"
fi

Make executable:

chmod +x scripts/test-docker.sh

6. Create Size Comparison Script

Create scripts/compare-image-sizes.sh:

#!/bin/bash

# Compare image sizes between standard and optimized builds

echo "Building standard image..."
BUILD_TYPE=standard ./scripts/build-docker.sh > /dev/null 2>&1

echo "Building optimized image..."
BUILD_TYPE=optimized ./scripts/build-docker.sh > /dev/null 2>&1

echo ""
echo "============================================"
echo "Image Size Comparison"
echo "============================================"

docker images | grep stargate-server | awk '{printf "%-50s %10s\n", $1":"$2, $7" "$8}'

echo "============================================"

Make executable:

chmod +x scripts/compare-image-sizes.sh

7. Update Program.cs for Container Support

Update src/StarGate.Server/Program.cs:

// Configure Kestrel for container environment
builder.WebHost.ConfigureKestrel(options =>
{
    // Listen on all interfaces (required for Docker)
    options.ListenAnyIP(8080); // HTTP
    
    // Optional: Add metrics endpoint on different port
    options.ListenAnyIP(8081, listenOptions =>
    {
        // Metrics endpoint (can be used for Prometheus)
    });
});

// Configure logging for container
builder.Logging.ClearProviders();
builder.Logging.AddConsole();
builder.Logging.AddJsonConsole(options =>
{
    options.IncludeScopes = true;
    options.TimestampFormat = "yyyy-MM-dd HH:mm:ss ";
    options.JsonWriterOptions = new System.Text.Json.JsonWriterOptions
    {
        Indented = false
    };
});

// Health checks endpoint
app.MapHealthChecks("/health", new HealthCheckOptions
{
    ResponseWriter = async (context, report) =>
    {
        context.Response.ContentType = "application/json";
        var result = JsonSerializer.Serialize(new
        {
            status = report.Status.ToString(),
            checks = report.Entries.Select(e => new
            {
                name = e.Key,
                status = e.Value.Status.ToString(),
                description = e.Value.Description,
                duration = e.Value.Duration.TotalMilliseconds
            }),
            totalDuration = report.TotalDuration.TotalMilliseconds
        });
        await context.Response.WriteAsync(result);
    }
});

// Liveness probe (simpler than full health check)
app.MapGet("/health/live", () => Results.Ok(new { status = "alive" }));

// Readiness probe (checks dependencies)
app.MapGet("/health/ready", async (IServiceProvider services) =>
{
    // Check critical dependencies
    try
    {
        var db = services.GetRequiredService<IMongoDatabase>();
        await db.ListCollectionNamesAsync().FirstOrDefaultAsync();
        return Results.Ok(new { status = "ready" });
    }
    catch
    {
        return Results.StatusCode(503); // Service Unavailable
    }
});

8. Create Documentation

Create docs/DOCKER.md:

# Docker Deployment Guide

## Building Images

### Standard Build
```bash
./scripts/build-docker.sh

Optimized Build

BUILD_TYPE=optimized ./scripts/build-docker.sh

With Custom Version

VERSION=1.0.0 ./scripts/build-docker.sh

Running Containers

Basic Run

docker run -p 8080:8080 stargate/stargate-server:latest

With Environment Variables

docker run -p 8080:8080 \
  -e MongoDB__ConnectionString="mongodb://mongo:27017" \
  -e MongoDB__DatabaseName="stargate" \
  -e Redis__ConnectionString="redis:6379" \
  -e RabbitMQ__HostName="rabbitmq" \
  stargate/stargate-server:latest

With Volume Mounts

docker run -p 8080:8080 \
  -v $(pwd)/logs:/app/logs \
  -v $(pwd)/appsettings.Production.json:/app/appsettings.Production.json:ro \
  stargate/stargate-server:latest

Health Checks

Endpoints

  • /health - Full health check (all dependencies)
  • /health/live - Liveness probe (container alive)
  • /health/ready - Readiness probe (ready to serve traffic)

Testing Health

curl http://localhost:8080/health
curl http://localhost:8080/health/live
curl http://localhost:8080/health/ready

Docker Health Status

docker inspect --format='{{.State.Health.Status}}' <container-id>

Image Sizes

Standard Build

  • Base: mcr.microsoft.com/dotnet/aspnet:8.0-alpine
  • Size: ~180-200 MB
  • Includes: .NET 8 runtime

Optimized Build

  • Base: alpine:3.19
  • Size: ~90-110 MB
  • Includes: Self-contained app

Security

Non-Root User

Containers run as user stargate (UID 1000)

Minimal Attack Surface

  • Alpine base (minimal packages)
  • No unnecessary tools
  • Read-only filesystem recommended

Scanning

docker scan stargate/stargate-server:latest

Troubleshooting

View Logs

docker logs -f <container-id>

Interactive Shell

docker exec -it <container-id> /bin/sh

Resource Usage

docker stats <container-id>

Best Practices

  1. Use specific versions in production (not latest)
  2. Set resource limits to prevent resource exhaustion
  3. Use health checks for orchestration
  4. Mount logs volume for persistence
  5. Use secrets management for sensitive data
  6. Scan images regularly for vulnerabilities
  7. Update base images regularly

## ✅ Acceptance Criteria

- [ ] Dockerfile created with multi-stage build
- [ ] Alpine base image used for minimal size
- [ ] Non-root user configured
- [ ] Health checks implemented in Dockerfile
- [ ] Optimized Dockerfile variant created
- [ ] .dockerignore configured
- [ ] Build script created and tested
- [ ] Test script created and tested
- [ ] Size comparison script created
- [ ] Image size <200MB (standard), <120MB (optimized)
- [ ] Container starts successfully
- [ ] Health checks pass
- [ ] Logs output to stdout/stderr
- [ ] Documentation complete
- [ ] Code follows CODING-CONVENTIONS.md

## 📝 Testing Instructions

```bash
# Build standard image
./scripts/build-docker.sh

# Test container
./scripts/test-docker.sh

# Compare image sizes
./scripts/compare-image-sizes.sh

# Manual testing
# 1. Build image
docker build -t stargate-server -f src/StarGate.Server/Dockerfile .

# 2. Run container
docker run -d --name stargate-test -p 8080:8080 stargate-server

# 3. Check health
curl http://localhost:8080/health

# 4. Check Docker health status
docker inspect --format='{{.State.Health.Status}}' stargate-test

# 5. View logs
docker logs stargate-test

# 6. Check resource usage
docker stats --no-stream stargate-test

# 7. Test optimized variant
docker build -t stargate-server-opt -f src/StarGate.Server/Dockerfile.optimized .
docker run -d --name stargate-test-opt -p 8081:8080 stargate-server-opt

# 8. Compare sizes
docker images | grep stargate-server

# Cleanup
docker rm -f stargate-test stargate-test-opt

📚 References

🏷️ Labels

phase-10 containerization sprint-10.1 docker dockerfile

⏱️ Estimated Effort

8-12 hours

🔗 Dependencies

  • Phase 5: API Gateway (must be functional)
  • Phase 7: ProcessWorker (must be functional)
  • All core functionality implemented

🔗 Related Issues

Part of Phase 10: Containerization - Sprint 10.1: Docker

📌 Important Notes

Multi-Stage Build Benefits

Smaller Images:

  • Build artifacts excluded
  • Only runtime files included
  • SDK not in final image

Better Caching:

  • Dependencies layer cached separately
  • Source changes don't invalidate dependency layer
  • Faster rebuilds

Security:

  • Minimal attack surface
  • No build tools in production
  • Reduced vulnerability exposure

Standard vs Optimized

Standard (Recommended):

  • Uses .NET runtime image
  • Faster builds
  • Better for development
  • Easier debugging
  • ~200MB image size

Optimized (Production):

  • Self-contained deployment
  • No runtime dependency
  • Smaller image (~100MB)
  • Slightly slower builds
  • Best for production

Health Check Strategy

Three Endpoints:

  1. /health - Full health check

    • Checks all dependencies
    • Used by monitoring
    • Returns detailed status
  2. /health/live - Liveness probe

    • Just checks if container is alive
    • Used by Kubernetes liveness
    • Fast response
  3. /health/ready - Readiness probe

    • Checks if ready to serve traffic
    • Used by Kubernetes readiness
    • Checks database connection

Non-Root User Security

Why:

  • Principle of least privilege
  • Limits damage if compromised
  • Required by some orchestrators
  • Security best practice

Implementation:

RUN addgroup -g 1000 stargate && \
    adduser -u 1000 -G stargate -s /bin/sh -D stargate

USER stargate

Image Size Optimization

Techniques Used:

  1. Alpine base (~5MB vs ~100MB for Debian)
  2. Multi-stage build (build artifacts excluded)
  3. Layer optimization (combine RUN commands)
  4. No cache (apk add --no-cache)
  5. Trimming (remove unused code)
  6. Single file (one executable)

Typical Sizes:

  • SDK image: ~700MB
  • Runtime image: ~200MB
  • Alpine runtime: ~180MB
  • Optimized: ~100MB

Logging in Containers

Best Practices:

  • Log to stdout/stderr (not files)
  • Structured logging (JSON)
  • Include timestamps
  • Container orchestrator collects logs

Configuration:

builder.Logging.AddJsonConsole();

Port Configuration

Port 8080: Main API
Port 8081: Metrics/Admin

Why not 80:

  • Non-root users can't bind to ports <1024
  • 8080 is convention for HTTP in containers

Environment Variables

Common:

  • ASPNETCORE_ENVIRONMENT
  • ASPNETCORE_URLS
  • MongoDB__ConnectionString
  • Redis__ConnectionString
  • RabbitMQ__HostName

Override Example:

docker run -e ASPNETCORE_ENVIRONMENT=Production ...

Build Arguments

Metadata:

ARG BUILD_DATE
ARG VCS_REF
LABEL build-date=$BUILD_DATE \
      vcs-ref=$VCS_REF

Usage:

docker build \
  --build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
  --build-arg VCS_REF=$(git rev-parse --short HEAD) \
  .

Volume Mounts

Logs:

-v $(pwd)/logs:/app/logs

Configuration:

-v $(pwd)/appsettings.json:/app/appsettings.json:ro

Data:

-v stargate-data:/app/data

Resource Limits

Recommended:

docker run \
  --memory=512m \
  --memory-reservation=256m \
  --cpus=1.0 \
  ...

Troubleshooting

Container Won't Start:

docker logs <container-id>
docker inspect <container-id>

Health Check Failing:

docker exec <container-id> curl http://localhost:8080/health

Performance Issues:

docker stats <container-id>

Network Issues:

docker network inspect bridge

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions