A Reproducible Dev Machine

Debian Development Machine Setup

Chapter 10 ยท Capstone: A Reproducible Dev Machine

You now have a working devserver: Debian 13, a graphics driver, Python, Node.js, PHP, MariaDB, editors, and your own Git server. The last question is the one that decides whether all that work is safe: if the disk died tomorrow, how long would it take to get back here? This chapter turns what you built into something you can repeat and recover: a list of what is installed, a script that rebuilds it, your settings stored in Git, and backups you have actually tested.

The idea running through the whole course
Every chapter ended with a record rather than a memory: a lock file for a project, a version pinned in a file, a list of extensions. A machine is the same. What you can rebuild from a file is safe; what only exists in your head or on one disk is not.

Step 1: Audit What Is There

Start from facts, using the checking habits from Chapter 3. Keep everything in one folder so it can go into a Git repository:

mkdir -p ~/machine-setup && cd ~/machine-setup # Packages you (or the installer) chose, not their dependencies apt-mark showmanual | sort > packages-manual.txt # Versions of the main tools { lsb_release -ds; uname -r; python3 --version; php --version | head -1; composer --version; mariadb --version; git --version; code --version | head -1; nvidia-smi --query-gpu=driver_version --format=csv,noheader; } > versions.txt # Things that did not come from apt pipx list > pipx.txt code --list-extensions > vscode-extensions.txt ls ~/.nvm ~/.pyenv /usr/local/bin > non-apt.txt 2>&1

Node's version is not in that list because it comes from nvm, so record it separately: . ~/.nvm/nvm.sh && nvm current. Read each file. packages-manual.txt is longer than you might expect, because the Debian installer and the desktop environment marked many packages as manual too; that is normal, and it is why the script below installs a deliberate list of your own instead of replaying the whole file.

Do not replay the whole list blindly
Feeding packages-manual.txt to apt install on a different install can fail, because packages get renamed or removed between releases, and it drags in installer choices you did not make on purpose. Use it as a reference to build your own short list, and to spot things you forgot you had.

Fill in the server side

Chapter 7 promised a comparison with debserver. Run the same version commands there and keep both files:

ssh debserver 'lsb_release -ds; php --version | head -1; mysql --version || mariadb --version' > ~/machine-setup/debserver-versions.txt

When debserver is reinstalled with Debian 13, run it again and the two files should agree.

Step 2: A Provisioning Script

A provisioning script is a shell script that does the setup for you. The important property is that it is safe to run twice: each step checks whether the work is already done, exactly the check-before-change habit from Chapter 3, using the same have and pkg_installed helpers. Run it as your normal user, not as root; it calls sudo where it needs to.

#!/usr/bin/env bash # bootstrap.sh - rebuild devserver's software on a fresh Debian 13 install. # Run as your normal user. Safe to run again: every step checks first. set -euo pipefail have() { command -v "$1" >/dev/null 2>&1; } pkg_installed() { dpkg-query -W -f='${Status}' "$1" 2>/dev/null | grep -q 'install ok installed'; } apt_install() { local missing=() for p in "$@"; do pkg_installed "$p" || missing+=("$p"); done if [ "${#missing[@]}" -gt 0 ]; then sudo apt-get install -y "${missing[@]}" else echo "already installed: $*" fi } echo "== 1. Sources: add contrib and non-free (needed for the NVIDIA driver) ==" SRC=/etc/apt/sources.list.d/debian.sources if ! grep -Eq '^Components:.*[[:space:]]non-free([[:space:]]|$)' "$SRC"; then sudo sed -i '/^Components:/ s/$/ contrib non-free/' "$SRC" # check the result afterwards fi sudo apt-get update echo "== 2. Everyday tools ==" apt_install build-essential curl wget git vim htop tree unzip zip jq ripgrep \ ca-certificates gnupg pciutils echo "== 3. NVIDIA driver ==" apt_install linux-headers-amd64 nvidia-kernel-dkms nvidia-driver nvidia-smi # Secure Boot: the MOK enrolment from Chapter 4 is still a manual step. echo "== 4. Python ==" apt_install python3-pip python3-venv python3-dev pipx echo "== 5. PHP, Composer and databases (php-cli, never the 'php' metapackage) ==" apt_install php-cli php-mbstring php-xml php-curl php-zip php-intl php-mysql php-sqlite3 \ composer mariadb-server sqlite3 echo "== 6. Node.js through nvm ==" if [ ! -d "$HOME/.nvm" ]; then git clone https://github.com/nvm-sh/nvm.git "$HOME/.nvm" git -C "$HOME/.nvm" checkout v0.40.8 fi set +u; . "$HOME/.nvm/nvm.sh"; nvm install --lts; set -u # The shell lines that load nvm at login are in Chapter 6; add them to ~/.bashrc. echo "== 7. VS Code from Microsoft's repository ==" if ! pkg_installed code; then apt_install wget gpg if [ ! -f /usr/share/keyrings/microsoft.gpg ]; then wget -qO- https://packages.microsoft.com/keys/microsoft.asc | sudo gpg --dearmor -o /usr/share/keyrings/microsoft.gpg fi sudo tee /etc/apt/sources.list.d/vscode.sources >/dev/null <<'EOF' Types: deb URIs: https://packages.microsoft.com/repos/code Suites: stable Components: main Architectures: amd64,arm64,armhf Signed-By: /usr/share/keyrings/microsoft.gpg EOF sudo apt-get update apt_install code fi if [ -f "$HOME/machine-setup/vscode-extensions.txt" ]; then xargs -L1 code --install-extension < "$HOME/machine-setup/vscode-extensions.txt" fi echo echo "Done. Still manual: Secure Boot key (Ch. 4), SSH keys and ssh config (Ch. 2)," echo "pyenv (Ch. 5), Gitea install and restore (Ch. 9 and below), dotfiles."

