TIMEWARP SKILL — deploy a generated app from its Aspire AppHost: the target matrix (Docker Compose, Kubernetes/Helm, Azure: AKS or Azure Container Apps), aspire publish / aspire deploy per target, operator-run dev deploy / dev deprovision / dev open / dev deploy migrate (never CI), a local kind recipe, production-safety rules, secrets and parameters, Postgres migrations per target, the ingress topology, and container-runtime neutrality. Invoke before deploying, before adding a publish target, or before touching publish-mode wiring in the AppHost. WHEN: deploy the app, dev deploy, dev deprovision, dev open, dev deploy migrate, open the deployed app, migrate the deployed database, tear down a deployment, kind cluster, aspire publish, aspire deploy, docker compose, Helm chart, Kubernetes, AKS, Azure, Azure Container Apps, ACA, Flexible Server, Key Vault purge, production secrets, run migrations in production, ingress controller, Podman.

Maintained in timewarp-architecture · Canonical file: SKILL.md

Install

With TimeWarp.Skills:

dnx TimeWarp.Skills -- add tw-deploy https://timewarp.software/

Add a harness target and skills sync (see the TimeWarp.Skills page). Or copy the SKILL.md into your agent's skills directory.


Deploy (Aspire publish targets)

The AppHost (source/container-apps/aspire/projects/aspire-app-host/program.cs) is the single source of truth for deployment. Every deployment artifact — docker-compose.yaml, .env, the Helm chart, the migration script — is generated by aspire publish / aspire deploy. There are no hand-written Dockerfiles, manifests, or charts, and there is no devops/ tree. The AppHost Design region holds the per-line reasoning; this skill is the operator's map.

Detection — when to invoke

Signal Where
Deploying the app anywhere other than dev run any target below
Adding or changing a publish target / compute environment AppHost program.cs
Adding a setting that must differ in production parameters, not literals
Touching code guarded by IsPublishMode / IsRunMode AppHost program.cs
Adding a schema migration that must reach production migrations section

Target matrix

Target AppHost environment Publish:Target Artifact Run with
Standalone hardware AddDockerComposeEnvironment("compose") compose (default) docker-compose.yaml + .env + efmigrations/ aspire deploy, or docker compose up / podman compose up
Kubernetes (on-prem, any distribution) AddKubernetesEnvironment("k8s") + WithHelm kubernetes Helm chart directory aspire deploy, or helm upgrade --install
Azure (shared cluster, portable default) an existing AKS cluster is a Kubernetes target kubernetes Helm chart aspire deploy against the AKS kubectl context
Azure Container Apps (Azure-only) AddAzureContainerAppEnvironment("aca-env") aca Bicep (main.bicep + one module per resource) + efmigrations/ aspire deploy into the az login subscription

One target per publish. Aspire assigns every compute resource to exactly one compute environment, so Compose, Kubernetes and Azure Container Apps are alternatives selected by configuration, never both in one model. Publish:Target is read in publish mode only; any other value throws. Run mode (dev run) always ignores it.

Publishing and deploying

aspire publish only writes files — no image build, no deploy, no container runtime needed. aspire deploy builds images through the detected container runtime and applies them.

# Compose (default target)
dev publish compose                         # publish to artifacts/aspire-output/compose + safety suite
aspire publish --apphost <apphost.csproj> --output-path out/compose
aspire deploy  --apphost <apphost.csproj>   # fills .env, builds images, brings the stack up

# Kubernetes (Helm chart)
dev publish kubernetes                      # publish to artifacts/aspire-output/kubernetes + safety suite + helm lint
aspire publish --apphost <apphost.csproj> --output-path out/k8s -- --Publish:Target=kubernetes
aspire deploy  --apphost <apphost.csproj> -- --Publish:Target=kubernetes   # helm upgrade --install, current kubectl context

# Azure Container Apps (Bicep) — publishing needs no Azure credentials
dev publish aca                             # publish to artifacts/aspire-output/aca + safety suite
aspire publish --apphost <apphost.csproj> --output-path out/aca -- --Publish:Target=aca
aspire deploy  --apphost <apphost.csproj> -- --Publish:Target=aca   # provisions + pushes images, az CLI credential

Deploying: dev deploy, dev deploy migrate, dev open, dev deprovision

dev deploy / dev deprovision are thin, operator-run wrappers over aspire deploy / aspire destroy for one Publish:Target; dev deploy migrate and dev open are the two steps after a deploy (apply the migrations, reach the app), keyed on the same --target. No CI job, workflow step or dev workflow mode calls them, and none may. A merge never deploys.

dev deploy                                  # compose (the Publish:Target default): preflight, plan, prompt
dev deploy --target kubernetes              # Helm chart to the CURRENT kubectl context
dev deploy --target aca                     # Azure Container Apps in the az CLI's subscription (az login first)
dev deploy --target compose --yes           # no prompt: aspire deploy --non-interactive

dev deploy migrate --target kubernetes      # psql the published migration script into the deployed postgres
dev open --target kubernetes                # port-forward the ingress controller and open the browser

dev deprovision --target kubernetes         # preflight, then aspire destroy asks before deleting
dev deprovision --target kubernetes --yes   # aspire destroy --yes --non-interactive — deletes the deployment and its data
dev deprovision --target aca                # aspire destroy, then purge the soft-deleted Key Vault

Local Kubernetes with kind

