docs(setup): add Debian 13 dev-VM setup procedure + scripts + devcontainer #220

Merged
julien merged 1 commits from docs/dev-debian-vm-setup into main 2026-05-24 18:40:09 +02:00
Owner

Summary

Adds a full Debian 13 dev-VM setup procedure (docs/setup/01-dev-debian-vm-setup.md) + 10 modular idempotent setup scripts + a systemd template + a .devcontainer/ spec, in preparation for the new dev VM (10.100.201.21) replacing the WSL-based workflow. Both IDE flows (VSCode Remote-SSH + Devcontainer) and both Node toolchains (nvm on host + devcontainer image) are available — devs pick per task.

Adjacent context (does not ship here, planned follow-up):

  • Preview infra on the GitLab VM (10.100.201.10) — same dev.compose.yml, deployed by CI on main. Doc placeholder in §8.6.
  • GitLab Runner migration (act_runner Gitea → GitLab Runner Docker executor) — bundled with the Gitea → GitLab cutover.
  • Private dotfiles repo (apf/dotfiles) — ~/.zshrc, ~/.p10k.zsh, ~/.tmux.conf versioned. 80-dotfiles.sh is already structured to fall back to a ~/.dotfiles/ clone when present.

No application-code changes. No CI gate impact (doc + scripts + devcontainer spec only).

What lands

Path Change
docs/setup/01-dev-debian-vm-setup.md New. Step-by-step doc: workstation prep (SSH agent + VSCode Remote-SSH + fonts), bootstrap orchestrator, per-script effects, project clone, infra boot, IDE flow A / B / C, apf-ai-service .NET appendix, troubleshooting.
docs/setup/02-wsl-terminal-setup.md Renamed from 01-wsl-terminal-setup.md. No content change.
docs/setup/03-dev-web-stack.md Renamed from 02-dev-web-stack.md. No content change.
docs/setup/04-angular-nx-monorepo.md Renamed from 03-angular-nx-monorepo.md. No content change.
docs/setup/README.md New. Index of the docs/setup/ folder.
docs/setup/scripts/lib.sh New. Shared helpers — colour-coded log/ok/warn/err/skip, apt_install skipping already-installed, ensure_line idempotent append, confirm prompt.
docs/setup/scripts/bootstrap.sh New. Orchestrator running scripts 10..80 in order with confirmation prompts.
docs/setup/scripts/10-base-packages.sh New. apt update + base packages (curl, wget, git, build-essential, …).
docs/setup/scripts/20-zsh.sh New. zsh + Oh My Zsh (RUNZSH=no, no shell hijack) + Powerlevel10k + zsh-autosuggestions + zsh-syntax-highlighting. Patches ~/.zshrc (theme, plugins, fzf hook).
docs/setup/scripts/30-cli-tools.sh New. bat eza fd-find ripgrep fzf zoxide ncdu keychain + jq yq httpie make tree htop tmux direnv dnsutils unzip rsync. Symlinks Debian-renamed binaries (batcatbat, fdfindfd) into ~/.local/bin so notes/aliases.zsh works as-is.
docs/setup/scripts/40-node.sh New. nvm v0.40.1 + Node from .nvmrc (currently 24) + corepack enable + pnpm warmed from package.json#packageManager.
docs/setup/scripts/50-docker.sh New. Docker CE + compose plugin from docker.com apt repo, user added to docker group, docker.service enabled at boot. Pinned GPG key + repo line for Debian Trixie.
docs/setup/scripts/60-tuning.sh New. fs.inotify.max_user_watches=524288 (Vite/Nx watch ceiling), optional 4 GB swapfile, optional hostname rename (only prompts on generic hostnames like debian13).
docs/setup/scripts/70-hardening.sh New. Best-effort UFW (allow OpenSSH only) + unattended-upgrades on security channel + fail2ban + sshd drop-in (PermitRootLogin no, PasswordAuthentication no, AllowAgentForwarding yes). Probes before applying, validates sshd -t before reloading, skips cleanly if infra already locked the box down.
docs/setup/scripts/80-dotfiles.sh New. Symlinks notes/aliases.zsh~/.oh-my-zsh/custom/aliases.zsh (backs up an existing target). Copies notes/gitconfig.txt to ~/.gitconfig, prompts for identity, applies via git config --global user.name/email.
docs/setup/systemd/apf-portal-infra@.service New. Template systemd unit auto-starting ./infra/local/dev.sh up at boot. Install: enable apf-portal-infra@$USER.service.
.devcontainer/devcontainer.json New. VSCode Dev Container spec on mcr.microsoft.com/devcontainers/typescript-node:1-24-bookworm. docker-outside-of-docker feature + --network=apf-portal-dev so DNS to postgres/redis/otel-collector works. initializeCommand fails fast if dev.sh up hasn't been run yet. Forwarded ports labelled. Six dev extensions pre-installed.
.devcontainer/post-create.sh New. corepack enable && pnpm install --frozen-lockfile.
CLAUDE.md "Environment conventions" rewritten: documents the two envs (local / development) + hybrid sub-mode + the two IDE flows (Remote-SSH / Devcontainer), points at the new VM setup doc and the legacy WSL doc.