Read it as a table of contents for the whole course: every section is a chapter, and every chapter's choices appear as a line. Things it deliberately does not do are worth noticing. It never stores a password. It does not restore Gitea, because Gitea's data comes from a backup, not from a script. And it leaves a few steps manual and says so, because a script that silently guesses (a Secure Boot key, an SSH key) is worse than one that tells you what is left.

Test it before you need it
Check the syntax with bash -n bootstrap.sh, and run it a second time on devserver itself: it should print “already installed” for everything and change nothing. That proves the checks work. The real proof is a fresh Debian 13 virtual machine, if you have one to spare. Read the sed line's effect on debian.sources afterwards, as it is the one step that edits a system file.

Step 3: Your Settings, in Git

Installed software is half of a machine. The other half is your configuration: the files in your home directory whose names start with a dot. Keep the ones you care about in a repository on your own Gitea (the payoff of Chapter 9):

mkdir -p ~/dotfiles && cd ~/dotfiles mv ~/.bashrc bashrc && ln -s ~/dotfiles/bashrc ~/.bashrc mv ~/.gitconfig gitconfig && ln -s ~/dotfiles/gitconfig ~/.gitconfig cp -r ~/machine-setup . # bootstrap.sh and the lists live here too git init -b main && git add . && git commit -m "Dotfiles and machine setup" git remote add origin http://devserver:3000/YOURNAME/dotfiles.git git push -u origin main # On a rebuilt machine: # git clone http://devserver:3000/YOURNAME/dotfiles.git ~/dotfiles # ln -sfn ~/dotfiles/bashrc ~/.bashrc

A symbolic link means the real file lives in the repository, so editing ~/.bashrc edits the tracked copy, and git diff shows what you changed.

Never put secrets in the repository
Private SSH keys, the Gitea password, database passwords, API tokens and .env files do not belong in it, even on your own server. Store the list of which secrets you need, not the secrets. Also remember that the repository lives on devserver itself: a copy that shares a disk with the original is not a backup, which brings us to the next step.

Step 4: Backups You Have Tested

Decide what must survive, and where a second copy lives.

WhatWhere it livesHow to protect it
Your files and projects/home/youCopy to another disk, leaving out what can be rebuilt (node_modules, virtual environments, vendor)
Gitea data/var/lib/gitea, /etc/gitea, and the database if you chose MariaDBStop or dump consistently, then copy
Databases you use for developmentMariaDBmariadb-dump for the ones that matter
Software and settingsPackage lists, bootstrap.sh, dotfilesIn Git, and Git copied elsewhere
Secrets and keys~/.sshEncrypted copy in a safe place, never in Git

Files

# Preview first: -n changes nothing and -v lists what would happen rsync -avn --delete --exclude node_modules --exclude .venv --exclude venv --exclude vendor \ ~/ /mnt/backup/home/ # Then for real (remove the n) rsync -av --delete --exclude node_modules --exclude .venv --exclude venv --exclude vendor \ ~/ /mnt/backup/home/

--delete makes the destination an exact mirror, including removing files you deleted. That is what you want for a mirror and dangerous when you get the two paths the wrong way round or the disk is not mounted, so always do the preview first and check that /mnt/backup is really your backup disk (findmnt /mnt/backup). If rsync is missing, sudo apt install rsync.

Gitea