A kind cluster is just another kubectl context: give it a local registry the nodes can pull from, install ingress-nginx once as cluster infrastructure, then deploy, migrate and open. The full pwsh and bash recipe (registry, cluster, node registry mapping, ingress, deploy, tear down) is in local-kubernetes-with-kind.md.

Production-safety rules

These never appear in published output. Each is enforced by the target's aspire-tests suite — extend the suite when you add a new publish-mode branch.

Never ships Why How it is kept out
Mock authentication (Authentication__UseMock) it signs requests in as a fake principal forwarded in run mode only; the server also fail-closes outside Development/Testing
Browser-log forwarding ships client telemetry to a dev dashboard Development-only; publish runs as Production
Dashboard Postgres REPL (WithRepl) an authenticated psql shell for anyone with dashboard access Development-only
Aspire dashboard in the deployment a second, unauthenticated UI and port WithDashboard(false) on every environment — point OTEL_EXPORTER_OTLP_ENDPOINT at your own collector. ACA deploys it as a public dotNetComponents resource unless disabled
Any host port / externally reachable service besides the ingress every extra exposure is an unaudited entry point Compose: only the ingress publishes a port (ingress-port). Kubernetes: every Service is ClusterIP; the cluster Ingress is the only way in. ACA: only the ingress container app has external: true; web, api and grpc are internal
Secret literals literals end up in images, repos and logs secrets are parameters (below)

Rules for new code:

Secrets and parameters

Every value that differs per deployment is an AddParameter, never a literal in code; a non-secret default every machine shares (the app's identity) is committed in the AppHost appsettings.json Parameters section, not in the AddParameter call. Secrets are AddParameter(name, secret: true) (or a generated secret such as Postgres' password).

Target Where parameter values live
Compose .env beside docker-compose.yaml; aspire deploy / aspire do prepare-compose fill it
Kubernetes values.yaml: secrets under secrets.<resource> (rendered into <resource>-secrets Secret objects, empty defaults), never ConfigMaps
Azure Container Apps @secure() Bicep parameters (no defaults) that become container-app secrets read through secretRef. The Postgres connection string lives in a Key Vault Aspire provisions, which web-server reads with its managed identity; the Postgres password is also on web-server as the container-app secrets postgres-db-password and postgres-db-uri, built from the @secure() parameter

Postgres and migrations per target

Migrations are explicit in every deployed environment: the AppHost never auto-migrates a deployment. web-server does not migrate at startup and tolerates a not-yet-migrated database. The published idempotent SQL script (efmigrations/web-migrations.sql) is safe to re-run.

Apply them with dev deploy migrate --target <target> (Deploying above). The last column is what it runs — run it yourself in a generated app without the dev CLI (<runtime> is docker or podman; dev deploy migrate passes the running project's --project-name and --file from <runtime> compose ls):

Target Storage Apply migrations (what dev deploy migrate runs)
Compose named volume postgres-data; POSTGRES_DB creates the database on first start <runtime> compose exec -T postgres sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U postgres -d postgres-db -v ON_ERROR_STOP=1' < efmigrations/web-migrations.sql
Kubernetes postgres-data PersistentVolumeClaim (postgres-storage-capacity, default 10Gi), single-replica StatefulSet kubectl exec -i -n <namespace> statefulset/postgres-statefulset -- sh -c 'PGPASSWORD="$POSTGRES_PASSWORD" psql -U postgres -d postgres-db -v ON_ERROR_STOP=1' < efmigrations/web-migrations.sql
Azure Container Apps Azure Database for PostgreSQL Flexible Server (managed storage and backups); the Bicep creates postgres-db the published bundle from the operator's machine, through a temporary firewall rule — see Azure Container Apps below

The script is piped on stdin (< efmigrations/web-migrations.sql in bash; in pwsh Get-Content -Raw efmigrations/web-migrations.sql | <command>).

Ingress topology

All public traffic enters through the YARP ingress, which owns the routing table (generated web /api prefixes, the api catch-all, /grpc prefix strip, and the public-host forwarding for web routes). The routing is therefore identical in run mode, Compose and Kubernetes.

Azure

An existing AKS cluster is a Kubernetes target: point the kubectl context at it, use the cluster's registry (for example an Azure Container Registry) as registry-endpoint, choose the controller's class as ingress-class, and run the Kubernetes commands above. The Kubernetes target uses nothing Azure-specific, so the same chart runs on any cluster.

Azure Container Apps (Publish:Target=aca)

Choose ACA for a small or mostly idle app with no cluster to share; choose AKS (the Kubernetes target) when a cluster exists, is shared, or the deployment must stay portable. The publish-only AppHost provisions a Container Apps environment (no dashboard), ingress/web/api/grpc container apps (only the ingress external) and an Azure Database for PostgreSQL Flexible Server whose firewall rule is AllowAllAzureIps (the admin password is the barrier; aca-publish-tests pins the rule). Deploy with dev deploy --target aca, migrate with dev deploy migrate --target aca --resource-group <rg>, open with dev open --target aca --resource-group <rg>, remove with dev deprovision --target aca.

Cost model, what the AppHost provisions, the deploy commands, the migration bundle by hand (firewall rule, connection string), where the Postgres credentials live, expected deploy problems (slow first deploy, "Server is busy", soft-deleted Key Vault names, web hop host) and deprovision detail: azure-container-apps.md.

Container-runtime neutrality