Key choices

  • Idempotent scripts, individually runnable. Each script probes before doing anything (apt package already installed? plugin already cloned? UFW already active? sshd drop-in already present?). Bootstrap is the orchestrator; each script also runs standalone. Re-running after a partial setup is safe and reports ↪ skip for the no-op cases.
  • No private key on the VM — SSH agent forwarding instead. ~/.ssh/config on the workstation carries ForwardAgent yes; the VM never holds long-lived secrets. Same future pattern for GPG signing (covered in §8.5 as appendix). keychain is installed by 30-cli-tools.sh as a fallback for scenarios where agent forwarding is not available (CI runners, scripts).
  • Hardening is "best-effort, probe-first". Some infra teams ship pre-hardened VMs; this script doesn't fight that. UFW already active? Print rules and skip. unattended-upgrades already on? Skip. SSH already locked down? Skip. Each section validates before reloading so a misconfig can't take SSH offline.
  • Devcontainer assumes infra-on-host. The container runs on the VM but talks to postgres / redis / otel on the same VM's host docker daemon through the shared apf-portal-dev Compose network. initializeCommand fails fast with a clear message if ./infra/local/dev.sh up hasn't been run yet — better than puzzling ECONNREFUSED errors at runtime.
  • 60-tuning.sh raises inotify to 524288. Default Debian limit is 8K; Vite/Nx in this monorepo blow past that. The setting is persisted in /etc/sysctl.d/99-apf-portal.conf so it survives reboot.
  • Hybrid mode (workstation IDE + VM infra) is a documented sub-mode. SSH LocalForward directives on 5432/6379/4317/4318 expose the VM's infra services as localhost:* on the workstation. Latency cost: 5-15 ms per query, fine for daily work; for long-running flows, wrap in tmux or use autossh.

Notes for the reviewer

  • Renaming the existing setup docs (01→02, 02→03, 03→04) is the only "destructive" change. git log --follow still works because of git mv. Diff shows up as renames, not delete-and-add.
  • The brief asked for bat eza fd-find ripgrep fzf zoxide docker keychain ncdu git. git is installed by 10-base-packages.sh (every other script needs it). Everything else lives in 30-cli-tools.sh + 50-docker.sh. The "fullstack-dev extras" (jq yq httpie make tree htop tmux direnv dnsutils unzip rsync) are additions I proposed in the lock-in question and that you greenlit (full scope) — easy to trim if any of them turn out to be unwanted.
  • Both Node toolchains in parallel — nvm on the VM (via 40-node.sh) and devcontainer in the repo. Devs can use either; both read the same .nvmrc + packageManager pin so the version stays consistent.
  • The systemd unit is a TEMPLATE (apf-portal-infra@.service) — install once, enable per-user (enable apf-portal-infra@$USER.service). This is the right shape for a shared VM with multiple devs eventually, even if today only one user uses it.
  • No PR-body Co-Authored-By trailer, no Generated-with-Claude footer, per the project rule.

Test plan

Manual (no automated test exists for this kind of setup work):

  • On a fresh Debian 13 VM: git clone …, ./docs/setup/scripts/bootstrap.sh, answer prompts, end up with zsh + Powerlevel10k + all the requested CLI tools + Node 24 + pnpm 10.33.4 + Docker on PATH.
  • ./infra/local/dev.sh up boots successfully against the VM's local docker daemon.
  • pnpm install + pnpm exec nx run-many -t lint test --parallel=3 passes on the VM.
  • VSCode Remote-SSH from a workstation: connect, open ~/Works/apf_portal, run pnpm exec nx serve portal-bff, confirm reaches postgres:5432.
  • VSCode Dev Containers from the same workstation: Reopen in Container, image builds, postCreateCommand runs pnpm install, dev server reaches postgres through the apf-portal-dev network.
  • Hybrid mode: SSH tunnel from workstation, pnpm exec nx serve portal-bff locally, confirm postgres reachable via localhost:5432.
  • pnpm exec prettier --check clean on the touched markdown files.
  • Scripts pass bash -n (syntax check) — verified during writing.

