Remote Coding Setup & Recovery Handbook
Devices: M1 MacBook (home server), iPad/iPhone (remote client)
Tools: Tailscale, Termius, SSH, tmux, Codex, Git, code-server, Next.js/NestJS
Rule: Unless marked otherwise, terminal commands run on the Mac, either directly or through Termius on the iPad.
Replace example usernames, repository paths, hostnames, and ports with your own. Never save SSH private keys, passwords, or API secrets in these notes.
1. Daily quick reference
| Device / app | What to do |
|---|---|
| iPad/iPhone — Tailscale | Connect to your private tailnet |
| iPad/iPhone — Termius | SSH to the Mac and attach to tmux sessions |
| iPad — Safari | Open the private Tailscale frontend URL to preview the site |
| iPad — Safari / code-server | Open the private editor URL (example: :8444) to review diffs |
| MacBook at home | Keep on power, ventilated, awake, and logged in as required |
# iPad → Termius → Mac
tmux ls
tmux new -A -s frontend
tmux new -A -s backend
tmux new -A -s codex
tailscale serve status
brew services list
tmux shortcuts: Ctrl+B, then D = detach; Ctrl+B, then C = new window; Ctrl+B, then N = next window. tmux survives SSH disconnects, not reboots.
2. Initial MacBook preparation
On MacBook — System Settings:
- General → Sharing → Remote Login: On; limit access to your account.
- Battery / Options: Prevent automatic system sleep on power adapter if available. Display sleep is fine; lid closure may still cause system sleep.
- Keep Tailscale signed in. Avoid forwarding SSH ports on your router.
On MacBook — Terminal (or iPad → Termius):
whoami # macOS username
tailscale ip -4 # Mac's private tailnet IP
tailscale status # connected devices
sudo systemsetup -getremotelogin # Remote Login status
pmset -g custom # sleep / power settings
3. SSH key authentication with Termius
On iPad/iPhone — Termius:
- Open Keychain → Keys → Generate Key.
- Choose ED25519, name it (e.g.,
MacBook Remote), and preferably add a passphrase. - Copy the entire public key:
ssh-ed25519 AAAA... Generated by Termius(the trailing comment is optional). - Never copy or share the private key.
On MacBook — Terminal:
mkdir -p ~/.ssh
chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys
# Paste the public key on ONE line. Save: Ctrl+O, Enter; exit: Ctrl+X.
chmod 600 ~/.ssh/authorized_keys
On MacBook — verify server host fingerprint:
ls -la /etc/ssh/
ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
If the host-key file is missing, first confirm Remote Login is enabled. If genuinely missing, sudo ssh-keygen -A creates missing SSH host keys without overwriting existing ones. Compare the ED25519 SHA256 host fingerprint on the Mac against the fingerprint displayed by Termius; don't accept a mismatch.
On iPad/iPhone — Termius → Hosts → New Host:
| Field | Value |
|---|---|
| Address | Mac's Tailscale IP (100.x.x.x) or tailnet DNS name |
| Port | 22 |
| Username | Output of whoami |
| Authentication | Your generated Termius ED25519 key |
Connect with Tailscale active, verify the fingerprint, and test whoami, pwd, and ls. Don't disable password-based SSH until key authentication is confirmed.
4. Persistent terminals and Codex
On MacBook or iPad → Termius:
brew install tmux lazygit
tmux -V
which codex
which lazygit
If Codex CLI is missing, follow the current official Codex installation instructions and sign in.
On iPad → Termius: Open separate tabs to the same Mac, one for each tmux session:
tmux new -A -s frontend
tmux new -A -s backend
tmux new -A -s codex
Inside each session, cd to the relevant repository and run its server or codex. Attaching to the same tmux session in two tabs mirrors that session; use different names for independent work.
5. Two repositories: frontend + backend
On iPad → Termius → frontend tmux session:
cd ~/Documents/code/munimuni-resort-admin
npm install # only if dependencies are missing
npm run dev # example: localhost:3000
On iPad → Termius → backend tmux session:
cd ~/Documents/code/munimuni_resort_backend
npm install # only if dependencies are missing
npm run start:dev # example: localhost:4000
The paths are examples; check your actual repository names and package.json scripts. The backend may need its database, migrations, and environment variables.
6. Private live website preview (Tailscale Serve)
On iPad → Termius → Mac (separate session):
tailscale serve --bg --https=443 3000 # frontend
tailscale serve --bg --https=8443 4000 # backend
tailscale serve status
On iPad → Safari: Connect Tailscale and open the frontend HTTPS URL printed by tailscale serve status. The backend can be accessed via the same hostname on port 8443 when needed.
Frontend API connectivity
Important: In iPad Safari, localhost means the iPad, not the Mac. For browser-side API calls, use the backend tailnet HTTPS address or a same-origin frontend proxy.
On MacBook — frontend .env.local (example only):
NEXT_PUBLIC_API_URL=https://YOUR-MAC.YOUR-TAILNET.ts.net:8443
Restart the frontend after changing environment variables. When using a separate backend origin, configure CORS for the frontend HTTPS origin and verify cookie SameSite, Secure, and credential settings. Don't commit secrets or local .env files.
Troubleshooting (iPad → Termius):
curl -I http://127.0.0.1:3000
curl -I http://127.0.0.1:4000
tailscale serve status
lsof -nP -iTCP:3000 -sTCP:LISTEN
lsof -nP -iTCP:4000 -sTCP:LISTEN
If the page loads but API requests fail, inspect API URL, backend health, CORS, and authentication cookies. Hot reload may occasionally need a Safari refresh.
7. code-server: visual editing and Git review
On iPad → Termius → Mac:
brew install code-server
brew services start code-server
brew services list
curl -I http://127.0.0.1:8080
Change code-server password (on Mac via Termius):
nano ~/.config/code-server/config.yaml
Typical configuration (replace password locally; never publish it):
bind-addr: 127.0.0.1:8080
auth: password
password: "YOUR_UNIQUE_STRONG_PASSWORD"
cert: false
chmod 600 ~/.config/code-server/config.yaml
brew services restart code-server
Expose editor privately (on Mac via Termius):
tailscale serve --bg --https=8444 8080
tailscale serve status
On iPad → Safari: Open the tailnet HTTPS editor URL ending in :8444, sign in, then File → Open Folder. To work with both repos, use File → Add Folder to Workspace and save a multi-root workspace.
Extensions (on Mac via Termius):
code-server --install-extension esbenp.prettier-vscode
code-server --install-extension dbaeumer.vscode-eslint
code-server --install-extension openai.chatgpt
code-server --list-extensions --show-versions
Codex extension caution: Installation does not guarantee browser/code-server compatibility. Test that the sidebar opens and responds. If not, run Codex CLI in Termius and use code-server for editing and diffs. Some extensions may be missing from Open VSX.
8. Git branches and reviewing changes
On iPad → Termius → project repository:
git status
git branch --show-current
git switch -c feature/my-change
git switch main
git diff # unstaged changes
git diff --cached # staged changes
git diff --stat # summary
git diff main...HEAD # feature changes since divergence from main
git log main..HEAD --oneline
lazygit
On iPad → Safari → code-server: Open Source Control to inspect uncommitted changes; use GitLens or terminal Git for comparisons against main.
Switching branches changes files in the same working directory used by Codex and the dev server. To run two branches at once, use separate worktrees and ports:
git worktree add ../feature-preview -b feature/my-change
git worktree list
9. Automatic startup after macOS login
Optional: This is a LaunchAgent, which runs after your Mac user logs in—not necessarily immediately after power-on. FileVault may require someone to unlock the Mac locally. Test while physically near the Mac. These jobs create tmux sessions, but do not supervise server crashes or restart Codex.
On MacBook — Terminal (or iPad → Termius):
mkdir -p ~/scripts
nano ~/scripts/start-dev.sh
Paste (adjust repo paths and commands):
#!/bin/zsh
export PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:$PATH"
sleep 10
tmux has-session -t frontend 2>/dev/null || \
tmux new-session -d -s frontend \
-c "$HOME/Documents/code/munimuni-resort-admin" \
"npm run dev; exec zsh"
tmux has-session -t backend 2>/dev/null || \
tmux new-session -d -s backend \
-c "$HOME/Documents/code/munimuni_resort_backend" \
"npm run start:dev; exec zsh"
chmod +x ~/scripts/start-dev.sh
~/scripts/start-dev.sh
tmux ls
Create the user LaunchAgent (Mac or iPad → Termius):
mkdir -p ~/Library/LaunchAgents
nano ~/Library/LaunchAgents/com.remote-dev.startup.plist
Paste, replacing YOUR_USERNAME with whoami:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
"http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.remote-dev.startup</string>
<key>ProgramArguments</key>
<array>
<string>/bin/zsh</string>
<string>/Users/YOUR_USERNAME/scripts/start-dev.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/remote-dev.log</string>
<key>StandardErrorPath</key>
<string>/tmp/remote-dev-error.log</string>
</dict>
</plist>
Validate and load (only if not already loaded):
plutil -lint ~/Library/LaunchAgents/com.remote-dev.startup.plist
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.remote-dev.startup.plist
launchctl print gui/$(id -u)/com.remote-dev.startup
cat /tmp/remote-dev-error.log
If the LaunchAgent is already loaded, do not bootstrap it again. Inspect it with launchctl print first. The script only starts sessions that don't already exist.
10. Recovery and troubleshooting
| Problem | What to check |
|---|---|
| Can't SSH from iPad | Tailscale on both devices; Mac power/sleep; Remote Login; Termius address/key |
| SSH key rejected | Username; ~/.ssh permissions; public key in authorized_keys |
| tmux session missing | tmux ls; recreate with tmux new -A -s NAME |
| Frontend preview unavailable | Frontend running on expected port; Tailscale Serve status; iPad Tailscale |
| Backend API fails | Backend running; browser API URL; CORS; cookies; .env |
| code-server unavailable | Homebrew service; local port 8080; Tailscale Serve; config |
| Codex extension freezes | Browser extension compatibility; use Codex CLI fallback |
| Nothing starts after reboot | Mac logged in; Tailscale connected; LaunchAgent and logs |
Useful inspection commands (on Mac or iPad → Termius):
tmux ls
tailscale status
tailscale serve status
brew services list
launchctl print gui/$(id -u)/com.remote-dev.startup
cat /tmp/remote-dev.log
cat /tmp/remote-dev-error.log
ps aux | grep -E 'node|code-server|tmux'
Warning: tailscale serve reset removes all Serve configurations on that Mac. Don't use it casually when frontend, backend, and editor routes coexist.
11. Final verification checklist
- Mac stays awake on power and has adequate ventilation.
- Tailscale reconnects after network changes.
- Termius SSH key authentication works and the host fingerprint was verified.
- Frontend and backend tmux sessions survive Termius disconnections.
- Codex CLI runs from the intended repository.
- Frontend live preview opens on iPad Safari.
- Frontend API requests work with the backend remotely.
- code-server opens on iPad and Git diff review works.
- Codex IDE extension responds, or Codex CLI fallback is available.
- After a test reboot and login, expected services return.
Security: Keep services private to Tailscale, use unique passwords and SSH keys, avoid exposing development ports publicly, use Git for backups, and don't expose production credentials in dev tools.