Skip to content

Latest commit

 

History

History
278 lines (197 loc) · 7.86 KB

File metadata and controls

278 lines (197 loc) · 7.86 KB

Setup: the Linux box

For the agent reading this: You are running on the always-on Linux box. Work through these phases with the user, one at a time. Confirm each phase succeeds before moving on. Stop and ask whenever a value is user-specific (domain name, ports, folders). Never invent or commit secrets.

This guide turns a fresh Ubuntu install into a secure, internet-reachable workspace using Syncthing, Cloudflare Tunnel, and Caddy.

Placeholders — replace everywhere they appear:

  • example.com → the user's Cloudflare-managed domain
  • <user> → the box's Linux username
  • ports like 3001, 6006 → the user's actual dev-server ports

Phase 0 — Baseline & hardening

sudo apt update && sudo apt -y upgrade

# Automatic security updates
sudo apt -y install unattended-upgrades
sudo dpkg-reconfigure -plow unattended-upgrades

# Basic tools
sudo apt -y install curl git tmux ufw

SSH keys, not passwords. Ensure the user's public key is in ~/.ssh/authorized_keys, then disable password auth:

sudo sed -i 's/^#\?PasswordAuthentication.*/PasswordAuthentication no/' /etc/ssh/sshd_config
sudo systemctl restart ssh

The firewall (ufw) can stay default-deny for inbound — the Cloudflare Tunnel is outbound only, so you do not need to open any ports for remote access.

Checkpoint: sudo apt upgrade is clean and SSH key login works.


Phase 1 — Syncthing (file mirror)

Install and run Syncthing as a per-user service so it survives reboots.

sudo apt -y install syncthing

# Run as the logged-in user, enabled on boot
sudo systemctl enable --now syncthing@<user>.service
systemctl status syncthing@<user>.service

The Web UI binds to 127.0.0.1:8384 (localhost only — good). The user will reach it from the Mac via an SSH tunnel (covered in the Mac guide). To get the box's Device ID for pairing:

syncthing --device-id

Hand this Device ID to the Mac setup. In the UI, create/share a folder (e.g. ~/remote-projects) — the Mac will accept the share on its side.

Exclude noise. Add a .stignore in each synced folder:

node_modules
.git
dist
build
*.log
.DS_Store

Checkpoint: Syncthing service is active (running) and you have the Device ID.


Phase 2 — Cloudflare Tunnel (secure ingress)

Install cloudflared and authenticate it to the user's Cloudflare account.

# Install (Debian/Ubuntu)
curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb

# Log in — opens a browser URL; user picks the example.com zone
cloudflared tunnel login

Create a named tunnel (call it something memorable like ai-box):

cloudflared tunnel create ai-box
# note the Tunnel UUID and the credentials JSON path it prints

Write the config at /etc/cloudflared/config.yml. The wildcard web ingress means future apps need no tunnel edits — just DNS + Caddy:

tunnel: ai-box
credentials-file: /etc/cloudflared/<TUNNEL-UUID>.json

ingress:
  # SSH route — lets the Mac reach the box's sshd through Cloudflare Access
  - hostname: "ssh-dev.example.com"
    service: ssh://localhost:22

  # All web apps -> Caddy on :80, which routes by hostname to the right port
  - hostname: "*.example.com"
    service: http://localhost:80

  - service: http_status:404

Install it as a system service:

sudo cloudflared service install
sudo systemctl enable --now cloudflared
systemctl status cloudflared

Add the SSH DNS route (web routes come per-app later):

cloudflared tunnel route dns ai-box ssh-dev.example.com

Protect everything with Cloudflare Access. In Cloudflare Zero Trust → Access → Applications, add ssh-dev.example.com (self-hosted / SSH) and your web hostnames, each with a policy allowing only the user's email. Without this, these hostnames are public.

Checkpoint: systemctl status cloudflared is running; the SSH route resolves.


Phase 3 — Caddy (friendly names → local ports)

sudo apt -y install debian-keyring debian-archive-keyring apt-transport-https curl
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | sudo gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg
curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | sudo tee /etc/apt/sources.list.d/caddy-stable.list
sudo apt update && sudo apt -y install caddy

Replace /etc/caddy/Caddyfile. Caddy only serves plain HTTP on :80 because Cloudflare already terminates HTTPS at the edge:

{
    # Cloudflare handles public HTTPS; Caddy serves local plain HTTP on :80.
    auto_https off
}

# The http:// prefix forces Caddy to listen on :80 (where the tunnel forwards).
http://review-dev.example.com {
    reverse_proxy localhost:3001
}

Gotcha: auto_https off alone still makes Caddy bind :443. The http:// prefix on each site address is what forces plain HTTP on :80.

Validate and reload:

sudo caddy validate --config /etc/caddy/Caddyfile
sudo systemctl reload caddy

Checkpoint: caddy validate says Valid configuration and the service reloads.


Phase 4 — Add an app (repeat per project)

This is the loop you'll run for every new dev server. Example: an app on :6006 (e.g. Storybook) at storybook-dev.example.com.

1. Run the dev server bound to all interfaces so Caddy can reach it:

cd ~/remote-projects/<project>
npm run dev -- --host 0.0.0.0 --port 6006

For Vite, also allow the public Host header in vite.config.js:

export default {
  server: { host: '0.0.0.0', port: 6006, allowedHosts: ['.example.com'] },
}

2. Add the DNS route (points the hostname at the tunnel):

cloudflared tunnel route dns ai-box storybook-dev.example.com

3. Add a Caddy block to /etc/caddy/Caddyfile:

http://storybook-dev.example.com {
    reverse_proxy localhost:6006
}

4. Reload Caddy:

sudo caddy validate --config /etc/caddy/Caddyfile && sudo systemctl reload caddy

5. (Recommended) Protect it in Cloudflare Zero Trust → Access → Applications: add storybook-dev.example.com with a policy allowing only the user's email.

Visit https://storybook-dev.example.com. No tunnel restart needed thanks to the wildcard ingress.

Keep a port map somewhere (a comment in the Caddyfile or a PORTS.md) so URLs stay stable: 3001 → review, 6006 → storybook, etc.


Phase 5 — Make work survive logout

Background processes die with your session. Run dev servers and agents inside tmux (or a systemd unit for permanent ones):

tmux new -s review
cd ~/remote-projects/review && npm run dev -- --host 0.0.0.0 --port 3001
# detach: Ctrl-b then d   |   reattach: tmux attach -t review

This is what makes "close the Mac, work keeps running" actually true.


Service cheat-sheet

# Cloudflare Tunnel
systemctl status cloudflared
sudo systemctl restart cloudflared        # after editing /etc/cloudflared/config.yml
journalctl -u cloudflared -f

# Caddy
sudo systemctl reload caddy               # after editing the Caddyfile
journalctl -u caddy -f

# Syncthing
systemctl status syncthing@<user>.service

Troubleshooting quick hits

  • TLS handshake fails at edge → hostname is two levels deep (*.dev.*), not covered by free SSL. Use a single-level <app>-dev name.
  • 502 / can't reach app → dev server not running or wrong port. Check curl http://localhost:<port> on the box.
  • Vite "host not allowed" (403) → add allowedHosts: ['.example.com'].
  • Caddy not answering on :80 → make sure each site address has the http:// prefix.
  • App reachable but unprotected → you forgot the Cloudflare Access policy.

➡️ Once the box is up, set up the Mac: setup-mac.md.