Skip to content
KH.Portfolio
All notes

Online Notes

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 / appWhat to do
iPad/iPhone — TailscaleConnect to your private tailnet
iPad/iPhone — TermiusSSH to the Mac and attach to tmux sessions
iPad — SafariOpen the private Tailscale frontend URL to preview the site
iPad — Safari / code-serverOpen the private editor URL (example: :8444) to review diffs
MacBook at homeKeep 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:

  1. General → Sharing → Remote Login: On; limit access to your account.
  2. Battery / Options: Prevent automatic system sleep on power adapter if available. Display sleep is fine; lid closure may still cause system sleep.
  3. 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:

  1. Open Keychain → Keys → Generate Key.
  2. Choose ED25519, name it (e.g., MacBook Remote), and preferably add a passphrase.
  3. Copy the entire public key: ssh-ed25519 AAAA... Generated by Termius (the trailing comment is optional).
  4. 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:

FieldValue
AddressMac's Tailscale IP (100.x.x.x) or tailnet DNS name
Port22
UsernameOutput of whoami
AuthenticationYour 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

ProblemWhat to check
Can't SSH from iPadTailscale on both devices; Mac power/sleep; Remote Login; Termius address/key
SSH key rejectedUsername; ~/.ssh permissions; public key in authorized_keys
tmux session missingtmux ls; recreate with tmux new -A -s NAME
Frontend preview unavailableFrontend running on expected port; Tailscale Serve status; iPad Tailscale
Backend API failsBackend running; browser API URL; CORS; cookies; .env
code-server unavailableHomebrew service; local port 8080; Tailscale Serve; config
Codex extension freezesBrowser extension compatibility; use Codex CLI fallback
Nothing starts after rebootMac 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.

Official references