This guide provisions a new two-drive Debian server so the operating system can boot from either drive while the remaining disk space is used as independent, unmirrored storage for replicated services.
Destructive, new-server-only procedure. Hetzner Installimage completely wipes every selected
DRIVE. Latersgdisk,pvcreate,lvcreate, andmkfscommands also change storage. Do not run any command from this guide on an existing server. Use it only on a new or explicitly disposable server after verifying both drive serial numbers.
Read the hybrid storage design before using this guide. The design records the accepted failure contract: the host remains bootable after either drive fails, but services whose unmirrored data was on that drive may remain stopped while replicas on other servers provide fleet availability.
This is the one procedure in Cloudfall that a human runs by hand and that no agent should ever run. Cloudfall is the operator's record for a fleet run by an AI agent, and a record only makes sense for operations that are declared, gated and verifiable; wiping two drives on a machine that is not yet in any inventory is none of those. There is no Cloudfall command, no MCP tool and no operator policy that reaches this layer, by construction. What the guide does instead is get the server to the point where the record can start: step 11 registers it and takes the first observation, and step 12 is the verify step for everything before it. Keep the installation record and the failure-test evidence; they are the first entries about this host, written by you.
Bare-metal RAID and storage provisioning is the one implemented layer not yet exercised in a live proving run (see the roadmap).
Record these values before opening the Rescue System:
Server name:
Provider server ID:
IPv4 and IPv6 allocation:
Debian version:
Boot mode: BIOS or UEFI
Drive 0 device:
Drive 0 model:
Drive 0 serial:
Drive 0 reported size:
Drive 1 device:
Drive 1 model:
Drive 1 serial:
Drive 1 reported size:
Swap size:
/boot size:
/ size:
Data VG 0 allocations:
Data VG 1 allocations:
Replica hosts for every data service:
Backup and restore procedure:
Use two equal drives for the standard layout. If the sizes differ, RAID1 and paired partition creation are constrained by the smaller drive; stop and make a server-specific plan rather than copying the example sizes.
For two nominal 512 GB drives, the default starting point is 16 GiB swap, 1 GiB
/boot, and 96 GiB /. Increase root before installation when workload
evidence requires it. Keep /var, /var/log, /tmp, /home, and /srv/apps
inside mirrored root.
Activate Hetzner's Linux Rescue System, reboot into it, and connect as root. Identify the disks by device path and serial:
lsblk -d -o NAME,PATH,SIZE,MODEL,SERIAL,ROTA,TRAN
ls -l /dev/disk/by-id/
smartctl -x /dev/nvme0n1
smartctl -x /dev/nvme1n1Stop if either device is absent, the serials do not match the installation record, SMART reports a failed health assessment, or an NVMe critical warning is nonzero. Replace unhealthy new hardware before installing.
The commands below assume the verified devices are /dev/nvme0n1 and
/dev/nvme1n1. Substitute the actual device names; do not infer disk identity
from the order alone.
Start the provider installer:
installimageSelect the supported Debian image. In the generated configuration, retain the selected image path and provider-generated boot settings, and set the relevant storage lines to this pattern:
DRIVE1 /dev/nvme0n1
DRIVE2 /dev/nvme1n1
SWRAID 1
SWRAIDLEVEL 1
BOOTLOADER grub
HOSTNAME new-host.example.net
PART swap swap 16G
PART /boot ext4 1G
PART / ext4 96G
Important checks in the editor:
- both verified physical drives are active
DRIVEentries; - software RAID is enabled at level 1;
- there is no
PART ... allline; - no LVM entry consumes the free tail;
- the hostname, Debian image, network settings, and SSH key are correct; and
- any provider-generated BIOS/UEFI boot partition settings are preserved.
Hetzner applies SWRAID to all selected drives and documents that omitting an
all partition leaves the remaining space unused. Save the configuration only
after calculating that the explicit system partitions fit on both disks.
Installimage validates the file and then wipes and installs the selected
drives.
Reboot into Debian when installation completes.
Run the following checks as root:
cat /proc/mdstat
mdadm --detail --scan
lsblk -o NAME,PATH,TYPE,SIZE,FSTYPE,FSVER,MOUNTPOINTS
findmnt --verify --verbose
findmnt /
findmnt /boot
swapon --showDo not continue unless:
- every system MD array reports RAID1 with both members active, normally
displayed as
[UU]in/proc/mdstat; - no resync, recovery, or reshape is incomplete;
/and/bootare mounted from MD-backed filesystems;- swap is active on the intended mirrored device; and
findmnt --verifyreports no errors or filesystem-type mismatches.
Confirm bootloader coverage. On a BIOS/GRUB installation, the Debian selection should include both disks:
debconf-show grub-pc | grep 'grub-pc/install_devices'UEFI installations use EFI system partitions and firmware boot entries rather
than the same grub-pc selection. Inspect the generated layout and entries:
findmnt /boot/efi
efibootmgr -v
lsblk -o NAME,PATH,PARTTYPE,FSTYPE,MOUNTPOINTSDo not assume that seeing two MD members proves that either physical disk can boot. The controlled failure test in step 12 is the acceptance test: a layout that looks right is not an outcome, a server that boots on either drive is.
Install the storage and monitoring tools if the selected image does not already provide them:
apt-get update
apt-get install --yes gdisk lvm2 mdadm smartmontoolsFirst display all free regions. The final free region on each disk should be approximately equal:
parted /dev/nvme0n1 unit GiB print free
parted /dev/nvme1n1 unit GiB print free
sgdisk --print /dev/nvme0n1
sgdisk --print /dev/nvme1n1Stop if the layout differs between disks, the free region overlaps an existing partition, or its size does not match the capacity plan.
Create one partition in the largest free region of each disk:
sgdisk --largest-new=0 /dev/nvme0n1
sgdisk --largest-new=0 /dev/nvme1n1
partprobe /dev/nvme0n1
partprobe /dev/nvme1n1
udevadm settleNow inspect the result and record the new partition numbers:
lsblk -o NAME,PATH,SIZE,TYPE,FSTYPE,PARTLABEL,MOUNTPOINTS
sgdisk --print /dev/nvme0n1
sgdisk --print /dev/nvme1n1The rest of this guide uses /dev/nvme0n1pN and /dev/nvme1n1pN as
placeholders. Replace each N with the new partition number shown on that
specific disk. Never copy an existing RAID-member partition number.
Optionally set the GPT type and labels after substituting the verified numbers:
sgdisk --typecode=N:8e00 --change-name=N:cloudfall-data0 /dev/nvme0n1
sgdisk --typecode=N:8e00 --change-name=N:cloudfall-data1 /dev/nvme1n1
partprobe /dev/nvme0n1
partprobe /dev/nvme1n1
udevadm settleConfirm once more that the two selected partitions are empty, unmounted, and are not MD members:
lsblk -f /dev/nvme0n1 /dev/nvme1n1
blkid /dev/nvme0n1pN /dev/nvme1n1pN
cat /proc/mdstatblkid should return no existing filesystem signature for the new partitions.
If it reports a signature, stop and investigate rather than erasing it.
Create one physical volume and one volume group per drive:
pvcreate /dev/nvme0n1pN
pvcreate /dev/nvme1n1pN
vgcreate vg_data0 /dev/nvme0n1pN
vgcreate vg_data1 /dev/nvme1n1pN
pvs -o pv_name,pv_uuid,vg_name,pv_size,pv_free
vgs -o vg_name,pv_count,lv_count,vg_size,vg_freeEach VG must show exactly one PV. Do not add the second drive's partition to the first VG.
Use the completed allocation worksheet, not the illustrative sizes below. Keep 10–15% of each VG free. This example separates workloads across the drives:
lvcreate --size 120G --name lv_mysql vg_data0
lvcreate --size 180G --name lv_clickhouse vg_data0
lvcreate --size 240G --name lv_elasticsearch vg_data1Verify allocation before formatting:
lvs -o lv_name,vg_name,lv_size,devices
vgs -o vg_name,vg_size,vg_freeFormat only the new data LVs. -m 0 removes ext4's root-reserved percentage,
which is appropriate for dedicated application-data volumes but not for the
root filesystem:
mkfs.ext4 -L mysql-data -m 0 /dev/vg_data0/lv_mysql
mkfs.ext4 -L clickhouse-data -m 0 /dev/vg_data0/lv_clickhouse
mkfs.ext4 -L elasticsearch-data -m 0 /dev/vg_data1/lv_elasticsearchCreate the intended mount points:
install -d -o mysql -g mysql -m 0750 /srv/storage/mysql
install -d -o clickhouse -g clickhouse -m 0750 /srv/storage/clickhouse
install -d -o elasticsearch -g elasticsearch -m 0750 /srv/storage/elasticsearchInstall the service packages first if their users do not yet exist. Adjust owners and modes to the actual service contract.
Record the filesystem UUIDs:
blkid /dev/vg_data0/lv_mysql
blkid /dev/vg_data0/lv_clickhouse
blkid /dev/vg_data1/lv_elasticsearchAdd one line per data filesystem to /etc/fstab, substituting the UUIDs:
UUID=<mysql-uuid> /srv/storage/mysql ext4 defaults,nofail,x-systemd.device-timeout=10s 0 2
UUID=<clickhouse-uuid> /srv/storage/clickhouse ext4 defaults,nofail,x-systemd.device-timeout=10s 0 2
UUID=<elasticsearch-uuid> /srv/storage/elasticsearch ext4 defaults,nofail,x-systemd.device-timeout=10s 0 2
Use filesystem UUIDs, not /dev/nvme*, /dev/dm-*, or assumed LV minor
numbers. Keep system entries conventional: root uses pass number 1, other
mirrored filesystems use 2, and swap uses 0.
Validate before mounting:
findmnt --verify --verbose
systemctl daemon-reload
mount /srv/storage/mysql
mount /srv/storage/clickhouse
mount /srv/storage/elasticsearch
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS /srv/storage/mysql /srv/storage/clickhouse /srv/storage/elasticsearch
df -hT / /boot /srv/storage/mysql /srv/storage/clickhouse /srv/storage/elasticsearchThe nofail option allows boot to complete if a data drive is absent. It does
not by itself protect a service from writing into the empty directory beneath
the missing mount; step 9 is mandatory.
Configure each application through its supported configuration so its actual
data directory is the corresponding /srv/storage/... path. Do this before
the service creates production data. Then add a systemd drop-in.
MariaDB example:
systemctl edit mariadb.service[Unit]
RequiresMountsFor=/srv/storage/mysqlClickHouse example:
systemctl edit clickhouse-server.service[Unit]
RequiresMountsFor=/srv/storage/clickhouseElasticsearch example:
systemctl edit elasticsearch.service[Unit]
RequiresMountsFor=/srv/storage/elasticsearchReload and inspect the effective relationships:
systemctl daemon-reload
systemctl show mariadb.service -p RequiresMountsFor
systemctl show clickhouse-server.service -p RequiresMountsFor
systemctl show elasticsearch.service -p RequiresMountsFor
systemctl list-dependencies mariadb.serviceRepeat this for every service backed by an unmirrored mount. Verify the real
unit name with systemctl list-unit-files; package names and units can differ
by Debian release or vendor repository.
The expected behavior is now:
- the server boots when a data drive is missing because the mount uses
nofail; and - the affected service fails to start because its required mount cannot be established.
Verify that /etc/mdadm/mdadm.conf contains current ARRAY definitions:
mdadm --detail --scan
grep -E '^(ARRAY|MAILADDR|MAILFROM)' /etc/mdadm/mdadm.confAdd or update MAILADDR only when mail sent by root is delivered to an
actionable destination. On Debian, ensure automatic checks are enabled:
grep '^AUTOCHECK=' /etc/default/mdadmThe required result is:
AUTOCHECK=true
After changing MD or boot configuration, rebuild early-boot and GRUB metadata:
update-initramfs -u -k all
update-grubCheck the distribution's RAID check schedule rather than assuming one exists:
systemctl list-timers --all | grep -E 'mdcheck|mdadm'
grep -R 'checkarray\|sync_action' /etc/cron.d/mdadm /etc/cron.* 2>/dev/nullEnable periodic SSD trimming and verify SMART for both physical drives:
systemctl enable --now fstrim.timer
systemctl status fstrim.timer --no-pager
smartctl -x /dev/nvme0n1
smartctl -x /dev/nvme1n1This is where the record starts. Everything above happened outside Cloudfall; from here the host is declared, observed and audited like any other, and every later change to it goes through a gated operation with a receipt.
Add the new Server only after the operating system and SSH access are stable.
Create a dedicated hybrid-storage ServerType; do not reuse
debian-application if its full-disk RAID and root thresholds do not match this
layout.
The following is a starting profile fragment for a 96 GiB root. Adjust mounts, packages, services, ownership, and capacity thresholds to the actual server:
---
apiVersion: cloudfall/v1
kind: ServerType
metadata:
id: debian-hybrid-storage
description: Two-drive RAID1 system with independent replicated-data tails
spec:
os:
distribution: Debian
versions:
- "13"
serviceManager: systemd
storage:
softwareRaid:
level: raid1
minimumActiveDevices: 2
minimumUsableBytes: 90000000000
mounts:
- path: /
filesystem: ext4
minimumBytes: 90000000000
- path: /srv/storage/mysql
filesystem: ext4
minimumBytes: 110000000000
packages:
required:
- name: mdadm
- name: lvm2
- name: smartmontools
forbidden: []
services:
required:
- name: ssh.service
state: running
status: enabled
configuration:
files:
- path: /etc/fstab
capture: hash
owner: root
group: root
mode: "0644"
- path: /etc/mdadm/mdadm.conf
capture: hash
owner: root
group: root
mode: "0644"In Cloudfall v1, minimumUsableBytes refers in practice to the largest observed MD
device rather than total capacity across all arrays. A 90 GB threshold is
therefore appropriate for the example 96 GiB root array; do not enter the sum
of swap, boot, and root, or the total capacity of both drives.
Validate, render, inspect, and audit:
task validate CONFIG_DIR=../my-project
task inventory CONFIG_DIR=../my-project
task inspect CONFIG_DIR=../my-project
task audit CONFIG_DIR=../my-projecttask inspect also rebuilds the local dashboard. Review the generated
tmp/dashboard/index.html, storage observations, MD state, SMART evidence,
mount capacities, and service results.
Cloudfall v1 does not yet prove that every MD array has two healthy members, parse
nofail, verify physical-to-VG placement, or inspect RequiresMountsFor=.
Complete the manual checklist below even when task audit reports compliance.
The audit says the host matches what was declared; it does not yet say the
storage contract holds, and Cloudfall does not report what it cannot prove.
Under the brownfield design, where the team's own
Ansible inventory is the config, this step becomes adding the host to their
inventory with a cloudfall block naming its server type; the observation
and audit are the same. Not built yet.
Run this test before production data is admitted, while rebuilding the server is still acceptable. Confirm replicas and backups first. Do not use any current production server for this drill.
This is the verify step for the whole guide. Cloudfall's rule that a run is
not an outcome until it is verified applies to hand work too: the layout is
not done because every command exited zero, it is done because the server
booted with each drive missing and came back to [UU]. Save the output of
the checks below from each boot next to the installation record; it is the
evidence that this host's failure contract was tested, and nothing in
Cloudfall can produce it later.
Use Hetzner's console/rescue capabilities or a provider-coordinated method to make one complete physical drive unavailable. A software-only MD member failure does not test firmware, bootloader, EFI, controller, and whole-disk behavior.
Test one drive at a time:
- Record healthy baseline evidence and stop the server cleanly.
- Make drive 0 unavailable and boot normally.
- Confirm SSH access, mirrored root, degraded-but-active MD arrays, absent data mounts from drive 0, failed/inactive dependent services, healthy unrelated mounts and services, and healthy remote cluster replicas.
- Restore drive 0, allow all MD arrays to resynchronize, and require
[UU]. - Repeat with drive 1 unavailable.
- Restore drive 1 and require a completely healthy final state.
Use these read-only checks after each boot:
cat /proc/mdstat
mdadm --detail --scan
lsblk -o NAME,PATH,SIZE,TYPE,FSTYPE,MOUNTPOINTS
findmnt -o TARGET,SOURCE,FSTYPE,OPTIONS
systemctl --failed
systemctl status mariadb.service clickhouse-server.service elasticsearch.service --no-pager
smartctl -x /dev/nvme0n1
smartctl -x /dev/nvme1n1An unavailable drive will make its smartctl command fail; that is expected.
Do not continue to production if either drive cannot boot the server, a missing
data volume blocks boot, an affected service starts on an unmounted directory,
or loss of the host/replica breaks cluster quorum.
- The installation record contains both model/serial pairs and the final partition/VG/LV/mount mapping.
-
/proc/mdstatshows every system array healthy with both members active. - Root, boot, system logs, temporary files, home directories, and
/srv/appsare on mirrored storage. - Each data tail is a separate one-PV VG.
- No RAID0, LVM stripe, or cross-drive VG exists.
- Every data filesystem uses a UUID in
/etc/fstabwithnofailand a finite device timeout. -
findmnt --verify --verbosereports no errors or type mismatches. - Every storage service has
RequiresMountsFor=for its real data mount. - Every dataset has replicas on other servers in different failure domains, with single-host loss preserving quorum.
- Backups and a restore procedure exist independently of replication.
- MD checks, SMART monitoring, filesystem usage alerts, and
fstrim.timerare active. - Both one-drive boot tests passed and the arrays returned to
[UU]. - Cloudfall config validates, inspection is current, audit results were reviewed, and the operations dashboard has no unexplained critical task.
- The installation record and the failure-test output are kept with the project as the first evidence about this host.
Confirm the data line uses the filesystem UUID and includes both nofail and a
finite x-systemd.device-timeout=. Run findmnt --verify --verbose and
systemctl status <escaped-path>.mount to see the generated mount-unit error.
Stop the service immediately. Verify that the application data path and
RequiresMountsFor= point to the same absolute mount path, then inspect the
effective unit with systemctl cat <service> and systemctl show <service> -p RequiresMountsFor.
Use blkid or lsblk -f to identify the real on-disk filesystem. Correct the
fstab type to match it. Do not relabel an ext3 filesystem as ext4 without a
separate, tested conversion plan.
Inspect the specific array with mdadm --detail /dev/mdX and both drives with
SMART. A running resync may only need to finish; a missing, faulty, or rejected
member must be corrected before any data-volume work or production activation.
Stop before production. On BIOS systems, correct the grub-pc install-device
selection and reinstall GRUB to both verified whole disks. On UEFI systems,
verify the EFI system partitions, files, firmware entries, and provider boot
behavior. In either mode, repeat the physical one-drive boot tests.