agentsclimarketplace

Public web deploy

Skill notque/vexjoy-agent/skills/infrastructure/public-web-deploy

Publish a public website safely: DNS, web server, HTTPS, hardening, verify. Routes raw dev servers through nginx/Caddy/Apache/Cloudflare Pages.From its SKILL.md

Install
npx -y skills add notque/vexjoy-agent --skill public-web-deploy

Assembled from the repository path, not quoted from the project. Check it against their README if it does not work.

SKILL.md

14.7 KB, ~3.7k tokens by cl100k_base, as published. Nobody here has run it

Public Web Deploy Skill

Top Rule

Serve public sites through a real web server. Local preview binds 127.0.0.1; public sites go through nginx, Caddy, Apache, or Cloudflare Pages — fronted by HTTPS and hardened nginx config.

Never use python -m http.server, Vite, Hugo, Next dev, Flask dev, or any raw dev server as an internet-facing public service. Local preview MUST bind 127.0.0.1. Public sites MUST go through nginx / Caddy / Apache / Cloudflare Pages.

Raw dev servers are single-threaded, unauthenticated, serve the whole working directory (dotfiles, .env, .git, source, backups), carry no TLS, no rate limiting, and no request filtering. They are correct for 127.0.0.1 local preview and wrong for any internet-facing service. A companion enforcement hook blocks public binds at the tool layer; this skill is the guidance that pairs with it.

Decide public vs private first. A public site is reached by HTTPS + hardened nginx — that is the security model. Reserve auth (basic-auth, SSO) for private/internal sites, decided explicitly. Adding basic-auth to a public site breaks it for its intended audience and adds no protection to content meant to be public.


Instructions

Phase 1: DNS

Goal: The domain/subdomain resolves to the host before any web server work.

Steps:

  1. Confirm the target FQDN (apex example.com, subdomain app.example.com).
  2. Create an A/AAAA record pointing at the host's public IP (or CNAME for managed platforms like Cloudflare Pages).
  3. Verify resolution propagated:
    dig +short A app.example.com
    dig +short AAAA app.example.com
    getent hosts app.example.com
    

Gate: dig +short returns the intended host IP. Proceed only when DNS resolves correctly — an HTTPS cert request fails if DNS does not yet point at the host.

Phase 2: Web Server Config

Goal: A production web server (not a dev server) serves the site from a defined docroot.

