Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
5c9b3b5
refactor(server): make Ubuntu Server a profile of the unified Linux path
ulises-c Jun 17, 2026
510f486
fix(server): install optional low-tier packages under --optional
ulises-c Jun 17, 2026
94fd830
chore: drop orphaned docker-ce packages.json entry
ulises-c Jun 17, 2026
f1e40b1
refactor: hoist platform_install_tier to core as apt-family default
ulises-c Jun 17, 2026
8ff8a2c
feat(server): per-service HTTPS over Tailscale; Forgejo reference impl
ulises-c Jun 17, 2026
248a1a9
refactor(zsh): collapse server zsh config onto the shared dotfiles base
ulises-c Jun 17, 2026
47d38d8
feat(server): add build-essential and rust to server package set
ulises-c Jun 17, 2026
58d8783
docs(server): document Forgejo HTTPS rollout gotchas; fix stale SSH port
ulises-c Jun 17, 2026
b86f4f9
feat(server): run nvm/node and add shellcheck+pytest on the server pr…
ulises-c Jun 17, 2026
5244133
fix(server): point Forgejo homepage widget at its Tailscale HTTPS domain
ulises-c Jun 17, 2026
963aca0
fix(server): run rust_toolchain_step on the server profile
ulises-c Jun 17, 2026
2f5568d
fix(ssh): omit redundant Port 22 line; fix empty port in env path
ulises-c Jun 17, 2026
859dc6d
feat(server): convert Portainer to Tailscale HTTPS sidecar; enrich HT…
ulises-c Jun 17, 2026
27fb944
docs(server): mark Portainer sidecar live; document homepage env-relo…
ulises-c Jun 17, 2026
cbcfacf
feat(server): convert uptime-kuma to Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
2d1759c
docs(server): mark uptime-kuma sidecar live in TODO
ulises-c Jun 17, 2026
82070ee
feat(server): convert speedtest-tracker to Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
107240b
feat(server): convert ntfy to Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
691e37c
docs(server): mark uptime-kuma and speedtest-tracker sidecars live
ulises-c Jun 17, 2026
4ee65b6
docs(server): mark ntfy sidecar live; document config.js base_url red…
ulises-c Jun 17, 2026
99ed039
feat(server): convert filebrowser to Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
726322b
docs(server): mark filebrowser sidecar live
ulises-c Jun 17, 2026
27a5a1e
feat(server): convert syncthing to Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
d777c9a
docs(server): mark syncthing sidecar live
ulises-c Jun 17, 2026
5499273
feat(server): convert glances to Tailscale HTTPS sidecar (host-net va…
ulises-c Jun 17, 2026
a437ae8
docs(server): mark glances sidecar live
ulises-c Jun 17, 2026
1289036
feat(server): convert adguard to Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
783ddfb
docs(server): mark adguard sidecar live
ulises-c Jun 17, 2026
6527ffa
feat(server): front Cockpit with a Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
a0de32b
feat(server): migrate atvloadly into repo and convert to Tailscale HT…
ulises-c Jun 17, 2026
9313350
feat(server): front homepage with a Tailscale HTTPS sidecar (host-net…
ulises-c Jun 17, 2026
efdc05c
docs(server): mark cockpit sidecar live; document Origin-check non-issue
ulises-c Jun 17, 2026
155fb37
docs(server): mark homepage sidecar live
ulises-c Jun 17, 2026
f606654
docs(server): add Resilience / exit strategy section to HTTPS.md
ulises-c Jun 17, 2026
1f79243
docs(server): resolve NPM decision — keep as the non-tailnet HTTPS edge
ulises-c Jun 17, 2026
11c643e
feat(server): front tailscale web with a Tailscale HTTPS sidecar
ulises-c Jun 17, 2026
da0012a
feat(server): front NPM admin UI with a Tailscale sidecar; document u…
ulises-c Jun 17, 2026
bb22c1b
docs(server): mark NPM admin-UI sidecar live; complete the HTTPS rollout
ulises-c Jun 17, 2026
63c0158
feat(server): expose watchtower metrics API + sidecar for Uptime Kuma…
ulises-c Jun 17, 2026
c56b3da
docs(server): mark watchtower metrics sidecar live
ulises-c Jun 17, 2026
abe484a
docs(todo): add server observability & hardening improvement plan
ulises-c Jun 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,6 @@ linux-server/syncthing/config/
linux-server/uptime-kuma/data/

linux-server/forgejo/data/

# Tailscale sidecar node state (HTTPS.md) — holds node keys, never commit
linux-server/*/ts-state/
17 changes: 9 additions & 8 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,15 @@ tracked in TODO.md.)
invocations).
- `dotfiles/` — configs shared across platforms (`tmux.conf`,
`ghostty.config`, `zshrc.example`, `zsh_plugins.txt`); the engine deploys
them from here. The zshrc base is cross-platform — macOS-specific bits are
guarded on `/opt/homebrew` or `$OSTYPE` — and has an override system
(`deploy_zshrc`): a platform folder shipping its own `zshrc.example` wins
over the dotfiles base. Only `linux-server/` does (headless: no
Ghostty/fastfetch/notification hooks).
- `macOS/`, `linux-desktop/`, `linux-server/` — platform-specific configs
(server zshrc override, p10k), docs, and thin shim scripts that exec the
root entrypoints.
them from here. One zshrc base serves every platform including the headless
server — macOS-specific bits guard on `/opt/homebrew` or `$OSTYPE`, and the
desktop-only bits self-disable headless (notify hook no-ops without
`$DISPLAY`/`$WAYLAND_DISPLAY`, fastfetch keys off Ghostty or `$SSH_CONNECTION`,
version managers/zoxide are command-guarded). The override system
(`deploy_zshrc`) still lets a platform folder ship its own `zshrc.example` to
win over the base, but no platform currently does.
- `macOS/`, `linux-desktop/`, `linux-server/` — platform-specific configs,
docs, and thin shim scripts that exec the root entrypoints.
- `scripts/dryrun-smoke.sh` — runs `setup.sh --dry-run` for every platform and
asserts it exits clean with install actions; also run in CI.

Expand Down
44 changes: 44 additions & 0 deletions HANDOFF.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# Session Handoff

Scratchpad for in-progress work that spans more than one session — live
infrastructure state, decisions made outside of code, and what's next.
Branch-specific; delete or trim entries once they're fully landed and the
branch merges.

## 2026-06-17 — Forgejo HTTPS-over-Tailscale, live on ollie-server

Branch: `refactor/server-streamline-divergence` (PR [#47](https://github.com/ulises-c/Computer-Setup/pull/47))

`linux-server/HTTPS.md` (added in `8ff8a2c`, on a different machine) documents
per-service HTTPS via a Tailscale sidecar per service. This session executed
that plan for Forgejo on the actual server (`ollie-server`) and documented two
gotchas hit along the way.

**Code changes (committed, this session):**
- `linux-server/HTTPS.md` — resolved the auth-method decision (OAuth client +
`tag:container`, not a reusable key); documented the ACL-tag gotcha and a
network-namespace gotcha that will recur for every other service conversion
- `TODO.md` — added a tracked checklist for the 11 remaining services + 2 open
decisions (NPM retire/keep, Homepage sidecar vs. main-node)
- `SSH_and_GPG/create_ssh_key.sh` + `README.md` — the self-hosted-server SSH
port was hardcoded to `2222` (Forgejo's old host-published mapping); now
prompts with default `22`, matching the sidecar setup. Verified live with
`ssh-keyscan -p 22 forgejo.tail01d63b.ts.net`.

**Live state on `ollie-server` (not in git):**
- `linux-server/forgejo/.env`: `TS_AUTHKEY` set to the (now read+write-scoped)
`tailscale-proxy` OAuth client secret; `FORGEJO_DOMAIN=forgejo.tail01d63b.ts.net`
- `forgejo-ts` + `forgejo` containers running under the new sidecar
`docker-compose.yml`; old host-published ports (`3300`, `2222`) are gone
- Verified: `https://forgejo.tail01d63b.ts.net/` → `200`; SSH banner on `:22`
- Tailscale admin console changes (made by the user, not visible in this repo):
existing `tailscale-proxy` OAuth client elevated from read-only to
read+write scope; `tag:container` added to the tailnet ACL's `tagOwners`
(owner: `autogroup:admin`)

**Not done yet:**
- Existing git remotes pointing at the old `http://...:3300` / `ssh://...:2222`
Forgejo address still need updating to `git@forgejo.tail01d63b.ts.net:user/repo.git`
(on every machine that had a clone, not just this one)
- Forgejo Site Administration → confirm the app URL picked up the new `ROOT_URL`
- Continue the rollout: Portainer is next per the `TODO.md` table
4 changes: 2 additions & 2 deletions SSH_and_GPG/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Creates an Ed25519 SSH key for authenticating to a Git host (GitHub, GitLab, Bit
- Prompts for an email (used as the key comment), Git host, key filename, and optional passphrase
- Generates the key in `~/.ssh/` (skips generation if the key already exists, with an overwrite prompt)
- Adds the key to `ssh-agent` and updates `~/.ssh/config` with a `Host` block (idempotent)
- For self-hosted servers: writes an alias with `HostName`, `Port 2222`, and `User git` so you can clone as `git clone <alias>:<user>/<repo>.git`
- For self-hosted servers: prompts for an SSH port (default `22`) and writes an alias with `HostName`, `Port`, and `User git` so you can clone as `git clone <alias>:<user>/<repo>.git`
- Prints the public key for pasting into the Git host's settings
- Tests the SSH connection after you confirm the key has been added

Expand All @@ -44,7 +44,7 @@ bash create_ssh_key.sh
# or pre-fill inputs via env vars:
EMAIL="jane@example.com" GIT_HOST="github.com" KEY_NAME="github" bash create_ssh_key.sh
# Self-hosted Git server:
EMAIL="jane@example.com" IS_SELF_HOSTED=true GIT_HOSTNAME="hostname.ts.net" GIT_HOST="gitserver" bash create_ssh_key.sh
EMAIL="jane@example.com" IS_SELF_HOSTED=true GIT_HOSTNAME="hostname.ts.net" GIT_HOST="gitserver" GIT_SSH_PORT=22 bash create_ssh_key.sh
```

---
Expand Down
10 changes: 8 additions & 2 deletions SSH_and_GPG/create_ssh_key.sh
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ prompt EMAIL "Email (comment in key)"
# ---- Git host selection ----
IS_SELF_HOSTED="${IS_SELF_HOSTED:-}"
GIT_HOSTNAME="${GIT_HOSTNAME:-}"
GIT_SSH_PORT="${GIT_SSH_PORT:-}"

if [[ -z "$GIT_HOST" ]]; then
echo ""
Expand Down Expand Up @@ -59,6 +60,10 @@ if [[ -z "$GIT_HOST" ]]; then
read -r -p "SSH alias for this server [gitserver]: " GIT_HOST
GIT_HOST="${GIT_HOST:-gitserver}"
fi
if [[ -z "$GIT_SSH_PORT" ]]; then
read -r -p "SSH port [22]: " GIT_SSH_PORT
GIT_SSH_PORT="${GIT_SSH_PORT:-22}"
fi
;;
0)
read -r -p "Git host: " GIT_HOST
Expand Down Expand Up @@ -156,7 +161,8 @@ if [[ -n "$IS_SELF_HOSTED" ]]; then
echo ""
echo "Host $GIT_HOST"
echo " HostName $GIT_HOSTNAME"
echo " Port 2222"
# Port 22 is the SSH default — only emit the line for a non-standard port.
[[ -n "$GIT_SSH_PORT" && "$GIT_SSH_PORT" != "22" ]] && echo " Port $GIT_SSH_PORT"
echo " User git"
echo " AddKeysToAgent yes"
echo " IdentityFile $KEY_PATH"
Expand Down Expand Up @@ -186,7 +192,7 @@ read -r -p "Press [Enter] after adding the public key to your $GIT_HOST account.
# -T avoids trying to open a shell; -v optional for debugging.
echo ""
if [[ -n "$IS_SELF_HOSTED" ]]; then
echo "Testing SSH connection to self-hosted server ($GIT_HOSTNAME:2222) ..."
echo "Testing SSH connection to self-hosted server ($GIT_HOSTNAME:${GIT_SSH_PORT:-22}) ..."
ssh -T "$GIT_HOST" || true
else
echo "Testing SSH connection to git@$GIT_HOST ..."
Expand Down
122 changes: 122 additions & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,128 @@ Test and validate the linux-desktop setup on the personal CachyOS desktop
- [ ] Test `--personal` flag end-to-end
- [ ] Create PR for CachyOS support

## Per-service HTTPS rollout (linux-server)

Convert each self-hosted service from `http://<server-ip>:<port>` to its own
`https://<svc>.<tailnet>.ts.net/` via a Tailscale sidecar. Pattern, prereqs,
and full rollout table in [linux-server/HTTPS.md](linux-server/HTTPS.md).

- [x] forgejo — reference impl (sidecar + SSH :22), done in 8ff8a2c
- [x] portainer — sidecar live at https://portainer.<tailnet>.ts.net/, homepage
link updated
- [x] uptime-kuma — sidecar live at https://uptime-kuma.<tailnet>.ts.net/,
homepage link & widget url updated
- [x] speedtest-tracker — sidecar live at https://speedtest.<tailnet>.ts.net/,
homepage link & widget url updated
- [x] ntfy — sidecar live at https://ntfy.<tailnet>.ts.net/, homepage link
updated
- [x] filebrowser — sidecar live at https://filebrowser.<tailnet>.ts.net/,
homepage link updated
- [x] syncthing — sidecar live at https://syncthing.<tailnet>.ts.net/, homepage
link & widget url updated; sync `:22000`/`:21027` confirmed published
on the sidecar
- [x] glances — sidecar live at https://glances.<tailnet>.ts.net/ (proxies via
`host.docker.internal`, glances untouched), homepage link updated
- [x] adguard — sidecar live at https://adguard.<tailnet>.ts.net/, homepage
link & widget url updated; DNS `:53` confirmed still resolving on the
sidecar after recreate
- [x] atvloadly — was never tracked in this repo (lived in an untracked
`/home/ollie/docker-compose.yml`); migrated into
`linux-server/atvloadly/`, sidecar live at
https://atvloadly.<tailnet>.ts.net/, homepage link updated. Apple TV
discovery (avahi/dbus socket mounts) confirmed unaffected by the netns
share — no `hostname:` on the app container, that conflicts with
`network_mode: service:...`
- [x] nginx-proxy-manager — KEPT as the host edge; admin UI sidecar live at
https://npm.<tailnet>.ts.net/ (proxies host :81 via host.docker.internal,
no app-side change needed), homepage link updated. NPM keeps binding
host :80/:443 unchanged
- [x] homepage — sidecar live at https://homepage.<tailnet>.ts.net/, confirmed
`HOMEPAGE_ALLOWED_HOSTS` includes the new domain and container is
healthy post-recreate
- [x] cockpit — sidecar live at https://cockpit.<tailnet>.ts.net/, homepage
link updated. `cockpit.conf.example`'s Origins allow-list turned out
unnecessary — verified via raw WebSocket upgrade (101 with matching
Origin, 403 with a foreign one)
- [x] tailscale-web — not in the original rollout; added because the homepage
Tailscale tile linked plain HTTP. New `linux-server/tailscale-web/`
sidecar-only stack live at https://tailscale-web.<tailnet>.ts.net/,
homepage link updated. Host's `tailscale-web.service` unit now runs
`tailscale web --listen 0.0.0.0:8088 --origin
https://tailscale-web.<tailnet>.ts.net`; sidecar proxies to
`host.docker.internal:8088`. Verified 200 with no redirect, real page
content, confirmed in browser. Don't use port `:5252` — see HTTPS.md
Gotchas
- [x] watchtower — sidecar live at https://watchtower.<tailnet>.ts.net/v1/metrics
(no UI, metrics-only API; confirmed 401 unauthenticated / 200 with the
Bearer token, and the 3am schedule is unaffected). Still need: the
Uptime Kuma HTTP monitor pointed at it (recipe in HTTPS.md).
- [x] Decide auth method: OAuth client + `tag:container` (reusing the elevated
tailscale-proxy client) — resolved during the Forgejo rollout, see HTTPS.md
- [x] Decide whether to retire NPM or keep it — KEEP, as the non-tailnet HTTPS
edge (trusted certs for LAN/public clients like a Plex TV that can't join
the tailnet). See HTTPS.md → "NPM — trusted HTTPS for non-tailnet clients"
- [ ] Set up the NPM trusted-HTTPS edge (domain `ulises-c.me`, already owned):
NPM wildcard Let's Encrypt cert for `*.home.ulises-c.me` via DNS-01, AdGuard
rewrite `*.home.ulises-c.me` → LAN IP, then per-service proxy hosts. Not
started — documented in HTTPS.md to pick up later.
- [ ] Update Homepage hrefs to HTTPS as each service converts; a service's widget
`url:` must move to the HTTPS domain too (localhost stops resolving once the
host port is dropped)

## Server observability & hardening (post-HTTPS rollout)

Improvements identified once every service was wired up with a Tailscale sidecar.

### Watchtower observability — "what updated, and when"

Watchtower has no native history UI, and its `/v1/metrics` endpoint (now monitored
by Uptime Kuma) is only cumulative **counters** (`watchtower_containers_updated` /
`_failed` / `_scanned`, `watchtower_scans_total`) — no container names or image
versions. So the "what was actually updated" has to come from notifications or
logs, not metrics. Build it up in layers:

- [ ] **Tier 1 — ntfy notifications (quick win, reuses the existing ntfy).** On the
watchtower service set `WATCHTOWER_NOTIFICATION_URL` to a shoutrrr ntfy URL
pointing at our ntfy instance (dedicated topic, e.g. `watchtower`) and
`WATCHTOWER_NOTIFICATION_REPORT=true` for a per-run report (which containers
updated/failed/skipped, old→new image). Gives a timestamped, persistent
history in ntfy + a phone push — directly answers "what & when." Lowest effort.
- [ ] **Tier 2 — Prometheus + Grafana on the existing `/v1/metrics`.** Scrape the
counters, dashboard the update/scan trend, alert on
`watchtower_containers_failed > 0`. Counts only (no names) — pairs with Tier 1
for the "what." Heavier (new stack); also becomes the home for other metrics
(glances, node-exporter, cAdvisor).
- [ ] **Tier 3 (optional) — dedicated update tracker with a UI.** Evaluate What's Up
Docker (WUD) or Diun, which show per-container available/applied updates in a
UI. Could complement or take over watchtower's notification role.

### Broader improvements (from the post-rollout review)

- [ ] **Pin the Tailscale sidecar image.** All ~13 sidecars run
`tailscale/tailscale:latest` and watchtower auto-updates them — a bad release
could drop every HTTPS front door at once. Pin a stable tag (bump
deliberately) or exclude the sidecars from watchtower. Cheap, high-value.
- [ ] **Backups.** Service data volumes (Forgejo repos, AdGuard/uptime-kuma/etc.),
all `.env` files, and `ts-state/` node keys live only on the server and are
gitignored — no backup today. Document what to back up + a restic/borg or
Syncthing job.
- [ ] **DRY the sidecar boilerplate.** ~13 near-identical `<svc>-ts` blocks +
`ts-serve.json` (differ only by hostname/port). Use Compose `extends` from a
shared base so a global change (the image pin above, `TS_EXTRA_ARGS`) is one
edit, not 13. Medium effort — touches all stacks, needs live re-verify.
- [ ] **One shared `TS_AUTHKEY`.** The same OAuth secret is copied into ~13 `.env`
files; rotation/rebuild means editing all of them. Share one env file.
- [ ] **Validation script for the server stacks** (CI, like `dryrun-smoke.sh`):
assert every `linux-server/*/` has matching compose + `ts-serve.json` +
`.env.example`, valid YAML/JSON, serve port == container port, `ts-state/`
gitignored. Catches the drift that bit us mid-rollout (wrong port, stale config).
- [ ] **Tighten the Tailscale ACL** — least-privilege for the `tag:container` nodes
(currently default allow-all).
- [ ] **Forward-auth for the NPM public edge** (Authelia/Authentik) — bundle with the
`*.home.ulises-c.me` NPM setup, since services like filebrowser/glances have
weak/no auth once exposed off-tailnet.

## linux-server — Raspberry Pi 4

Set up the Raspberry Pi 4 headless server config under `linux-server/`.
Expand Down
14 changes: 10 additions & 4 deletions dotfiles/zshrc.example
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
# ═══════════════════════════════════════════════════════════════
# ~/.zshrc — shared zsh config (macOS / Ubuntu / Arch)
# ═══════════════════════════════════════════════════════════════
# Base zshrc deployed by setup.sh; a platform folder can ship its own
# zshrc.example to override it (linux-server does — headless, no Ghostty).
# Single zshrc deployed by setup.sh on every platform (macOS, Linux desktop,
# and the headless server). Desktop-only bits self-disable headless: the
# notify hook no-ops without a $DISPLAY/$WAYLAND_DISPLAY, fastfetch keys off
# Ghostty or an SSH session, and version managers/zoxide are command-guarded.
# A platform folder may still ship its own zshrc.example to override this base.
# macOS-specific bits are guarded on /opt/homebrew (Apple Silicon) or $OSTYPE.
# Requires: zsh, antidote, zoxide, fastfetch, fzf, bat, fd, eza
# Plugins managed by antidote — see ~/.zsh_plugins.txt
Expand Down Expand Up @@ -299,8 +302,11 @@ precmd_functions=(_notify_precmd $precmd_functions)


# ═══════════════════════════════════════════════════════════════
# SYSINFO DISPLAY (Ghostty startup)
# SYSINFO DISPLAY (Ghostty startup, or any SSH login)
# ═══════════════════════════════════════════════════════════════
[[ -o interactive && "$TERM_PROGRAM" == "ghostty" ]] && fastfetch
# Ghostty on the desktop/laptop; SSH covers the headless server, where
# there is no TERM_PROGRAM but every login is a remote session.
[[ -o interactive && ( "$TERM_PROGRAM" == "ghostty" || -n "$SSH_CONNECTION" ) ]] && \
command -v fastfetch &>/dev/null && fastfetch

[[ -f ~/.p10k.zsh ]] && source ~/.p10k.zsh
Loading
Loading