Start and manage tmux-backed dev servers exposed through Caddy at wildcard subdomains.
---
name: dev-serve
description: Start and manage tmux-backed dev servers exposed through Caddy at wildcard subdomains.
---
# dev-serve — One-Command Dev Server Hosting
Start a dev server in a tmux session and expose it via Caddy at `<project>.YOUR_DOMAIN`. One command up, one command down.
## Setup
1. **Install the script:**
```bash
cp scripts/dev-serve.sh ~/.local/bin/dev-serve
chmod +x ~/.local/bin/dev-serve
```
2. **Set your domain** (one of):
- Export `DEV_SERVE_DOMAIN` in your shell profile
- Or edit the `DOMAIN` variable in the script
3. **Requirements:**
- Caddy running with wildcard DNS + TLS (see [caddy](https://clawhub.com/skills/caddy) skill)
- `tmux`, `jq`, `curl`
- Caddy admin API on `localhost:2019`
## CLI
```bash
dev-serve up <repo-path> [port] # Start dev server + add Caddy route
dev-serve down <name> # Stop dev server + remove Caddy route
dev-serve ls # List active dev servers
dev-serve restart <name> # Restart dev server (keep Caddy route)
```
## How It Works
1. Derives subdomain from the repo folder name (`~/projects/myapp` → `myapp.YOUR_DOMAIN`)
2. Detects the dev command from `package.json` `scripts.dev` (supports vite, next, nuxt, sveltekit)
3. Auto-patches Vite `allowedHosts` if a vite config file exists
4. Starts the dev server in a tmux session named `dev-<name>` with `--host 0.0.0.0 --port <port>`
5. Adds a Caddy route + dashboard link to the Caddyfile
6. Reloads Caddy via admin API (no sudo, no restart)
7. Verifies end-to-end: waits for the dev server to listen, then polls HTTPS until 2xx/3xx (up to 90s)
## Examples
```bash
# Start with auto-assigned port (starts at 5200, skips used ports)
dev-serve up ~/projects/myapp
# → https://myapp.YOUR_DOMAIN
# Explicit port
dev-serve up ~/projects/myapp 5200
# Override dev command
DEV_CMD="bun dev" dev-serve up ~/projects/myapp 5300
# Stop and clean up
dev-serve down myapp
# List what's running
dev-serve ls
```
## Configuration
| Variable | Default | Description |
|----------|---------|-------------|
| `DEV_SERVE_DOMAIN` | *(must be set)* | Your wildcard domain (e.g. `mini.example.com`) |
| `DEV_SERVE_STATE_DIR` | `~/.config/dev-serve` | Where state JSON is stored |
| `CADDYFILE` | `~/.config/caddy/Caddyfile` | Path to your Caddyfile |
| `CADDY_ADMIN` | `http://localhost:2019` | Caddy admin API address |
| `DEV_CMD` | *(auto-detected)* | Override the dev server command |
## Port Convention
- **Permanent services:** 3100 range (managed in Caddyfile directly)
- **Dev servers:** 5200+ (managed by dev-serve, auto-assigned)
## Vite `allowedHosts`
Vite blocks requests from unrecognized hostnames. `dev-serve up` automatically patches `vite.config.ts` (or `.js`/`.mts`/`.mjs`) to add the subdomain. If auto-patching fails, it prints the manual fix.
## Architecture
```
Browser (Tailscale / LAN / etc.)
→ DNS: *.YOUR_DOMAIN → your server IP
→ Caddy (HTTPS with auto certs)
→ reverse_proxy localhost:<port>
→ Dev server (in tmux session)
```
## Companion Skills
- **[caddy](https://clawhub.com/skills/caddy)** — Required. Sets up the Caddy reverse proxy with wildcard TLS.
## Troubleshooting
**Dev server not starting:**
```bash
tmux attach -t dev-<name> # see what happened
```
**Cert not provisioning (curl exit 35):**
Wait 30-60s for DNS-01 challenge. Check `tail -20 /var/log/caddy-error.log`.
**Caddy reload failed:**
```bash
caddy reload --config ~/.config/caddy/Caddyfile --address localhost:2019
```
**403 from Vite:**
The subdomain wasn't added to `allowedHosts`. Add it manually to your `vite.config.ts`:
```ts
server: { allowedHosts: ['myapp.YOUR_DOMAIN'] }
```
don't have the plugin yet? install it then click "run inline in claude" again.
added explicit inputs section with env vars and dependencies, broke procedure into 10 numbered steps with clear input-output contracts, formalized decision points for error cases (port conflict, missing dev script, api failure), added output contract for each command, added outcome signal with concrete verification steps, preserved original intent and all configuration details.
spin up a dev server in a tmux session and expose it over HTTPS at <project>.YOUR_DOMAIN via Caddy reverse proxy. use this when you need a live dev environment accessible from outside localhost (tailscale, LAN, etc.) without manual DNS, TLS, or port forwarding. one command to launch, one to tear down.
environment variables:
DEV_SERVE_DOMAIN (required): your wildcard domain, e.g. mini.example.com. must resolve to your server IP with wildcard DNS (*.mini.example.com).DEV_SERVE_STATE_DIR (optional, default ~/.config/dev-serve): where the skill stores state JSON (active servers list).CADDYFILE (optional, default ~/.config/caddy/Caddyfile): path to your Caddyfile.CADDY_ADMIN (optional, default http://localhost:2019): Caddy admin API endpoint.DEV_CMD (optional, auto-detected): override the dev server command. useful if auto-detection fails.external connections:
http://localhost:2019. ensure it's listening and accessible.binary dependencies:
tmux: session manager for dev servers.jq: JSON parsing for state and Caddy API responses.curl: HTTP requests to Caddy admin API and dev server healthchecks.node or compatible package manager: to read package.json and detect dev command.local setup:
scripts/dev-serve.sh to ~/.local/bin/dev-serve and chmod +x.DEV_SERVE_DOMAIN in your shell profile (.bashrc, .zshrc, etc.) or export it inline.~/.config/dev-serve/ exists or is creatable (skill creates it if missing).step 1: parse input and resolve repo path
<repo-path> (e.g. ~/projects/myapp) and optional [port].~ and relative paths to absolute path./home/user/projects/myapp → myapp.DEV_SERVE_DOMAIN).step 2: detect dev server command
DEV_CMD.DEV_CMD is set, use it.package.json in repo root, extract scripts.dev field.dev script, check for dev:vite, dev:nuxt, dev:sveltekit, dev:next.DEV_CMD).vite dev, next dev, npm run dev).step 3: select or auto-assign port
[port] argument, or none.netstat or lsof, skip occupied ports, return first free port.step 4: auto-patch Vite allowedHosts if needed
vite.config.ts, vite.config.js, vite.config.mts, or vite.config.mjs exists in repo root.server.allowedHosts array.myapp.YOUR_DOMAIN) is not already in the array, append it programmatically.step 5: create tmux session and start dev server
dev-<subdomain>.tmux new-session -d -s dev-<subdomain>.--host 0.0.0.0 --port <port> flags injected (or equivalent for the framework).step 6: poll for dev server to listen locally
nc -zv localhost <port> or curl http://localhost:<port> to check if port is accepting connections.step 7: add Caddy reverse proxy route
<subdomain>.YOUR_DOMAIN {
reverse_proxy localhost:<port>
}
step 8: reload Caddy via admin API
CADDY_ADMIN endpoint.http://localhost:2019/load with the updated Caddyfile.step 9: verify end-to-end (dev server HTTPS response)
curl -s -o /dev/null -w "%{http_code}" https://<subdomain>.YOUR_DOMAIN/.step 10: save state and print success
~/.config/dev-serve/servers.json):{
"name": "<subdomain>",
"port": <port>,
"url": "https://<subdomain>.YOUR_DOMAIN",
"started": "<ISO8601>",
"tmux_session": "dev-<subdomain>"
}
✓ dev-serve up: myapp on port 5200
→ https://myapp.YOUR_DOMAIN
down command procedure:
myapp).tmux kill-session -t dev-<subdomain>.✓ dev-serve down: myapp.ls command procedure:
~/.config/dev-serve/servers.json).name | port | url | started.restart command procedure:
tmux kill-session -t dev-<subdomain>.✓ dev-serve restart: myapp.if port is already in use:
if dev command cannot be auto-detected:
package.json and error message.dev script to package.json or set DEV_CMD environment variable.if Vite config auto-patch fails:
allowedHosts: ['myapp.YOUR_DOMAIN'] to vite.config.ts).if tmux session already exists:
dev-serve restart <subdomain> or dev-serve down <subdomain> first.if Caddy admin API is unreachable:
if HTTPS healthcheck fails after 90 seconds:
tmux attach -t dev-<name>" and exit (leave tmux session for user inspection).if DEV_SERVE_DOMAIN is not set:
if state file is corrupted (invalid JSON):
servers.json.bak.up command:
~/.config/dev-serve/servers.json) updated with new entry.dev-<subdomain> created and running.down command:
ls command:
restart command:
error cases:
https://<subdomain>.YOUR_DOMAIN in a browser (via tailscale, LAN, or public if DNS allows) and see your dev server running.dev-serve ls lists the active server with correct URL and port.tmux ls shows the dev-<subdomain> session active.curl -s https://<subdomain>.YOUR_DOMAIN -v returns 2xx/3xx or expected response from dev server (no 5xx, no cert errors).dev-serve down <subdomain> immediately removes the route; HTTPS access fails (Caddy 404).dev-serve restart <subdomain> does not change the URL or require DNS/TLS updates.