Steps:

  1. Pick the server: nginx or Caddy (self-hosted), Apache (existing stacks), or Cloudflare Pages (managed static).
  2. Define a server block bound to the FQDN with an explicit docroot:
    server {
        listen 80;
        server_name app.example.com;
        root /var/www/app.example.com;
        index index.html;
        location / { try_files $uri $uri/ =404; }
    }
    
  3. If an app backend exists, proxy to it on 127.0.0.1:
    location /api/ { proxy_pass http://127.0.0.1:8000; }
    
    The backend binds 127.0.0.1, never 0.0.0.0 — only nginx faces the internet.
  4. Test config and reload:
    nginx -t && systemctl reload nginx
    

Gate: nginx -t reports syntax OK and the site responds on port 80. The application/dev port is reachable only on 127.0.0.1.

Phase 3: HTTPS

Goal: Valid TLS cert installed, auto-renewal proven, HTTP redirects to HTTPS.

Steps:

  1. Issue a cert (Let's Encrypt via certbot, or Caddy/Cloudflare automatic TLS):
    certbot --nginx -d app.example.com
    
  2. Prove renewal works before trusting it:
    certbot renew --dry-run
    
  3. Confirm the HTTP->HTTPS redirect returns a 301/308 to https:// (certbot's nginx installer adds it; verify both the status and the target):
    curl -sI http://app.example.com | grep -iE '^HTTP/.* (301|308)'
    curl -sI http://app.example.com | grep -i '^location: https'
    

Gate: HTTPS serves a valid chain, certbot renew --dry-run exits 0, and plain HTTP 301/308-redirects to HTTPS. Proceed only when all three hold.

Phase 4: Hardening

Goal: Lock the surface to the minimum needed to serve the site.

Steps:

  1. Firewall — allow only intended ports. UFW applies to both IPv4 and IPv6 when IPV6=yes in /etc/default/ufw; confirm v6 parity if the host has an AAAA record:
    ufw allow 80/tcp
    ufw allow 443/tcp
    ufw allow OpenSSH
    ufw --force enable          # --force skips the interactive prompt
    ufw status verbose          # entries show (v6) duplicates when IPv6 is on
    
    No raw app/dev ports (8000, 5173, 1313, 3000…) appear in ufw status.
  2. nginx — deny sensitive paths, disable directory listing, restrict methods:
    autoindex off;   # no directory listing (default off; set explicitly so it can't be inherited on)
    
    # Deny dotfiles, source, configs, logs, backups, archives, editor/VCS leftovers
    location ~ /\.            { deny all; }            # .env .git .htaccess
    location ~* \.(md|env|ini|conf|log|bak|old|orig|swp|sql|yml|yaml|zip|tar|tar\.gz|tgz)$ { deny all; }
    location ~ ~$            { deny all; }             # editor backup files like index.html~
    
    # Restrict methods to read-only INSIDE the served location.
    # limit_except is location-scoped and evaluated per matched location;
    # a server-level `if ($request_method ...)` runs before location
    # selection and leaks the restriction to proxied/other locations.
    # Add this directive to the SAME `location /` from Phase 2 — do not
    # create a second `location /`:
    #   limit_except GET HEAD { deny all; }   # 403 on POST/PUT/DELETE
    
    The consolidated location / (method limit + rate limit) is shown in step 4. When an API location genuinely needs POST/PUT, give it its own limit_except (or none) — the restriction stays scoped to the static location /.
  3. Security headers:
    # HSTS: start WITHOUT includeSubDomains. Add it only once you have confirmed
    # EVERY subdomain (current and future) serves HTTPS — includeSubDomains forces
    # HTTPS on all of them and a non-HTTPS subdomain becomes unreachable.
    add_header Strict-Transport-Security "max-age=31536000" always;
    # PRECONDITION to append "; includeSubDomains": all subdomains are HTTPS-capable.
    #   add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    
    add_header X-Content-Type-Options "nosniff" always;
    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;
    
    # CSP: deploy in Report-Only first so a too-strict policy cannot break the site.
    # Inventory this site's real asset origins (CDNs, fonts, analytics, inline
    # scripts) BEFORE enforcing — a blanket "default-src 'self'" silently blocks
    # any third-party asset the pages actually load.
    add_header Content-Security-Policy-Report-Only "default-src 'self'" always;
    # Promote to enforcing only after Report-Only shows zero legitimate violations:
    #   add_header Content-Security-Policy "default-src 'self'" always;
    
  4. Rate limiting — define the zone in http{}, enforce it in the served location:
    # http{} context (e.g. /etc/nginx/nginx.conf or a conf.d include)
    limit_req_zone $binary_remote_addr zone=web:10m rate=10r/s;
    
    This is the ONE canonical location / for the server block — it replaces the minimal location / from Phase 2 and carries both the method limit and the rate limit (one location / per server block, no duplicates):
    location / {
        try_files $uri $uri/ =404;             # Phase 2
        limit_except GET HEAD { deny all; }    # step 2: 403 on POST/PUT/DELETE
        limit_req zone=web burst=20 nodelay;   # active rate limit — enforced, not commented
    }
    
    A limit_req_zone with no matching limit_req defines a zone that never throttles anything.
  5. fail2ban watching nginx for bad requests and bot probes:
    apt-get install -y fail2ban
    # enable jails: nginx-http-auth, nginx-bad-request, nginx-botsearch
    systemctl enable --now fail2ban && fail2ban-client status
    

Gate: All five hardening items applied; nginx -t passes; ufw status shows only 80/443/SSH.

Phase 5: Verify

Goal: Prove the live site is correct and the dangerous surfaces are closed.

Steps (run the full security checklist below), then spot-check from outside the host:

curl -sI https://app.example.com | head             # 200 + security headers (HSTS/CSP/etc.)
curl -s  https://app.example.com/.env -o /dev/null -w '%{http_code}\n'        # 403/404
curl -s  https://app.example.com/.git/config -o /dev/null -w '%{http_code}\n' # 403/404
curl -s  https://app.example.com/backup.zip -o /dev/null -w '%{http_code}\n'  # 403/404 (archive deny)
curl -s  https://app.example.com/ -o /dev/null -w '%{http_code}\n'            # no autoindex on a dir
curl -X POST -s https://app.example.com/ -o /dev/null -w '%{http_code}\n'     # 403 for static (limit_except deny all)
ss -tlnp | grep -vE '127\.0\.0\.1|:(80|443)\b'      # no app/dev port on a public iface
# Mixed content: served pages reference only https:// subresources
curl -s https://app.example.com/ | grep -oE 'http://[^"'\'' ]+' || echo "no http:// subresources"

Gate: Every checklist item passes. The site serves over HTTPS; dotfiles/configs return 403/404; no raw dev/app port is internet-reachable.


Public-Site Security Checklist

Run every item before declaring the site live. Each line is pass/fail.

#CheckPass condition
1DNS points to hostdig +short returns the intended public IP (A and, if present, AAAA)
2HTTPS cert installed + renewal provenvalid chain served; certbot renew --dry-run exits 0
3HTTP->HTTPS redirectplain HTTP returns 301/308 to https:// (status, not just a Location header)
4No raw app/dev ports exposedapp/backend binds 127.0.0.1; nothing else on a public iface
5UFW allows only intended ports, IPv4 and IPv6ufw status shows only 80/443/SSH; v6 parity when an AAAA record exists
6nginx denies sensitive pathsdotfiles, .md/.env/config/logs, backups, archives (.zip/.tar/.tgz), editor/VCS leftovers (~/.old/.orig/.swp) return 403/404
7Directory listing disabledautoindex off; a directory URL does not return a file index
8Methods limited to GET/HEAD unless app needs morelimit_except GET HEAD in the served location; other methods return 403 for static sites
9Security headers presentHSTS (includeSubDomains only when all subdomains are HTTPS-capable), X-Content-Type-Options, X-Frame-Options, Referrer-Policy set; CSP deployed Report-Only first, promoted to enforcing after zero legitimate violations
10No mixed contentevery subresource (img/script/style/font) loads over HTTPS; no http:// references in served pages
11Rate limiting activelimit_req zone applied to public locations
12fail2ban watches nginx bad requests/bot probesnginx-bad-request + nginx-botsearch jails active
13Public-vs-private access decided explicitlypublic = HTTPS + hardened nginx (no default basic-auth); auth only on private/internal sites

Examples

Example 1: "Put my static site online at mysite.com"

  1. Add A record -> host IP; verify dig +short (DNS).
  2. nginx server block, docroot /var/www/mysite.com, nginx -t && reload (Web Server).
  3. certbot --nginx -d mysite.com; certbot renew --dry-run; confirm HTTP->HTTPS (HTTPS).
  4. UFW 80/443/SSH; deny dotfiles; GET/HEAD only; headers; rate limit; fail2ban (Hardening).
  5. Run the 13-item checklist; curl-probe .env/.git/POST from outside (Verify). Result: public HTTPS static site, no dev server, sensitive paths closed.

Example 2: "Use my domain to serve this app I'm running on :8000"

  1. Keep the app bound to 127.0.0.1:8000 (local). Add DNS for the FQDN.
  2. nginx server block reverse-proxies location / -> http://127.0.0.1:8000.
  3. certbot for TLS; HTTP->HTTPS redirect.
  4. UFW exposes only 80/443/SSH — port 8000 stays private; harden nginx.
  5. Verify the app is reachable only through nginx, never directly on :8000. Result: app served publicly via nginx+HTTPS; raw app port never internet-facing.

Error Handling

Error: certbot fails with "DNS problem / NXDOMAIN"

Cause: DNS has not propagated, or the record points elsewhere. Solution: Re-run dig +short A <fqdn>; wait for propagation or fix the record. Re-issue the cert once DNS resolves to the host.

Error: site loads on HTTP but not HTTPS

Cause: cert issued but nginx not listening on 443, or firewall blocks 443. Solution: nginx -t for a listen 443 ssl block; ufw allow 443/tcp; systemctl reload nginx.

Error: dotfiles or .env are downloadable

Cause: deny rules missing, or a more specific location (an exact = or longer prefix match) serves the file before the regex deny is consulted. nginx matches regex location blocks in file order but a prefix/exact match can win regardless of position. Solution: Add the location ~ /\. and extension deny blocks; keep them ahead of other regex location blocks and confirm no exact/prefix location shadows them; reload and re-probe with curl.

Error: someone reached the app directly on its dev port

Cause: backend bound 0.0.0.0 and/or UFW allows the port. Solution: Rebind the backend to 127.0.0.1; remove the port from UFW; verify with ss -tlnp that only 80/443 face the internet.


References

Why not a raw dev server in public

ConcernRaw dev server (http.server, Vite, Next/Flask dev)nginx / Caddy / Apache / CF Pages
TLSnonefirst-class, auto-renew
Directory exposureserves whole CWD incl. dotfilesexplicit docroot + deny rules
Concurrencysingle-threadedproduction event loop / workers
Request filteringnonemethod limits, rate limiting, fail2ban
HeadersnoneHSTS/CSP/etc. configured

Local preview (allowed)

Local preview binds loopback so it never faces the internet:

python3 -m http.server 8080 --bind 127.0.0.1   # local only

For public access, front it with nginx — see the workflow above.

What ships with it

Read from the repository

Just SKILL.md. No reference files, no scripts.

Gives 0 of the 12 instructions most ship operate skills give in ~3.7k tokens

Counted across 779 of the 1,178 authors here whose files we hold, read 2026-08-07

  • Document a rollback plan before deploymentin 41 of 779, across 22 files
  • Update the changelogin 21 of 779, across 19 files
  • Run the test suitein 20 of 779
  • Create an annotated git tagin 20 of 779
  • Clean up feature flags after full rolloutin 18 of 779, across 10 files
  • Verify deployment health after launchin 18 of 779, across 10 files
  • Test both feature flag statesin 17 of 779, across 9 files
  • Verify the working tree is cleanin 17 of 779
  • Make database migrations backward-compatiblein 16 of 779, across 8 files
  • Set up error monitoring before launchin 15 of 779, across 7 files
  • Monitor metrics at each rollout stagein 14 of 779, across 5 files
  • Create a GitHub releasein 14 of 779

Said here and by no other author read

  • route public sites through a real web server
  • bind local previews to localhost only
  • create DNS records pointing to the host
  • verify DNS propagation before web server setup
  • bind app backends to localhost only
  • obtain TLS certificates

Grouped from the skills themselves: near-identical wordings counted once, and counted by distinct author, so one author publishing three of these counts once. Length counted with cl100k_base; the agent that loads this file may tokenize it differently.

Keep looking

Skills are one crate of 326,861. Ordering is by how many stacks a row turns up in, so the top of any crate is what has actually been picked rather than what has the most stars.