Server Setup: App Server¶
Every server in my lab is built the same way. The OS is boring on purpose: all the interesting parts live in containers, and those are deployed from git.
| Layer | What every server runs | Why |
|---|---|---|
| OS | Debian 13 "trixie", minimal (netinst, no desktop) | Stable, long support, nothing installed that I didn't ask for |
| Access | OpenSSH with key login, an admin user in the sudo group |
See SSH and Users & Permissions |
| Containers | Docker CE + Compose plugin from Docker's own apt repo | Debian's docker.io package lags behind; the fleet runs Docker 29.x |
| State | /mnt/appdata/<service> |
One directory to back up, one place to look |
| Deploys | Komodo periphery agent; compose files come from a git repo | Push to git → the changed stacks redeploy. See GitOps |
| Updates | unattended-upgrades for Debian security/point releases, Docker excluded |
See Host Maintenance |
1. Preparation & Media Creation¶
A. Download ISO Get the Small Installation Image (Netinst).
Architecture
For Lenovo M920q/M70q nodes, ensure you download the amd64 ISO.
B. Flash to USB
Use BalenaEtcher, Rufus, or dd to write the ISO to a USB drive (4GB+).
2. BIOS / UEFI Configuration¶
Mini PCs often ship with settings optimized for Windows. Adjust these for a Linux server.
- Enter BIOS: Power on and rapidly tap the setup key (
F1for Lenovo). - Secure Boot: Set to Disabled.
- Reasoning: While Debian supports Secure Boot, disabling it prevents headaches with third-party drivers (Nvidia) or unsigned kernel modules later.
- Power Behavior: Set "After Power Loss" to Power On. After a power cut the servers come back on their own.
- Boot Order: Prioritize the USB drive.
3. Installation Process¶
Boot from USB. The Debian installer uses a classic text-based interface. Navigate with Arrow Keys, select with Enter, and toggle options with Space.
A. Initial Settings
- Install: Select
Graphical Install(easier) orInstall(text-only). - Language/Location/Keyboard: Select defaults.
- Hostname:
<YOUR_HOSTNAME>(or your preference). - Domain Name: Leave blank (unless you have a local domain).
B. User & Password (Crucial)
Sudo Configuration
Leave the 'Root password' field BLANK.
If you leave the root password blank, the installer locks the root account, installs sudo and adds your new user to the sudo group. If you set a root password now, sudo isn't installed and you'll have to set it up yourself later.
- Full name / Username: Enter your details (e.g.,
<YOUR_USER>). - Password: Set a strong password for this user. You'll still need it for
sudoafter SSH goes keys-only.
C. Partitioning
- Method: "Guided - use entire disk and set up LVM".
- Partition Scheme: "All files in one partition" (easiest for beginners).
- Confirm: Select "Finish partitioning and write changes to disk" -> Yes.
/tmp is RAM-backed on Debian 13
New trixie installs mount /tmp as a tmpfs (up to 50% of RAM), and files in /tmp and /var/tmp are cleaned up after 10 and 30 days. Don't park large files there. systemctl edit tmp.mount resizes it, systemctl mask tmp.mount puts it back on disk.
D. Software Selection (Tasksel)
The installer will install the base system and then ask for additional software.
- Scan extra media? No.
- Package Manager: Select a mirror close to you (e.g.,
deb.debian.org). - Software selection:
- Debian desktop environment (Uncheck this)
- GNOME (Uncheck this)
- SSH server (Check this)
- Standard system utilities (Check this)
4. Post-Install Configuration¶
Remove the USB and reboot. Log in via the physical terminal one last time to get the IP (ip -br a).
A. Network Configuration¶
Debian servers use ifupdown (/etc/network/interfaces), not Netplan, with dhcpcd as the DHCP client on trixie.
I keep the servers on DHCP with a reservation on the router, so every address is managed in one place. A static
address in the file works just as well.
-
Check Interface Name:
-
Edit Config:
-
Make sure the interface has an
autoline. The installer writes onlyallow-hotplug <iface>. That's fine for a laptop but wrong for a Docker host (see the warning below).Why
automatters on a Docker hostAt boot,
networking.serviceonly waits for interfaces markedauto. Withallow-hotplugalone,network-online.targetis reached before the NIC has a lease, Docker starts in that gap, and the first containers get aresolv.confwith no nameservers. They stay broken until restarted. I hit this on every server after a power cut. Full write-up: Networking & DNS lessons learned. -
Apply (from the console, not over SSH: a typo here drops your session):
B. Connect via SSH and copy your key¶
Switch to your main PC terminal:
Then turn off password logins and restrict who can log in. See SSH Hardening.
C. APT sources and updates¶
Debian 13 moves APT to the deb822 .sources format. If the install (or an upgrade from 12) left you a one-line
/etc/apt/sources.list, convert it, then update:
sudo apt modernize-sources # converts sources.list / *.list files to deb822 .sources
sudo apt update && sudo apt full-upgrade -y
The result should look like this. non-free-firmware is what the installer enables for NIC/CPU firmware:
Types: deb
URIs: http://deb.debian.org/debian
Suites: trixie trixie-updates
Components: main non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
Types: deb
URIs: http://security.debian.org/debian-security
Suites: trixie-security
Components: main non-free-firmware
Signed-By: /usr/share/keyrings/debian-archive-keyring.gpg
5. Quality of Life & Security¶
A. Install QEMU Guest Agent (If VM)¶
If this is running as a VM inside Proxmox:
B. Prevent Sleep¶
Prevent the Mini PC from suspending when idle:
C. Install Vital Tools¶
Debian "Standard Utilities" is very barebones. dnsutils gives you dig, which every DNS problem eventually needs.
D. Reboot on kernel panic¶
The kernel default is to hang forever after a panic, so a headless box stays down until someone walks over and power-cycles it. I'd rather it reboot after 10 seconds:
Debian 13 no longer reads /etc/sysctl.conf; put settings in /etc/sysctl.d/*.conf. The panic log isn't lost: on
UEFI machines it's kept in efi_pstore and systemd-pstore copies it to /var/lib/systemd/pstore on the next boot.
E. No IPv6 route? Turn IPv6 off¶
If your network doesn't route IPv6 but your DNS server hands out AAAA records, some software (.NET apps, Docker Hub pulls) waits on IPv6 connections that can never succeed. On those hosts I disable IPv6 outright:
net.ipv6.conf.all.disable_ipv6 = 1
net.ipv6.conf.default.disable_ipv6 = 1
net.ipv6.conf.lo.disable_ipv6 = 1
Apply with sudo sysctl --system. Background: AAAA records with no IPv6 route.
6. Docker and the deploy agent¶
- Install Docker CE from Docker's apt repository: Docker → Installation.
- Apply the fleet
daemon.json(log rotation, container DNS, longer shutdown timeout): Docker → Post-Installation. -
Create the state directory. Every service keeps its data in bind mounts under
/mnt/appdata/<service>: -
Install the Komodo periphery agent. Komodo Core (on the control host) tells periphery which stacks to deploy, and periphery checks the compose files out of the git repo and runs
docker compose up. Nothing on the server is edited by hand. See GitOps. - Hand-applied stacks that can't deploy themselves (Komodo Core itself) live in
/opt/docker/<stack>/with theircompose.yamland a mode-600.env.
Wait for DNS, not just for a link
Even with auto set, network-online.target only means the interface has an address, not that the router is
answering DNS yet. After a power cut, the router and the servers boot together. I install a small oneshot unit
ordered Before=network-online.target that loops dig @<gateway> until it gets an answer (bounded to a few
minutes). That keeps docker.service from starting into the dead window.
Unattended upgrades¶
Debian security updates and point releases install themselves; Docker packages are held back because upgrading
docker-ce restarts every container. Reboots happen only when required, in a weekly window. Setup:
Host Maintenance.
Troubleshooting¶
- "Username is not in the sudoers file": You likely set a root password during install.
- Fix: Switch to root (
su -), runapt install sudoif it's missing, thenusermod -aG sudo <YOUR_USER>. Log out and back in.
- Fix: Switch to root (
- SSH Connection Refused: Check if the service is running (
sudo systemctl status ssh). If missing,sudo apt install openssh-server. - DNS failures on the host: Check
/etc/resolv.conf. On a DHCP host,dhcpcdwrites it from the router's lease. On a static host,dns-nameserversonly takes effect with theresolvconfpackage installed; otherwise writenameserver 192.168.1.1into/etc/resolv.confyourself. - DNS failures in containers after a reboot: See Networking & DNS lessons learned and Docker & systemd lessons learned.