Debian 13, minimum ram and disk, 38mb - Guide - Part 1 (running nothing)
AnthonySmith
ModeratorHosting ProviderOGSenpai
Took longer than expected, but here we are, the minimal Debian 13 guide, without recompiling the kernel and on a 256mb TierHive VPS the kernel eats 44mb as standard in the daily Debian 13 cloud image and will also require an additional 88mb of userspace memory, so thats 132MB this is what we are targeting and this is why you don't get away with 128mb as standard.
AI USE Declaration: md formatting, spelling, grammar done with Claude, the work and methods is my own.
Minimising Debian 13 on a KVM VPS
This guide documents how to strip a freshly deployed Debian 13 (trixie) VPS down to the minimum RAM and disk footprint without breaking it.
This targets KVM-based VPS instances with a virtio-blk disk (/dev/vda), a single network interface with a static IP assigned at deployment, and BIOS boot. The instance is a NAT VPS: SSH is exposed on a forwarded external port, not directly on port 22. Adjust the port forwarding in your provider portal where noted if you use NAT or just use your external IP and port 22 if a fixed IP is assigned.
Before
Fresh deploy, cloud-init has run, nothing changed yet.
total used free shared buff/cache available
Mem: 213 88 27 0 108 124
Swap: 0 0 0
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 2.8G 832M 1.8G 32% /
Note your network details before starting
Stage 5 replaces systemd-networkd with a static /etc/network/interfaces file. old school debain style, before making any changes, record your interface name, IP address, and gateway. You will need them later.
ip a
ip route show
On TierHive the interface is ens3, this may vary (but probably not) The IP and gateway are shown in the VPS control panel and in the output above. Note them down before proceeding.
Stage 1: GRUB Cmdline
Reduce the boot timeout (Because why not) and add kernel parameters to cut memory overhead.
sed -i 's/^GRUB_TIMEOUT=5/GRUB_TIMEOUT=1/' /etc/default/grub
sed -i 's|^GRUB_DISTRIBUTOR=.*|GRUB_DISTRIBUTOR=Debian|' /etc/default/grub
sed -i 's|^GRUB_CMDLINE_LINUX=.*|GRUB_CMDLINE_LINUX="mitigations=off console=tty0 console=ttyS0,115200 earlyprintk=ttyS0,115200 consoleblank=0 ipv6.disable=1 audit=0 nowatchdog"|' /etc/default/grub
update-grub
The GRUB_DISTRIBUTOR line is changed from a shell call to lsb_release to a static string. The lsb-release package is removed later; leaving the shell call in place would cause update-grub to fail after that point.
Remove the GRUB locale files. They exist to translate GRUB menu entries. On a headless server booting via serial console, you can live without them:
rm -rf /boot/grub/locale
The details on the parameters added to GRUB_CMDLINE_LINUX:
mitigations=offdisables all Spectre and Meltdown mitigations. On a single-user VPS where you control all running code, these protect against nobody as VM-to-VM isolation is handled by the hypervisor. This eliminates the overhead of PTI (page table isolation on every syscall), IBRS, IBPB, and several other mitigations Skip this if you run untrusted code.ipv6.disable=1disables IPv6 at kernel level. This is a NAT VPS with an IPv4 address only. Skip this if you use IPv6 or are @yoursunnyaudit=0disables the Linux audit subsystem. you can live without it.nowatchdogdisables the softlockup and hardlockup detectors.
Stage 2: Replace OpenSSH with Dropbear
Dropbear is a minimal SSH server designed for low-resource systems. It is significantly smaller than OpenSSH and links against far fewer libraries. On a NAT VPS with a single exposed port, the switch must be done atomically: stop sshd and start dropbear in one command or you will lose access, you might anyway, but should be able to get back in fine after.
DEBIAN_FRONTEND=noninteractive apt-get install -y dropbear
Dropbear is now installed but cannot start because OpenSSH holds port 22. Set the empty extra-args variable to suppress a harmless boot warning:
sed -i 's/#DROPBEAR_EXTRA_ARGS=""/DROPBEAR_EXTRA_ARGS=""/' /etc/default/dropbear
Now do the atomic swap. Debian 13 uses socket activation for SSH stopping ssh.service alone does not release port 22 because ssh.socket continues to hold it. Both must be stopped together:
systemctl stop ssh.socket ssh && systemctl start dropbear && systemctl disable ssh ssh.socket
Your session will drop. Reconnect on the same external port as before. Dropbear converts and reuses the existing OpenSSH host keys, so the host fingerprint is unchanged.
Once reconnected, remove OpenSSH:
DEBIAN_FRONTEND=noninteractive apt-get purge -y \
openssh-server openssh-sftp-server openssh-client ssh-import-id
apt-get autoremove -y --purge
Stage 3: Remove Cloud-Init and Python
Cloud-init runs once at first boot to configure the instance. It has already run. It pulls in Python 3 and approximately 40 Python packages. All of it can be removed.
First disable the unattended-upgrades service and its associated timers, which also depend on Python:
systemctl stop unattended-upgrades
systemctl disable unattended-upgrades apt-daily.timer apt-daily-upgrade.timer apt-listchanges.timer man-db.timer dpkg-db-backup.timer
Remove cloud-init, its utilities, unattended-upgrades, and netplan. Netplan was used by cloud-init to generate the systemd-networkd configuration. The generated network config file persists after netplan is removed and will be used again in Stage 5:
DEBIAN_FRONTEND=noninteractive apt-get purge -y cloud-init cloud-guest-utils cloud-image-utils cloud-utils cloud-initramfs-growroot unattended-upgrades apt-listchanges netplan.io python3-netplan reportbug
DEBIAN_FRONTEND=noninteractive apt-get purge -y $(dpkg -l | grep '^ii' | awk '{print $2}' | grep -E '^(python3|python-apt-common|libpython3)')
apt-get autoremove -y --purge
Removing
cloud-initramfs-growroottriggers an automaticupdate-initramfsrun via the package post-remove hook. This is expected. The initramfs will be rebuilt again with optimised settings in Stage 7.
Remove the cloud-init data directory, which persists after the package is removed:
rm -rf /var/lib/cloud
Stage 4: Package Cleanup
Remove packages that serve no purpose on a running headless VPS:
DEBIAN_FRONTEND=noninteractive apt-get purge -y vim vim-runtime vim-tiny man-db groff-base manpages locales libc-l10n sudo qemu-guest-agent qemu-utils polkitd pciutils bind9-host traceroute socat wget ethtool screen apparmor lsb-release
apt-get autoremove -y --purge
apt-get clean
The autoremove step may remove procps as an orphaned dependency. Reinstall it explicitly. ps, free, and kill are probably wanted even when cut to the bone.
apt-get install -y procps
Configure dpkg to suppress docs and man pages on future installs. Without this, any subsequent apt-get install will reinstall them:
cat > /etc/dpkg/dpkg.cfg.d/nodoc << 'EOF'
path-exclude=/usr/share/doc/*
path-include=/usr/share/doc/*/copyright
path-exclude=/usr/share/man/*
path-exclude=/usr/share/groff/*
path-exclude=/usr/share/info/*
path-exclude=/usr/share/lintian/*
EOF
Remove the doc and man page files already installed by previous packages:
find /usr/share/doc -depth -type f ! -name 'copyright' -delete
find /usr/share/doc -depth -empty -type d -delete
rm -rf /usr/share/man /usr/share/groff /usr/share/info /usr/share/lintian
Remove non-English locale files. The locales package was removed above, but locale data installed by glibc and other packages remains. Only the en_US directory is kept:
find /usr/share/locale -mindepth 1 -maxdepth 1 -type d ! -name 'en_US' -exec rm -rf {} +
find /usr/share/locale -mindepth 1 -maxdepth 1 ! -type d -delete
Remove all timezone data except UTC. The system clock is set to UTC at deployment and the hypervisor maintains it:
find /usr/share/zoneinfo -mindepth 1 -maxdepth 1 ! -name 'Etc' -exec rm -rf {} +
find /usr/share/zoneinfo/Etc -mindepth 1 ! -name 'UTC' -delete
Remove the deb-src lines from the apt sources file. Source package lists are never needed on a production server and regenerate as 55MB on every apt-get update if left in place:
sed -i 's/^Types: deb deb-src/Types: deb/' /etc/apt/sources.list.d/debian.sources
Stage 5: Replace systemd-networkd with ifupdown
systemd-networkd runs as a persistent daemon consuming approximately 11MB of RAM. For a server with a static IP that never changes, a traditional /etc/network/interfaces file managed by the lightweight ifupdown package is sufficient and leaves no daemon running after the interface is up.
Fix DNS before removing systemd-resolved. The current /etc/resolv.conf is a symlink to the resolved stub. Replace it with a static file first:
rm /etc/resolv.conf
printf 'nameserver 1.1.1.1\nnameserver 1.0.0.1\n' > /etc/resolv.conf
Remove the resolve NSS module reference from /etc/nsswitch.conf, then remove the package:
sed -i 's/resolve \[!UNAVAIL=return\] //g' /etc/nsswitch.conf
sed -i 's/ resolve//g' /etc/nsswitch.conf
systemctl stop systemd-resolved
DEBIAN_FRONTEND=noninteractive apt-get purge -y systemd-resolved libnss-resolve
Install ifupdown. It pulls in dhcpcd-base as a dependency; remove it immediately since the IP is static:
DEBIAN_FRONTEND=noninteractive apt-get install -y ifupdown
DEBIAN_FRONTEND=noninteractive apt-get purge -y dhcpcd-base
Write the network configuration. Replace the address and gateway with the values you recorded before starting:
cat > /etc/network/interfaces << 'EOF'
auto lo
iface lo inet loopback
auto ens3
iface ens3 inet static
address YOUR_IP/24
gateway YOUR_GATEWAY
EOF
The interface name on TierHive KVM instances is
ens3. If yours differs, use the name shown byip a. The subnet prefix/24is standard for TierHive instances; adjust if your allocation differs on your host.
Disable and mask systemd-networkd, then remove the network configuration directory it managed:
systemctl disable systemd-networkd systemd-networkd.socket systemd-network-generator.service systemctl mask systemd-networkd systemd-networkd.socket systemctl mask systemd-networkd-wait-online.service
rm -rf /etc/systemd/network/
ifupdown's networking.service was enabled automatically when the package was installed. It is a oneshot service that brings up interfaces at boot and exits, leaving no daemon.
Stage 6: Service Cleanup
Disable systemd-timesyncd. On KVM the guest clock is disciplined by the hypervisor via kvm-clock. The hypervisor keeps the clock accurate and timesyncd adds no value:
systemctl disable --now systemd-timesyncd
Mask kernel debug and config filesystems, EFI pstore, binfmt_misc, and timers that have no purpose on a headless VPS:
systemctl mask sys-kernel-config.mount
systemctl mask sys-kernel-debug.mount
systemctl mask sys-kernel-tracing.mount
systemctl mask systemd-pstore.service
systemctl mask proc-sys-fs-binfmt_misc.automount
systemctl mask proc-sys-fs-binfmt_misc.mount
systemctl disable fstrim.timer
systemctl mask uuidd.socket
systemctl disable e2scrub_reap.service e2scrub_all.timer
sys-kernel-config.mount— configfs for USB gadgets and iSCSIsys-kernel-debug.mount/sys-kernel-tracing.mount— kernel debug filesystemssystemd-pstore.service— EFI pstore crash dump collection; theefi_pstoremodule is blacklisted in Stage 7proc-sys-fs-binfmt_misc— binary format handlers for Wine, Java, etc.fstrim.timer— TRIM does not pass through virtual storageuuidd.socket— UUID daemon, not needede2scrub— online ext4 filesystem checks, not needed on a VPS
Remove PAM session tracking. The pam_systemd.so module registers each login session with systemd-logind via dbus. On a root-only dropbear server there is no use for session tracking. Without this change, every SSH login activates dbus, which then auto-activates logind:
sed -i '/pam_systemd\.so/s/^/# /' /etc/pam.d/common-session
Mask systemd-logind. Logind is wired into multi-user.target by the systemd package and starts at every boot. It manages user seats and sessions, neither of which exist on a headless server:
systemctl mask systemd-logind.service
Provide clean reboot and poweroff commands. Masking logind causes the standard reboot binary to print errors when it attempts to notify logind via dbus before falling back to systemd's private socket. The reboot succeeds either way, but the errors are avoidable. Replace the commands with wrappers that go directly to systemd:
cat > /usr/local/sbin/reboot << 'EOF'
#!/bin/sh
exec systemctl reboot --no-wall 2>/dev/null
EOF
chmod +x /usr/local/sbin/reboot
cat > /usr/local/sbin/poweroff << 'EOF'
#!/bin/sh
exec systemctl poweroff --no-wall 2>/dev/null
EOF
chmod +x /usr/local/sbin/poweroff
Stage 7: Kernel Module Blacklist and Initramfs
Set the explicit module list and switch the initramfs from most (load everything) to dep (load only what this hardware needs). The three lines below are a safety net: with MODULES=dep, update-initramfs scans running modules and their dependencies, so virtio_blk and ext4 would be detected automatically. The explicit list ensures they are included even if detection misses them:
cat > /etc/initramfs-tools/modules << 'EOF'
virtio_blk
virtio_net
ext4
EOF
sed -i 's/^MODULES=most/MODULES=dep/' /etc/initramfs-tools/initramfs.conf
Create the module blacklist:
cat > /etc/modprobe.d/blacklist-vps.conf << 'EOF'
# CD/ISO (no optical drive on VPS)
blacklist isofs
blacklist sr_mod
blacklist cdrom
# KVM (not nesting VMs)
blacklist kvm_intel
blacklist kvm
install kvm /bin/true
install kvm_intel /bin/true
# Memory ballooning
blacklist virtio_balloon
# ATA/IDE (using virtio-blk, not ATA)
blacklist ata_piix
blacklist ata_generic
blacklist libata
# Input devices (headless)
blacklist evdev
blacklist button
blacklist serio_raw
# VMware VSOCK stack (not VMware)
blacklist vmw_vmci
blacklist vmw_vsock_vmci_transport
blacklist vmw_vsock_virtio_transport_common
blacklist vsock_loopback
blacklist vsock
install vsock /bin/true
# QEMU firmware config (already booted)
blacklist qemu_fw_cfg
# SCSI generic (no SCSI devices)
blacklist sg
# i6300ESB watchdog
blacklist i6300esb
# Intel RAPL power management (not needed on VPS)
blacklist intel_rapl_msr
blacklist intel_rapl_common
blacklist iosf_mbi
blacklist rapl
# EFI pstore (crash dump storage in EFI vars, not needed)
blacklist efi_pstore
# Binary format handlers (Wine, Java, etc)
blacklist binfmt_misc
# Automount
blacklist autofs4
# Watchdog hardware driver
blacklist watchdog
# configfs (USB gadgets, iSCSI config filesystem)
blacklist configfs
install configfs /bin/true
# T10 DIF SCSI data integrity (no SCSI on this VPS)
blacklist crct10dif_pclmul
EOF
Rebuild the initramfs once with all of the above in place:
update-initramfs -u -k all
Stage 8: System Tuning
Sysctl tuning. The file is named 90- to ensure it loads after systemd's /usr/lib/sysctl.d/50-pid-max.conf, which sets kernel.pid_max = 4194304. A file with a lower prefix would be overridden by it:
mkdir -p /etc/sysctl.d
cat > /etc/sysctl.d/90-minvps.conf << 'EOF'
# Reduce network socket buffers
net.core.rmem_default = 32768
net.core.wmem_default = 32768
net.core.rmem_max = 131072
net.core.wmem_max = 131072
net.core.netdev_max_backlog = 64
net.core.somaxconn = 128
# Reclaim inode and dentry caches more aggressively under memory pressure
vm.vfs_cache_pressure = 500
# Reduce PID table overhead
kernel.pid_max = 4096
# Dirty page writeback thresholds
vm.dirty_background_ratio = 5
vm.dirty_ratio = 10
# Disable watchdog
kernel.watchdog = 0
EOF
Reduce block device read-ahead. The kernel defaults to 8MB of read-ahead on the block device. On virtual storage this is wasted memory. 128KB is sufficient as it only needs to exist, it does nothing this is a legacy thing from days gone by that still exists for physical spinners I guess but it holds some Ram.
echo 128 > /sys/block/vda/queue/read_ahead_kb
cat > /etc/systemd/system/readahead.service << 'EOF'
[Unit]
Description=Set block device read-ahead
After=local-fs.target
[Service]
Type=oneshot
ExecStart=/bin/sh -c 'echo 128 > /sys/block/vda/queue/read_ahead_kb'
RemainAfterExit=yes
[Install]
WantedBy=multi-user.target
EOF
systemctl enable readahead.service
Replace systemd-journald with busybox syslogd. journald runs as a persistent 8MB process. Busybox syslogd with a 64KB in-memory circular buffer does the same job for a minimal server at a fraction of the cost. Logs are accessed with busybox logread:
DEBIAN_FRONTEND=noninteractive apt-get install -y busybox
cat > /etc/systemd/system/syslogd.service << 'EOF'
[Unit]
Description=Busybox syslogd
DefaultDependencies=false
After=systemd-tmpfiles-setup.service
Before=sysinit.target
[Service]
Type=simple
ExecStart=/bin/busybox syslogd -n -C64
Restart=on-failure
[Install]
WantedBy=multi-user.target
EOF
systemctl enable syslogd.service
systemctl mask systemd-journald.service
systemctl mask systemd-journald.socket
systemctl mask systemd-journald-dev-log.socket
systemctl mask systemd-journald-audit.socket 2>/dev/null
journalctlno longer works after this step. Usebusybox logreadto view logs andbusybox logread -fto follow them.systemctl statuscontinues to work for checking individual service states as it uses systemd's own data, not the journal.
Final Cleanup
All package operations are now complete. Clear the apt package lists and cache binaries. They consume around 150MB and are not needed until the next time packages are installed, at which point apt-get update will regenerate them:
rm -rf /var/lib/apt/lists/* /var/cache/apt/*.bin
apt-get clean
Reboot
reboot
After
total used free shared buff/cache available
Mem: 213 38 141 0 41 174
Swap: 0 0 0
Filesystem Size Used Avail Use% Mounted on
/dev/vda1 2.8G 275M 2.4G 11% /
RAM down from 88MB to 38MB. Disk down from 832MB to 275MB, meaning total ram use for kernel and userspace is 82mb so this now fits into 128mb ram, if your provider supports that, you can now downgrade and save some money
but realistically, if you want to run web based services, you will probably want to stay on 256mb
What Is Still Running
The running userspace processes after boot are systemd (PID 1, 13MB), systemd-udevd (9.5MB), dbus-daemon (2MB), busybox syslogd in circular buffer mode (2MB), dropbear (3MB), and two getty processes: one on ttyS0 for serial console access and one on tty1 for the browser-based console panel.
dbus-daemon starts at boot via socket activation and runs persistently. The PAM change in Stage 6 prevents SSH logins from activating systemd-logind through it, but dbus itself is socket-activated early in the boot sequence and stays running.
The loaded kernel modules after reboot are: the virtio stack (virtio_blk, virtio_net, virtio_rng), the EFI partition (vfat, fat, nls_ascii, nls_cp437), hardware AES acceleration (aesni_intel, gf128mul, crypto_simd, cryptd, ghash_clmulni_intel), SHA acceleration (sha256_ssse3, sha512_ssse3, sha1_ssse3), CRC (crc32c_intel, crc32_pclmul), netfilter (ip_tables, x_tables, nfnetlink), and network failover (net_failover, failover).
Also posted on the tierhive blog: https://tierhive.com/blog/tierhive-howto/debian-13-minimal-guide-reduce-ram-to-38mb-and-disk-to-275mb
I will add making WordPress fit in this VPS guide next.
TierHive - Hourly VPS - NAT Native - /24 per customer - Lab in the cloud - Free to try. | I am Anthony Smith
FREE tokens when you sign up, try before you buy. | Join us on Reddit
Comments
Nice... Systemd is bloatware