What's next

  • Validate by walking through this doc on the actual VM 10.100.201.21. Any friction surfaced becomes a follow-up PR (docs(setup): ...).
  • Once the dev VM is operational, return to ADR-0027 Implementation PR 1 (Region / Delegation / Structure Prisma schema + inline reference-data migration) — paused since the start of this PR.
  • Set up the private dotfiles repo (apf/dotfiles) as a small follow-up, then teach 80-dotfiles.sh to prefer the dotfiles repo over notes/.
  • When migrating to GitLab: PR pair — (a) git remote set-url doc updates here, (b) infra/gitlab-runners/ replacing infra/ci-runners.compose.yml.
  • Future: deploy the shared preview infra on vm-gitlab (10.100.201.10) — CI-driven, separate PR.
## Summary Adds a full Debian 13 dev-VM setup procedure ([docs/setup/01-dev-debian-vm-setup.md](docs/setup/01-dev-debian-vm-setup.md)) + 10 modular idempotent setup scripts + a systemd template + a `.devcontainer/` spec, in preparation for the new dev VM (`10.100.201.21`) replacing the WSL-based workflow. Both IDE flows (VSCode Remote-SSH + Devcontainer) and both Node toolchains (nvm on host + devcontainer image) are available — devs pick per task. Adjacent context (does not ship here, planned follow-up): - **Preview infra on the GitLab VM (`10.100.201.10`)** — same `dev.compose.yml`, deployed by CI on `main`. Doc placeholder in §8.6. - **GitLab Runner migration** (act_runner Gitea → GitLab Runner Docker executor) — bundled with the Gitea → GitLab cutover. - **Private dotfiles repo** (`apf/dotfiles`) — `~/.zshrc`, `~/.p10k.zsh`, `~/.tmux.conf` versioned. `80-dotfiles.sh` is already structured to fall back to a `~/.dotfiles/` clone when present. No application-code changes. No CI gate impact (doc + scripts + devcontainer spec only). ## What lands | Path | Change | | --- | --- | | `docs/setup/01-dev-debian-vm-setup.md` | **New.** Step-by-step doc: workstation prep (SSH agent + VSCode Remote-SSH + fonts), bootstrap orchestrator, per-script effects, project clone, infra boot, IDE flow A / B / C, apf-ai-service .NET appendix, troubleshooting. | | `docs/setup/02-wsl-terminal-setup.md` | Renamed from `01-wsl-terminal-setup.md`. No content change. | | `docs/setup/03-dev-web-stack.md` | Renamed from `02-dev-web-stack.md`. No content change. | | `docs/setup/04-angular-nx-monorepo.md` | Renamed from `03-angular-nx-monorepo.md`. No content change. | | `docs/setup/README.md` | **New.** Index of the `docs/setup/` folder. | | `docs/setup/scripts/lib.sh` | **New.** Shared helpers — colour-coded log/ok/warn/err/skip, `apt_install` skipping already-installed, `ensure_line` idempotent append, `confirm` prompt. | | `docs/setup/scripts/bootstrap.sh` | **New.** Orchestrator running scripts 10..80 in order with confirmation prompts. | | `docs/setup/scripts/10-base-packages.sh` | **New.** apt update + base packages (curl, wget, git, build-essential, …). | | `docs/setup/scripts/20-zsh.sh` | **New.** zsh + Oh My Zsh (RUNZSH=no, no shell hijack) + Powerlevel10k + `zsh-autosuggestions` + `zsh-syntax-highlighting`. Patches `~/.zshrc` (theme, plugins, fzf hook). | | `docs/setup/scripts/30-cli-tools.sh` | **New.** `bat eza fd-find ripgrep fzf zoxide ncdu keychain` + `jq yq httpie make tree htop tmux direnv dnsutils unzip rsync`. Symlinks Debian-renamed binaries (`batcat`→`bat`, `fdfind`→`fd`) into `~/.local/bin` so notes/aliases.zsh works as-is. | | `docs/setup/scripts/40-node.sh` | **New.** nvm v0.40.1 + Node from `.nvmrc` (currently 24) + corepack enable + pnpm warmed from `package.json#packageManager`. | | `docs/setup/scripts/50-docker.sh` | **New.** Docker CE + compose plugin from docker.com apt repo, user added to `docker` group, `docker.service` enabled at boot. Pinned GPG key + repo line for Debian Trixie. | | `docs/setup/scripts/60-tuning.sh` | **New.** `fs.inotify.max_user_watches=524288` (Vite/Nx watch ceiling), optional 4 GB swapfile, optional hostname rename (only prompts on generic hostnames like `debian13`). | | `docs/setup/scripts/70-hardening.sh` | **New.** Best-effort UFW (`allow OpenSSH` only) + unattended-upgrades on security channel + fail2ban + sshd drop-in (`PermitRootLogin no`, `PasswordAuthentication no`, `AllowAgentForwarding yes`). Probes before applying, validates `sshd -t` before reloading, skips cleanly if infra already locked the box down. | | `docs/setup/scripts/80-dotfiles.sh` | **New.** Symlinks `notes/aliases.zsh` → `~/.oh-my-zsh/custom/aliases.zsh` (backs up an existing target). Copies `notes/gitconfig.txt` to `~/.gitconfig`, prompts for identity, applies via `git config --global user.name/email`. | | `docs/setup/systemd/apf-portal-infra@.service` | **New.** Template systemd unit auto-starting `./infra/local/dev.sh up` at boot. Install: `enable apf-portal-infra@$USER.service`. | | `.devcontainer/devcontainer.json` | **New.** VSCode Dev Container spec on `mcr.microsoft.com/devcontainers/typescript-node:1-24-bookworm`. `docker-outside-of-docker` feature + `--network=apf-portal-dev` so DNS to `postgres`/`redis`/`otel-collector` works. `initializeCommand` fails fast if `dev.sh up` hasn't been run yet. Forwarded ports labelled. Six dev extensions pre-installed. | | `.devcontainer/post-create.sh` | **New.** `corepack enable && pnpm install --frozen-lockfile`. | | `CLAUDE.md` | "Environment conventions" rewritten: documents the two envs (`local` / `development`) + hybrid sub-mode + the two IDE flows (Remote-SSH / Devcontainer), points at the new VM setup doc and the legacy WSL doc. | ## Key choices - **Idempotent scripts, individually runnable.** Each script probes before doing anything (apt package already installed? plugin already cloned? UFW already active? sshd drop-in already present?). Bootstrap is the orchestrator; each script also runs standalone. Re-running after a partial setup is safe and reports `↪ skip` for the no-op cases. - **No private key on the VM — SSH agent forwarding instead.** `~/.ssh/config` on the workstation carries `ForwardAgent yes`; the VM never holds long-lived secrets. Same future pattern for GPG signing (covered in §8.5 as appendix). `keychain` is installed by `30-cli-tools.sh` as a fallback for scenarios where agent forwarding is not available (CI runners, scripts). - **Hardening is "best-effort, probe-first".** Some infra teams ship pre-hardened VMs; this script doesn't fight that. UFW already active? Print rules and skip. unattended-upgrades already on? Skip. SSH already locked down? Skip. Each section validates before reloading so a misconfig can't take SSH offline. - **Devcontainer assumes infra-on-host.** The container runs on the VM but talks to postgres / redis / otel **on the same VM's host docker daemon** through the shared `apf-portal-dev` Compose network. `initializeCommand` fails fast with a clear message if `./infra/local/dev.sh up` hasn't been run yet — better than puzzling `ECONNREFUSED` errors at runtime. - **`60-tuning.sh` raises inotify to 524288.** Default Debian limit is 8K; Vite/Nx in this monorepo blow past that. The setting is persisted in `/etc/sysctl.d/99-apf-portal.conf` so it survives reboot. - **Hybrid mode (workstation IDE + VM infra) is a documented sub-mode.** SSH `LocalForward` directives on 5432/6379/4317/4318 expose the VM's infra services as `localhost:*` on the workstation. Latency cost: 5-15 ms per query, fine for daily work; for long-running flows, wrap in `tmux` or use `autossh`. ## Notes for the reviewer - **Renaming the existing setup docs (01→02, 02→03, 03→04) is the only "destructive" change.** `git log --follow` still works because of `git mv`. Diff shows up as renames, not delete-and-add. - **The brief asked for `bat eza fd-find ripgrep fzf zoxide docker keychain ncdu git`.** `git` is installed by `10-base-packages.sh` (every other script needs it). Everything else lives in `30-cli-tools.sh` + `50-docker.sh`. The "fullstack-dev extras" (`jq yq httpie make tree htop tmux direnv dnsutils unzip rsync`) are additions I proposed in the lock-in question and that you greenlit (full scope) — easy to trim if any of them turn out to be unwanted. - **Both Node toolchains in parallel** — nvm on the VM (via `40-node.sh`) **and** devcontainer in the repo. Devs can use either; both read the same `.nvmrc` + `packageManager` pin so the version stays consistent. - **The systemd unit is a TEMPLATE** (`apf-portal-infra@.service`) — install once, enable per-user (`enable apf-portal-infra@$USER.service`). This is the right shape for a shared VM with multiple devs eventually, even if today only one user uses it. - **No PR-body Co-Authored-By trailer, no Generated-with-Claude footer**, per the project rule. ## Test plan Manual (no automated test exists for this kind of setup work): - [ ] On a fresh Debian 13 VM: `git clone …`, `./docs/setup/scripts/bootstrap.sh`, answer prompts, end up with zsh + Powerlevel10k + all the requested CLI tools + Node 24 + pnpm 10.33.4 + Docker on PATH. - [ ] `./infra/local/dev.sh up` boots successfully against the VM's local docker daemon. - [ ] `pnpm install` + `pnpm exec nx run-many -t lint test --parallel=3` passes on the VM. - [ ] VSCode Remote-SSH from a workstation: connect, open `~/Works/apf_portal`, run `pnpm exec nx serve portal-bff`, confirm reaches `postgres:5432`. - [ ] VSCode Dev Containers from the same workstation: `Reopen in Container`, image builds, `postCreateCommand` runs `pnpm install`, dev server reaches postgres through the `apf-portal-dev` network. - [ ] Hybrid mode: SSH tunnel from workstation, `pnpm exec nx serve portal-bff` locally, confirm postgres reachable via `localhost:5432`. - [x] `pnpm exec prettier --check` clean on the touched markdown files. - [x] Scripts pass `bash -n` (syntax check) — verified during writing. ## What's next - Validate by walking through this doc on the actual VM `10.100.201.21`. Any friction surfaced becomes a follow-up PR (`docs(setup): ...`). - Once the dev VM is operational, return to **ADR-0027 Implementation PR 1** (Region / Delegation / Structure Prisma schema + inline reference-data migration) — paused since the start of this PR. - Set up the **private dotfiles repo** (`apf/dotfiles`) as a small follow-up, then teach `80-dotfiles.sh` to prefer the dotfiles repo over `notes/`. - When migrating to GitLab: PR pair — (a) `git remote set-url` doc updates here, (b) `infra/gitlab-runners/` replacing `infra/ci-runners.compose.yml`. - Future: deploy the **shared preview infra** on `vm-gitlab` (10.100.201.10) — CI-driven, separate PR.
julien added 1 commit 2026-05-24 18:39:36 +02:00
docs(setup): add Debian 13 dev-VM setup procedure + scripts + devcontainer
CI / commits (pull_request) Successful in 2m52s
CI / check (pull_request) Successful in 3m3s
CI / scan (pull_request) Successful in 3m11s
CI / a11y (pull_request) Successful in 3m5s
Docs site / build (pull_request) Successful in 3m10s
CI / perf (pull_request) Successful in 6m53s
f70b1d3a1b
New procedure in docs/setup/01-dev-debian-vm-setup.md targeting the new
dev VM (10.100.201.21, Debian 13) that replaces the WSL workflow. The
existing setup files are renumbered (01->02, 02->03, 03->04) to free
prefix 01 for the default flow.

