skills/dotnet-env/SKILL.md
Structures .NET project environments (Development, Docker, Production) with proper appsettings per environment, docker-compose files, .env management, SSL configuration for production, and deploy pipeline.
npx skillsauth add landim32/awesome-ai-skills dotnet-envInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
You are an expert assistant that structures .NET projects with three well-defined environments: Development, Docker, and Production. Each environment has its own configuration files, secrets management strategy, and deployment approach.
The user will describe what to configure: $ARGUMENTS
Before generating files:
Program.cs, existing appsettings*.json filesappsettings.json — Understand current configuration sections and keysdocker-compose*.yml — Check for existing Docker setup.gitignore — Ensure .env* and secrets are properly ignoredDockerfile — Check for existing Dockerfile setup┌─────────────────────────────────────────────────────────────┐
│ Environment Matrix │
├──────────────┬──────────────┬──────────────┬────────────────┤
│ │ Development │ Docker │ Production │
├──────────────┼──────────────┼──────────────┼────────────────┤
│ Runs on │ Local machine│ Docker local │ Docker remote │
│ IDE │ VS / VS Code │ N/A │ N/A │
│ appsettings │ .Development │ .Docker │ .Production │
│ Secrets in │ JSON file │ .env file │ .env.prod file │
│ SSL │ Dev cert │ No SSL │ SSL required │
│ Compose file │ N/A │ docker-compose│docker-compose-prod│
│ Ports │ Default │ Exposed │ Per demand │
└──────────────┴──────────────┴──────────────┴────────────────┘
__ Convention for DockerIn Docker environments, use the double-underscore (__) separator to map environment variables to appsettings.json sections. This is the standard .NET configuration binding.
environment:
ConnectionStrings__DefaultConnection: ${CONNECTION_STRING}
JwtSettings__Secret: ${JWT_SECRET}
Maps to:
{
"ConnectionStrings": { "DefaultConnection": "value" },
"JwtSettings": { "Secret": "value" }
}
AddEnvironmentVariables("PREFIX") in codeUse only the default configuration providers. Environment variables with __ convention are automatically bound.
.env, .env.prod → always in .gitignore.env.example, .env.prod.example → committed with placeholder valuesappsettings.Development.jsonappsettings.Docker.jsonappsettings.Production.jsonRuns directly on the developer's machine via Visual Studio or VS Code. All values are hardcoded in the appsettings file for simplicity.
appsettings.Development.jsonAll configuration values — including secrets — are written directly in this file. This is acceptable because it's a local-only environment.
{
"Logging": {
"LogLevel": {
"Default": "Debug",
"Microsoft.AspNetCore": "Information"
}
},
"ConnectionStrings": {
"DefaultConnection": "Server=localhost;Port=5432;Database=myapp_dev;Username=postgres;Password=postgres"
},
"JwtSettings": {
"Secret": "dev-secret-key-min-32-chars-long-here",
"Issuer": "myapp-dev",
"Audience": "myapp-dev",
"ExpirationInMinutes": 60
}
}
ASPNETCORE_ENVIRONMENT=Development is the default in launchSettings.jsonProperties/launchSettings.jsonEnsure the ASPNETCORE_ENVIRONMENT is set:
{
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": false,
"applicationUrl": "http://localhost:5000",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
Runs in Docker containers locally. Secrets and configuration are injected via .env file and docker-compose.yml.
appsettings.Docker.jsonContains only structure and non-secret defaults. Secret values are overridden by environment variables from docker-compose:
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": ""
},
"JwtSettings": {
"Secret": "",
"Issuer": "",
"Audience": "",
"ExpirationInMinutes": 60
}
}
.envContains ALL configuration and secrets for the Docker environment:
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=postgres
POSTGRES_DB=myapp
CONNECTION_STRING=Server=db;Port=5432;Database=myapp;Username=postgres;Password=postgres
# JWT
JWT_SECRET=docker-secret-key-min-32-chars-long-here
JWT_ISSUER=myapp-docker
JWT_AUDIENCE=myapp-docker
# App
APP_PORT=5000
.env.exampleCommitted to the repository with placeholder values:
# Database
POSTGRES_USER=postgres
POSTGRES_PASSWORD=your_password_here
POSTGRES_DB=myapp
CONNECTION_STRING=Server=db;Port=5432;Database=myapp;Username=postgres;Password=your_password_here
# JWT
JWT_SECRET=your_jwt_secret_min_32_chars
JWT_ISSUER=myapp
JWT_AUDIENCE=myapp
# App
APP_PORT=5000
docker-compose.yml.env file for variable substitutionservices:
api:
build:
context: .
dockerfile: Dockerfile
container_name: myapp-api
ports:
- "${APP_PORT:-5000}:8080"
environment:
ASPNETCORE_ENVIRONMENT: Docker
ConnectionStrings__DefaultConnection: ${CONNECTION_STRING}
JwtSettings__Secret: ${JWT_SECRET}
JwtSettings__Issuer: ${JWT_ISSUER}
JwtSettings__Audience: ${JWT_AUDIENCE}
depends_on:
db:
condition: service_healthy
networks:
- myapp-network
db:
image: postgres:17-alpine
container_name: myapp-db
ports:
- "5432:5432"
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- myapp-network
volumes:
postgres_data:
networks:
myapp-network:
driver: bridge
ASPNETCORE_ENVIRONMENT=Docker set in docker-compose.env via ${VARIABLE} substitutiondepends_on with health checksRuns on a remote server via Docker. SSL is required. Only secrets are injected via .env.prod — non-secret configuration lives in appsettings.Production.json.
appsettings.Production.jsonContains all non-secret values directly. Only secret placeholders are empty (overridden by environment variables):
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft.AspNetCore": "Warning"
}
},
"ConnectionStrings": {
"DefaultConnection": ""
},
"JwtSettings": {
"Secret": "",
"Issuer": "myapp",
"Audience": "myapp",
"ExpirationInMinutes": 30
}
}
.env.prodContains ONLY secrets — no non-secret configuration:
# Database
POSTGRES_USER=prod_user
POSTGRES_PASSWORD=strong_production_password
POSTGRES_DB=myapp_prod
CONNECTION_STRING=Server=db;Port=5432;Database=myapp_prod;Username=prod_user;Password=strong_production_password
# JWT
JWT_SECRET=production-secret-key-very-strong-min-64-chars-recommended
.env.prod.exampleCommitted to repository — only secrets with placeholders:
# Database
POSTGRES_USER=prod_user
POSTGRES_PASSWORD=<STRONG_PASSWORD>
POSTGRES_DB=myapp_prod
CONNECTION_STRING=Server=db;Port=5432;Database=myapp_prod;Username=prod_user;Password=<STRONG_PASSWORD>
# JWT
JWT_SECRET=<STRONG_JWT_SECRET_MIN_64_CHARS>
docker-compose-prod.yml.env.prod for secretsservices:
api:
build:
context: .
dockerfile: Dockerfile
container_name: myapp-api
restart: unless-stopped
ports:
- "8080:8080"
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_URLS: http://+:8080
ConnectionStrings__DefaultConnection: ${CONNECTION_STRING}
JwtSettings__Secret: ${JWT_SECRET}
depends_on:
db:
condition: service_healthy
networks:
- myapp-network
- myapp-external
db:
image: postgres:17-alpine
container_name: myapp-db
restart: unless-stopped
# No port exposed externally — only internal access
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER}"]
interval: 10s
timeout: 5s
retries: 5
networks:
- myapp-network
volumes:
postgres_data:
networks:
myapp-network:
driver: bridge
myapp-external:
external: true
Production SSL is handled via reverse proxy (recommended). The API container listens on HTTP internally, and a reverse proxy (Nginx, Traefik, Caddy) terminates SSL.
nginx:
image: nginx:alpine
container_name: myapp-nginx
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/certs:/etc/nginx/certs:ro
depends_on:
- api
networks:
- myapp-network
- myapp-external
nginx/nginx.conf):events {
worker_connections 1024;
}
http {
server {
listen 80;
server_name yourdomain.com;
return 301 https://$host$request_uri;
}
server {
listen 443 ssl;
server_name yourdomain.com;
ssl_certificate /etc/nginx/certs/fullchain.pem;
ssl_certificate_key /etc/nginx/certs/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!aNULL:!MD5;
location / {
proxy_pass http://api:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}
caddy:
image: caddy:2-alpine
container_name: myapp-caddy
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- caddy_data:/data
- caddy_config:/config
depends_on:
- api
networks:
- myapp-network
- myapp-external
# Caddyfile
yourdomain.com {
reverse_proxy api:8080
}
ASPNETCORE_ENVIRONMENT=Production.env.prod — non-secret config in appsettings.Production.jsonrestart: unless-stopped on all services.github/workflows/deploy-prod.ymlGenerate a GitHub Actions workflow for production deployment via SSH:
name: Deploy Production
on:
workflow_dispatch:
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Deploy via SSH
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.PROD_SSH_HOST }}
username: ${{ secrets.PROD_SSH_USER }}
password: ${{ secrets.PROD_SSH_PASSWORD }}
port: ${{ secrets.PROD_SSH_PORT || 22 }}
script: |
set -e
DEPLOY_DIR="/opt/$PROJECT_NAME"
REPO_URL="https://github.com/${{ github.repository }}.git"
BRANCH="main"
# Clone or update repository
if [ -d "$DEPLOY_DIR" ]; then
echo "Updating existing repository..."
cd "$DEPLOY_DIR"
git fetch origin
git reset --hard "origin/$BRANCH"
git clean -fd
else
echo "Cloning repository..."
git clone --branch "$BRANCH" --single-branch "$REPO_URL" "$DEPLOY_DIR"
cd "$DEPLOY_DIR"
fi
# Inject .env.prod from GitHub Secrets
cat > .env.prod <<'ENVEOF'
$SECRET_VARIABLES_HERE
ENVEOF
# Remove leading whitespace from .env.prod
sed -i 's/^[[:space:]]*//' .env.prod
# Create external network if it doesn't exist
docker network inspect $EXTERNAL_NETWORK >/dev/null 2>&1 || docker network create $EXTERNAL_NETWORK
# Deploy with docker compose
docker compose --env-file .env.prod -f docker-compose-prod.yml down
docker compose --env-file .env.prod -f docker-compose-prod.yml up --build -d
# Wait and verify
echo "Waiting for services to start..."
sleep 10
docker compose --env-file .env.prod -f docker-compose-prod.yml ps
echo "Deployment completed successfully."
- name: Summary
run: |
echo "## Production Deployment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "- Server: \`${{ secrets.PROD_SSH_HOST }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Branch: \`main\`" >> $GITHUB_STEP_SUMMARY
echo "- Commit: \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY
echo "- Triggered by: \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY
When generating the pipeline:
$PROJECT_NAME with the actual project name (lowercase, kebab-case)$EXTERNAL_NETWORK with the project's external network name$SECRET_VARIABLES_HERE with the actual secret variables from .env.prod.example, using GitHub Secrets syntax:
POSTGRES_USER=${{ secrets.POSTGRES_USER }}
POSTGRES_PASSWORD=${{ secrets.POSTGRES_PASSWORD }}
CONNECTION_STRING=${{ secrets.CONNECTION_STRING }}
JWT_SECRET=${{ secrets.JWT_SECRET }}
.env.prod must have a corresponding GitHub Secret| Secret | Description |
|--------|-------------|
| PROD_SSH_HOST | Production server IP/hostname |
| PROD_SSH_USER | SSH username |
| PROD_SSH_PASSWORD | SSH password |
| PROD_SSH_PORT | SSH port (default: 22) |
| + all secrets from .env.prod.example | Application secrets |
Ensure a multi-stage Dockerfile exists:
FROM mcr.microsoft.com/dotnet/aspnet:9.0 AS base
WORKDIR /app
EXPOSE 8080
FROM mcr.microsoft.com/dotnet/sdk:9.0 AS build
WORKDIR /src
COPY . .
RUN dotnet restore
RUN dotnet publish -c Release -o /app/publish /p:UseAppHost=false
FROM base AS final
WORKDIR /app
COPY --from=build /app/publish .
ENTRYPOINT ["dotnet", "MyApp.dll"]
MyApp.dll with the actual project assembly nameTargetFramework8080 (default Kestrel port in .NET 8+)Ensure these entries exist in .gitignore:
# Environment files with secrets
.env
.env.prod
.env.local
# SSL certificates
nginx/certs/
*.pem
*.key
When the user invokes this skill, follow this order:
appsettings.json, Program.cs, .gitignore, Dockerfileappsettings.Development.json — fill all values directlyappsettings.Docker.json — empty secrets, structure onlyappsettings.Production.json — non-secret values filled, secrets empty.env and .env.example for Docker environment.env.prod and .env.prod.example for Production (secrets only)docker-compose.yml — no SSL, expose ports, use .envdocker-compose-prod.yml — SSL via reverse proxy, use .env.prod.github/workflows/deploy-prod.yml — SSH deploy pipeline.gitignore — ensure .env and .env.prod are ignoredDockerfile exists and is correctProgram.cs does NOT use AddEnvironmentVariables("PREFIX")appsettings.jsontools
Guides how to integrate the zTools package for ChatGPT, DALL-E image generation, file upload (S3), slug generation, email sending, and document validation in a .NET 8 project. Use when the user wants to use AI features, upload files, generate slugs, send emails, or understand zTools integration.
documentation
Generates a comprehensive, standardized README.md for any project. Use when the user wants to create or regenerate a README file following the project's documentation standard.
development
Create modal dialogs in the frontend using a custom Modal component built on top of Radix UI Dialog. Use this skill whenever the user asks to create, add, or modify a modal, dialog, popup, or confirmation prompt in the React application.
development
Create the complete frontend architecture for a new entity in the React application. Generates TypeScript types, service class, context provider, custom hook, and registers the provider in main.tsx. Use this skill when the user asks to create a new entity, feature module, or domain area in the frontend.