Skip to content
Development
Skill

/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 plugin
vexjoy-agent
421122 skills198 agents11 commands76 hooks
Install
$ npx -y skills add notque/vexjoy-agent --skill public-web-deploy --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/public-web-deploy

Context preview

The summary Claude sees to decide when to auto-load this skill.

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

SKILL.md

public-web-deploy.SKILL.md
name: public-web-deploy
description: "Publish a public website safely: DNS, web server, HTTPS, hardening, verify. Routes raw dev servers through nginx/Caddy/Apache/Cloudflare Pages."
user-invocable: false
allowed-tools:
  - Bash
  - Read
  - Edit
  - Write
  - Glob
  - Grep
routing:
  force_route: true
  triggers:
    - "public site"
    - "public website"
    - "static site"
    - "landing page"
    - "deploy website"
    - "deploy site"
    - "host a website"
    - "serve this website"
    - "put this online"
    - "put site online"
    - "website online"
    - "make it public"
    - "make public"
    - "use my domain"
    - "point my domain"
    - "set up https"
    - "nginx public site"
    - "go live"
  category: infrastructure
  not_for: "domain MODELING / DDD bounded contexts (use a code agent); local-only preview on 127.0.0.1 (no deploy needed); generating HTML artifacts (use html-artifact); HTTPS client/API bugs and internal-only nginx reverse proxies (use a code/infra agent). Fires when a site goes internet-facing."
  pairs_with:
    - shell-process-patterns
    - service-health-check
    - kubernetes

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 ($requ
Read more
Ships withvexjoy-agent

Essays and writing behind this toolkit live at vexjoy.com. VexJoy Agent connects plain-English requests to specialist agents, skills, and workflows. /do selects the knowledge and tools needed for your task.

Get the whole plugin

Other skills on vexjoy-agent.