filiptrivan/spiderly/deployment

Claude Code and agent skill by filiptrivan/spiderly

Trust tier 2
  • community
Source check pending

Checking that the SKILL.md file is still in the repository.

Deploy a Spiderly project to production and keep it running. Use when deploying, redeploying, shipping, releasing, or rolling out the .NET backend or Angular admin — first-time setup or an ongoing deploy — and when diagnosing a down or erroring production origin (502/521, container crash-loop, failed deploy workflow). Covers the recommended VPS + Docker Compose + Caddy + Cloudflare + Terraform stack, CI/CD pipelines, TLS with Cloudflare origin certificates, and infrastructure-as-code layout.

Install

npx vskill@latest install filiptrivan/spiderly/deployment

SKILL.md

Deployment

Spiderly runs anywhere a .NET app does — there is no required host, orchestrator, or reverse proxy. The stack below is the one we recommend and test end to end: a single VPS running Docker Compose, fronted by Caddy and Cloudflare, with all infrastructure declared in Terraform. Swap any tier for your own — the only genuinely Spiderly-specific deploy concerns are flagged inline (EF migrations before app start, forwarded-header/proxy trust, cookie domain across subdomains, the CORS frontend URL, and the api + admin subdomain split).

Recommended stack

TierChoiceWhy
ComputeHetzner Cloud (or any VPS)Predictable monthly cost, full control, no PaaS lock-in
OrchestrationDocker ComposeSingle-host simplicity; reproducible, agent-friendly deploys
Reverse proxy / TLSCaddy v2Auto-config from Cloudflare origin certs, simple Caddyfile
DNS / WAF / CDNCloudflare (orange-cloud)DDoS protection, origin certs, Turnstile
IaCTerraformDeclarative; one source of truth for VPS + DNS + certs
State backendCloudflare R2S3-compatible, free tier, encrypted at rest
Container registryGitHub Container Registry (GHCR)Free for public/internal repos, native to GitHub Actions
CI/CDGitHub ActionsBuild, push, SSH-deploy in one workflow file

What to host where

  • .NET Backend → Hetzner+Docker (recommended)
  • Angular admin → Hetzner+Docker, same VPS as the backend, served by the same Caddy on a separate subdomain
  • Next.js storefront (if applicable) → Vercel recommended. Next.js + Vercel gives you ISR, edge caching, image optimization, PPR, and instant preview URLs. Hetzner+Docker for SSR is viable but loses these features. Use Hetzner only if you need a single ops surface.

Compose stack shape

Backend/docker-compose.prod.yml (placed alongside the backend project):

