Skip to content

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.

  1. Enter BIOS: Power on and rapidly tap the setup key (F1 for Lenovo).
  2. 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.
  3. Power Behavior: Set "After Power Loss" to Power On. After a power cut the servers come back on their own.
  4. 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) or Install (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 sudo after 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.

  1. Check Interface Name:

    ip link
    # Look for eno1, eth0, or enp3s0
    

  2. Edit Config:

    sudo nano /etc/network/interfaces
    

  3. Make sure the interface has an auto line. The installer writes only allow-hotplug <iface>. That's fine for a laptop but wrong for a Docker host (see the warning below).

    /etc/network/interfaces
    auto lo
    iface lo inet loopback
    
    # The primary network interface
    auto eno1
    allow-hotplug eno1
    iface eno1 inet dhcp
    
    /etc/network/interfaces
    auto lo
    iface lo inet loopback
    
    # The primary network interface
    auto eno1
    iface eno1 inet static
        address 192.168.1.50/24
        gateway 192.168.1.1
        # DNS servers (needs the resolvconf package to take effect)
        dns-nameservers 192.168.1.1
    

    Why auto matters on a Docker host

    At boot, networking.service only waits for interfaces marked auto. With allow-hotplug alone, network-online.target is reached before the NIC has a lease, Docker starts in that gap, and the first containers get a resolv.conf with no nameservers. They stay broken until restarted. I hit this on every server after a power cut. Full write-up: Networking & DNS lessons learned.

  4. Apply (from the console, not over SSH: a typo here drops your session):

    sudo systemctl restart networking
    

B. Connect via SSH and copy your key

Switch to your main PC terminal:

ssh-copy-id -i ~/.ssh/id_ed25519.pub <YOUR_USER>@<YOUR_SERVER_IP>
ssh <YOUR_USER>@<YOUR_SERVER_IP>

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:

/etc/apt/sources.list.d/debian.sources
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:

sudo apt install qemu-guest-agent -y
sudo systemctl enable --now qemu-guest-agent

B. Prevent Sleep

Prevent the Mini PC from suspending when idle:

sudo systemctl mask sleep.target suspend.target hibernate.target hybrid-sleep.target

C. Install Vital Tools

Debian "Standard Utilities" is very barebones. dnsutils gives you dig, which every DNS problem eventually needs.

sudo apt install curl wget git htop vim tmux dnsutils -y

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:

echo 'kernel.panic = 10' | sudo tee /etc/sysctl.d/90-panic.conf
sudo sysctl --system

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:

/etc/sysctl.d/99-disable-ipv6.conf
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

  1. Install Docker CE from Docker's apt repository: Docker → Installation.
  2. Apply the fleet daemon.json (log rotation, container DNS, longer shutdown timeout): Docker → Post-Installation.
  3. Create the state directory. Every service keeps its data in bind mounts under /mnt/appdata/<service>:

    sudo mkdir -p /mnt/appdata
    
  4. 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.

  5. Hand-applied stacks that can't deploy themselves (Komodo Core itself) live in /opt/docker/<stack>/ with their compose.yaml and 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 -), run apt install sudo if it's missing, then usermod -aG sudo <YOUR_USER>. Log out and back in.
  • 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, dhcpcd writes it from the router's lease. On a static host, dns-nameservers only takes effect with the resolvconf package installed; otherwise write nameserver 192.168.1.1 into /etc/resolv.conf yourself.
  • DNS failures in containers after a reboot: See Networking & DNS lessons learned and Docker & systemd lessons learned.

Sources & further reading