Loading...
Loading...
Deploys .NET containers. Kubernetes probes, Docker Compose for local dev, CI/CD integration.
npx skill4agent add novotnyllc/dotnet-artisan dotnet-container-deploymentapiVersion: apps/v1
kind: Deployment
metadata:
name: order-api
labels:
app: order-api
app.kubernetes.io/name: order-api
app.kubernetes.io/version: "1.0.0"
app.kubernetes.io/component: api
spec:
replicas: 3
selector:
matchLabels:
app: order-api
template:
metadata:
labels:
app: order-api
spec:
containers:
- name: order-api
image: ghcr.io/myorg/order-api:1.0.0
ports:
- containerPort: 8080
protocol: TCP
env:
- name: ASPNETCORE_ENVIRONMENT
value: "Production"
- name: OTEL_EXPORTER_OTLP_ENDPOINT
value: "http://otel-collector.monitoring:4317"
- name: OTEL_SERVICE_NAME
value: "order-api"
- name: ConnectionStrings__DefaultConnection
valueFrom:
secretKeyRef:
name: order-api-secrets
key: connection-string
resources:
requests:
cpu: "100m"
memory: "128Mi"
limits:
cpu: "500m"
memory: "512Mi"
livenessProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 10
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /health/ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30
securityContext:
runAsNonRoot: true
runAsUser: 1654
fsGroup: 1654
terminationGracePeriodSeconds: 30apiVersion: v1
kind: Service
metadata:
name: order-api
labels:
app: order-api
spec:
type: ClusterIP
selector:
app: order-api
ports:
- port: 80
targetPort: 8080
protocol: TCP
name: httpapiVersion: v1
kind: ConfigMap
metadata:
name: order-api-config
data:
ASPNETCORE_ENVIRONMENT: "Production"
Logging__LogLevel__Default: "Information"
Logging__LogLevel__Microsoft.AspNetCore: "Warning"envFrom:
- configMapRef:
name: order-api-configapiVersion: v1
kind: Secret
metadata:
name: order-api-secrets
type: Opaque
stringData:
connection-string: "Host=postgres;Database=orders;Username=app;Password=secret"| Probe | Purpose | Endpoint | Failure Action |
|---|---|---|---|
| Startup | Has the app finished initializing? | | Keep waiting (up to |
| Liveness | Is the process healthy? | | Restart the pod |
| Readiness | Can the process serve traffic? | | Remove from Service endpoints |
# Startup probe: give the app time to initialize
# Total startup budget: failureThreshold * periodSeconds = 30 * 5 = 150s
startupProbe:
httpGet:
path: /health/live
port: 8080
initialDelaySeconds: 0
periodSeconds: 5
failureThreshold: 30
# Liveness probe: detect deadlocks and hangs
# Only runs after startup probe succeeds
livenessProbe:
httpGet:
path: /health/live
port: 8080
periodSeconds: 15
timeoutSeconds: 3
failureThreshold: 3
# Readiness probe: control traffic routing
readinessProbe:
httpGet:
path: /health/ready
port: 8080
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3SIGTERMterminationGracePeriodSecondsspec:
terminationGracePeriodSeconds: 30IHostApplicationLifetimeapp.Lifetime.ApplicationStopping.Register(() =>
{
// Perform cleanup: flush telemetry, close connections
Log.CloseAndFlush();
});Host.ShutdownTimeoutbuilder.Host.ConfigureHostOptions(options =>
{
options.ShutdownTimeout = TimeSpan.FromSeconds(25);
});ShutdownTimeoutterminationGracePeriodSecondsSIGKILL# docker-compose.yml
services:
order-api:
build:
context: .
dockerfile: src/OrderApi/Dockerfile
ports:
- "8080:8080"
environment:
- ASPNETCORE_ENVIRONMENT=Development
- ConnectionStrings__DefaultConnection=Host=postgres;Database=orders;Username=app;Password=devpassword
- OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4317
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
# Note: CMD-SHELL + curl requires a base image with shell and curl installed.
# Chiseled/distroless images lack both. For chiseled images, either use a
# non-chiseled dev target in the Dockerfile or omit the healthcheck and rely
# on depends_on ordering (acceptable for local dev).
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost:8080/health/live || exit 1"]
interval: 10s
timeout: 3s
retries: 3
start_period: 10s
postgres:
image: postgres:17
environment:
POSTGRES_DB: orders
POSTGRES_USER: app
POSTGRES_PASSWORD: devpassword
ports:
- "5432:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d orders"]
interval: 5s
timeout: 3s
retries: 5
redis:
image: redis:7-alpine
ports:
- "6379:6379"
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 5s
timeout: 3s
retries: 5
volumes:
postgres-data:# docker-compose.override.yml (auto-loaded by docker compose up)
services:
order-api:
build:
target: build # Stop at build stage for faster rebuilds
volumes:
- .:/src # Mount source for hot reload
environment:
- ASPNETCORE_ENVIRONMENT=Development
- DOTNET_USE_POLLING_FILE_WATCHER=true
command: ["dotnet", "watch", "run", "--project", "src/OrderApi/OrderApi.csproj"]# docker-compose.observability.yml
services:
otel-collector:
image: otel/opentelemetry-collector-contrib:latest
command: ["--config=/etc/otelcol-config.yaml"]
volumes:
- ./infra/otelcol-config.yaml:/etc/otelcol-config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
volumes:
- grafana-data:/var/lib/grafana
volumes:
grafana-data:docker compose -f docker-compose.yml -f docker-compose.observability.yml up# .github/workflows/docker-publish.yml
name: Build and Push Container
on:
push:
branches: [main]
tags: ["v*"]
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository }}
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4
- name: Log in to container registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max| Tag Pattern | Example | Use Case |
|---|---|---|
| | Development only -- never use in production |
| Semver | | Release versions -- immutable |
| Major.Minor | | Floating tag for patch updates |
| SHA | | Unique per commit -- traceability |
| Branch | | CI builds -- latest from branch |
dotnet publish /t:PublishContainersteps:
- uses: actions/checkout@v4
- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
- name: Publish container image
run: |
dotnet publish src/OrderApi/OrderApi.csproj \
--os linux --arch x64 \
/t:PublishContainer \
-p:ContainerRegistry=${{ env.REGISTRY }} \
-p:ContainerRepository=${{ env.IMAGE_NAME }} \
-p:ContainerImageTag=${{ github.sha }}runAsNonRoot: truedepends_onfailureThreshold * periodSecondslatestlatestimagePullPolicy: IfNotPresentsecretKeyRefconfigMapRefterminationGracePeriodSecondsHost.ShutdownTimeoutcondition: service_healthydepends_on