yaml
services:
caddy:
image: caddy:2
command: caddy run --config /etc/caddy/Caddyfile --watch
ports: ["80:80", "443:443"]
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile:ro
- ./certs:/etc/caddy/certs:ro
- caddy_data:/data
depends_on:
backend:
condition: service_started
admin:
condition: service_healthy
restart: unless-stopped
backend:
image: ${BACKEND_IMAGE}
environment:
ASPNETCORE_ENVIRONMENT: Production
ASPNETCORE_HTTP_PORTS: "8080"
AppSettings__Spiderly.Shared__ConnectionString: "Host=postgres;Port=5432;Database=${DB_NAME};Username=postgres;Password=${DB_PASSWORD};SSL Mode=Disable;"
# ... plus storage / mail / OAuth env vars (see file-storage skill for the storage set)
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8080/health"]
interval: 30s
timeout: 5s
start_period: 60s
retries: 3
depends_on:
postgres: { condition: service_healthy }
restart: unless-stopped
admin:
image: ${ADMIN_IMAGE}
expose: ["80"]
healthcheck:
test: ["CMD", "wget", "-q", "--spider", "http://localhost/"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
restart: unless-stopped
postgres:
image: postgres:18
environment:
POSTGRES_DB: ${DB_NAME}
POSTGRES_USER: postgres
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes: [postgres_data:/var/lib/postgresql] # 18+ layout — see Key points
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
retries: 5
restart: unless-stopped
volumes:
caddy_data:
postgres_data:

Key points:

  • Only caddy binds host ports. backend and admin are reachable only on the internal Docker network — Caddy reverse-proxies to them by service name.
  • caddy.depends_on uses condition: service_healthy for the admin container (so Caddy doesn't 502 before the inner Caddy has bound port 80) but condition: service_started for the backend — the backend's /health endpoint only goes green after EF migrations and warmup, and gating Caddy on that would block all traffic for 20–30 s on every restart.
  • Image tags come from CI via envsubst (${BACKEND_IMAGE}, ${ADMIN_IMAGE}).
  • postgres:18+ mounts at `/var/lib/postgresql`, not `/var/lib/postgresql/data`. The 18 image moved data into a major-version subdirectory (PGDATA=/var/lib/postgresql/18/docker). With the pre-18 mount path the entrypoint hard-errors ("there appears to be PostgreSQL data in /var/lib/postgresql/data") and the container crash-loops — and even when it starts, data lands in an anonymous volume and does not survive container recreation.

Caddy site blocks

Backend/Caddyfile — extracts compression + security headers into a (common) snippet so both site blocks stay DRY. Cloudflare also sets some of these (HSTS, Brotli) at its edge, but defense-in-depth at the origin is cheap and keeps origin→Cloudflare bandwidth compressed:

(common) {
encode zstd gzip
header {
Strict-Transport-Security "max-age=31536000; includeSubDomains; preload"
X-Content-Type-Options nosniff
X-Frame-Options DENY
Referrer-Policy strict-origin-when-cross-origin
-Server
}
}
api.<your-domain> {
import common
tls /etc/caddy/certs/origin.pem /etc/caddy/certs/origin-key.pem
reverse_proxy backend:8080
}
admin.<your-domain> {
import common
tls /etc/caddy/certs/origin.pem /etc/caddy/certs/origin-key.pem
reverse_proxy admin:80
}

Both subdomains share a single Cloudflare origin cert with both names as SANs (set up in Terraform — see below).

Backend Dockerfile

Backend/Dockerfile (multi-stage SDK build → aspnet runtime):

dockerfile
FROM mcr.microsoft.com/dotnet/sdk:9.0.102 AS build
WORKDIR /src
COPY ["<YourApp>.WebAPI/<YourApp>.WebAPI.csproj", "<YourApp>.WebAPI/"]
# ... copy other csprojs, restore, copy source, publish ...
RUN dotnet publish "<YourApp>.WebAPI/<YourApp>.WebAPI.csproj" -c Release -o /app/publish /p:UseAppHost=false
FROM mcr.microsoft.com/dotnet/aspnet:9.0.1
WORKDIR /app
RUN apt-get update && apt-get install -y --no-install-recommends curl && rm -rf /var/lib/apt/lists/*
COPY --from=publish /app/publish .
USER app
EXPOSE 8080
ENTRYPOINT ["dotnet", "<YourApp>.WebAPI.dll"]

Pin patch versions (9.0.102 SDK, 9.0.1 runtime) — :9.0 floats and breaks reproducible builds.

Run as non-root. The aspnet image ships an app user (UID 1654). Add USER app after COPY for defense-in-depth — limits the blast radius of a container escape and matches the platform's sandbox model.

Log volumes: if you're using a Serilog File sink (not just Console) with a Hangfire log-archival job, see Log archival in the backups skill for the full bind-mount-vs-Console-only trade-off. Greenfield deploys can stick with Console sink + Docker's json-file rotation and skip the volume entirely.

Angular admin Dockerfile

Frontend/Dockerfile (multi-stage Node build → Caddy alpine runtime):

dockerfile
FROM node:22-alpine AS build
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
RUN npm run build
FROM caddy:2-alpine
COPY --from=build /app/dist/<YourApp>/browser /srv
COPY Caddyfile /etc/caddy/Caddyfile
EXPOSE 80
HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \
CMD wget -q --spider http://localhost/ || exit 1

Frontend/Caddyfile (internal — runs inside the admin container):

:80 {
root * /srv
encode zstd gzip
# Content-hashed build output (main-QCWZPP5O.js, media/primeicons-4GST5W3O.woff2) that exists
# on disk; a missing chunk falls back to index.html and must not be marked immutable.
@hashed {
path_regexp ^/(media/)?[^/]+-[A-Z0-9]{8}\.[a-z0-9]+$
file
}
header @hashed Cache-Control "public, max-age=31536000, immutable"
# Everything else (index.html under every route, /assets/*) is revalidated on every use.
header ?Cache-Control "no-cache"
try_files {path} /index.html
file_server
}

The try_files {path} /index.html line is the SPA fallback that lets the Angular router handle deep links.

Two details in the cache rules matter. Angular's esbuild builder names hashed files name-HASH.js (eight uppercase letters and digits), so a \.[a-f0-9]{8,}\.js pattern written for the old webpack names matches nothing. And header runs before try_files, so a path /index.html matcher never sees a navigation to / or /orders: the ?Cache-Control default covers them. Without any Cache-Control a browser guesses a freshness lifetime (Chromium: a tenth of the time since Last-Modified) and can open the app from a pre-deploy index.html.

Lockfile gotcha: npm ci requires package.json and package-lock.json to be in sync. If a stale dev-dep pins an older Angular major as a peer (e.g. @jsverse/transloco-keys-manager 5.x pins Angular 17 while your project is on Angular 19), npm ci fails. Bump the dev-dep to the matching major (e.g. transloco-keys-manager 6.x for Angular 19) rather than reaching for --legacy-peer-deps.

Terraform layout

Split files by provider concern; one provider, one or two files:

infrastructure/
├── main.tf # required_providers + state backend
├── variables.tf
├── outputs.tf
├── hetzner-firewall.tf # firewall, allowed ports
├── cloudflare-dns.tf # api/admin A records
├── cloudflare-origin-cert.tf # origin CA cert + private key
└── cloudflare-zone.tf # zone + headers

main.tf providers:

hcl
terraform {
required_version = ">= 1.5"
backend "s3" {
# Cloudflare R2 — S3-compatible
bucket = "<your-app>-terraform-state"
key = "infrastructure/terraform.tfstate"
region = "auto"
skip_credentials_validation = true
skip_metadata_api_check = true
skip_region_validation = true
skip_requesting_account_id = true
skip_s3_checksum = true
endpoints = { s3 = "https://<account-id>.r2.cloudflarestorage.com" }
}
required_providers {
cloudflare = { source = "cloudflare/cloudflare", version = "~> 5.0" }
hcloud = { source = "hetznercloud/hcloud", version = "~> 1.49" }
tls = { source = "hashicorp/tls", version = "~> 4.0" }
}
}

Origin cert covering both api and admin subdomains (cloudflare-origin-cert.tf):

hcl
resource "tls_private_key" "origin" {
algorithm = "RSA"
rsa_bits = 2048
lifecycle {
prevent_destroy = true
}
}
resource "tls_cert_request" "origin" {
private_key_pem = tls_private_key.origin.private_key_pem
subject {
common_name = "<your-domain>"
organization = "<YourApp>"
}
dns_names = [
"api.<your-domain>",
"admin.<your-domain>",
]
}
resource "cloudflare_origin_ca_certificate" "origin" {
csr = tls_cert_request.origin.cert_request_pem
hostnames = tls_cert_request.origin.dns_names
request_type = "origin-rsa"
requested_validity = 5475 # 15 years
lifecycle {
prevent_destroy = true
# Cloudflare normalizes hostname/CSR ordering server-side; ignore both to avoid spurious recreate.
ignore_changes = [hostnames, csr]
}
}

Sensitive outputs (outputs.tf) so the deploy workflow can pull cert + key into GitHub Secrets:

hcl
output "origin_cert" {
value = cloudflare_origin_ca_certificate.origin.certificate
sensitive = true
}
output "origin_cert_key" {
value = tls_private_key.origin.private_key_pem
sensitive = true
}

Retrieve with terraform output -raw origin_cert and paste into a GitHub Secret. Note: the API token must have Origin CA: Edit scope for cloudflare_origin_ca_certificate to work.

CI/CD

Two workflows — one per deploy unit. Both should share a concurrency group so they serialize on the same VPS:

This is the start of the file. Read the full SKILL.md on GitHub.

Security

  • Security scanPassed, 100 of 100Pattern checks on the skill's files.
  • Model reviewPassed

Quality evaluation

  • Task evaluationNot runNo evaluation yet. Run one in Skill Studio.

Source

Popularity

Repository stars
60
Repository forks
24
7-day trend score
21

Stars and forks are for the whole GitHub repository, not this skill alone. Not refreshed yet.

Works with

Any agent vskill installs to: 56 targets in all. These 39 have a page on this site.

  • Claude Code
  • Cursor
  • GitHub Copilot
  • Windsurf
  • Codex
  • Gemini CLI
  • Cline
  • Amp
  • Roo Code
  • Goose
  • Continue
  • Kilo Code
  • Junie
  • Trae
  • OpenHands
  • Qwen Code
  • AI Agent
  • Tabnine Chat
  • Sourcegraph Cody
  • Amazon Q
  • Firebase Genkit
  • Sweep
  • Devin
  • Phind
  • Mentat
  • Claude Desktop
  • Aider
  • GPT Pilot
  • Plandex
  • bolt.new
  • v0
  • Replit Agent
  • Anse
  • TypingMind
  • BetterChatGPT
  • LibreChat
  • Warp AI
  • Pieces
  • Fig AI
Embed badge
vskill VERIFIED badge
[![vskill VERIFIED](https://verified-skill.com/api/v1/skills/filiptrivan/spiderly/deployment/badge)](https://verified-skill.com/skills/filiptrivan/spiderly/deployment)

Something wrong with this skill or its page? Tell us.