10 modular idempotent scripts under docs/setup/scripts/ install the
stack: zsh + Oh My Zsh + Powerlevel10k + plugins, modern CLI tooling
(bat, eza, fd-find, ripgrep, fzf, zoxide, ncdu, keychain + dev extras),
nvm + Node from .nvmrc + corepack + pnpm, Docker CE + compose plugin,
inotify watches + swap + hostname tuning, best-effort UFW + sshd
hardening that probes-before-applying, and notes/aliases.zsh +
notes/gitconfig.txt installation.

A systemd template (apf-portal-infra@.service) auto-starts the local
dev infra stack at boot per user.

.devcontainer/ ships a VSCode Dev Container spec pinning Node 24 and
attaching to the host's apf-portal-dev Compose network so postgres /
redis / otel resolve by DNS from inside the container. initializeCommand
fails fast if the infra hasn't been brought up yet.

CLAUDE.md Environment conventions documents the two envs (local +
development) plus the hybrid sub-mode (workstation IDE + VM infra via
SSH LocalForward) and the two IDE flows (Remote-SSH + Devcontainer).

Adjacent follow-ups (not in this PR): preview infra on the GitLab VM
(10.100.201.10), GitLab Runner migration alongside the Gitea -> GitLab
cutover, private dotfiles repo.
julien merged commit 8a04540410 into main 2026-05-24 18:40:09 +02:00
julien deleted branch docs/dev-debian-vm-setup 2026-05-24 18:40:10 +02:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: julien/apf_portal#220