Gitea's own dump command bundles its repositories, database and configuration into one archive. Run /usr/local/bin/gitea dump --help first to see the options for your version, and then run it as the git user so that file ownership matches the running service:

cd /tmp sudo -u git /usr/local/bin/gitea dump --help sudo -u git /usr/local/bin/gitea dump -c /etc/gitea/app.ini ls -lh /tmp/gitea-dump-*.zip

If the command complains about a working directory or a permission, read its message rather than working around it; the --help output tells you where the archive is written. Then copy the archive to the backup disk. Keep a few dated dumps, not just the latest.

A backup you have not restored is a hope
Test on something harmless: restore a single file from the rsync copy into a scratch folder and open it, and unpack the Gitea archive somewhere temporary and look inside. A full Gitea restore onto a spare machine is the real test, and worth doing once, before the day you need it. Chapter 10's exercises walk through the small versions.

Upgrading Gitea, the manual routine

Because apt does not manage Gitea, updating it is your job. Do it in this order: dump first, then download the new binary and verify its signature (Chapter 9), then sudo systemctl stop gitea, copy the new file over /usr/local/bin/gitea, sudo systemctl start gitea, and check journalctl -u gitea -n 30. Read the release notes for the versions you are skipping, since a jump across several versions can include changes you need to know about.

The Regular Routine

How oftenDo this
Weeklysudo apt update && sudo apt full-upgrade; reboot if the kernel changed (NVIDIA driver check, Chapter 4); run the file backup
MonthlyGitea dump, MariaDB dump, refresh packages-manual.txt and vscode-extensions.txt, commit and push the setup repository, look at the Gitea release page
When you install something newAdd it to bootstrap.sh in the same sitting
Twice a yearRestore something from backup on purpose; re-check Node's and Debian's support dates

Closing Checklist

Each line points back to the chapter that built it.

ChapterYou can now…
1 Planning & InstallingInstall Debian 13 with sensible choices, and know what you chose
2 First BootKeep the system updated, use sudo, log in over SSH with keys, and tell devserver from debserver at a glance
3 What's InstalledCheck whether something is installed, which version, and where it came from
4 Graphics DriversInstall and verify the NVIDIA driver, handle Secure Boot, and survive kernel updates
5 PythonUse venvs, pipx and pyenv instead of fighting the system Python
6 Node.jsRun several Node versions with nvm and keep project and global installs apart
7 PHP & DatabasesInstall PHP without a web server, use Composer, run MariaDB with a limited user, and compare with the server
8 EditorsInstall VS Code safely, set a default editor, and record extensions
9 GiteaRun a Git server as a proper service and push to it
10 This chapterRebuild the machine from a script, keep your settings in Git, and restore from a backup you have tested
Where to go next
Docker for Beginners is the natural next addition to this machine, and it would go into bootstrap.sh as an eleventh section. When debserver is reinstalled with Debian 13, much of this course, and this very script, applies to it too, with the web server courses (Setting Up a Web Server on Debian, and Nginx In Depth) covering the parts that differ.

Hands-On Exercises

Exercise 1

Run the audit: create ~/machine-setup with the package list, versions, extension list and non-apt tool list, plus the debserver versions file. Then find three packages in packages-manual.txt that you did not install on purpose, and say what each is.

๐Ÿ“„ View solution
Exercise 2

Save the provisioning script as bootstrap.sh, syntax-check it, and run it twice on devserver. Show from the output that the second run changed nothing. Then add one new tool of your choice to it, correctly, using the helper functions.

๐Ÿ“„ View solution
Exercise 3

Put your dotfiles and setup folder into a Gitea repository. Take a file backup with an rsync preview first and then for real, and a Gitea dump. Prove both work by restoring one file from the rsync copy and listing the contents of the Gitea archive.

๐Ÿ“„ View solution

Chapter 10 Quick Reference

  • Audit: apt-mark showmanual, version commands, pipx list, code --list-extensions, nvm current
  • The manual-package list is a reference, not something to replay blindly
  • A provisioning script is re-runnable: every step checks first (have, pkg_installed)
  • Install php-cli plus extensions, never the php metapackage; add contrib non-free before the NVIDIA packages
  • The script never holds passwords and leaves risky steps (Secure Boot, SSH keys, Gitea restore) manual
  • Dotfiles: real files in a Git repository, symbolic links from the home directory, no secrets
  • Backups: rsync -avn preview then run; gitea dump (check --help); a copy on the same disk is not a backup
  • Upgrade Gitea by hand: dump, verify, stop, copy, start, check the journal
  • A backup is only real once you have restored from it