Provisioning a server
This guide will walk you through provisioning a virtual machine for the purpose of running nocloud.
Configuring a VPS (optional)
You may acquire a virtual machine from a virtual private server provider such as OVH (cheap, unaffiliated). The following instructions pertain to this provider but should generalize to others.
On 2026-02-16 we paid 136.01 PLN for 6 months of VPS-1.
On 2026-07-22 we paid 328.08 PLN for additional 12 months of VPS-1.
Go to https://www.ovhcloud.com/en/vps/configurator/, select a variant (the smallest one is ok for a start), the commitment period and location. We recommend choosing a location physically close to you.
For the image we recommend one that uses UEFI boot. In the case of OVH, FreeBSD-XX-zfs - UEFI satisfies this requirement. The actual operating system doesn’t matter because we’ll replace it later with Alpine Linux, a distribution we recommend for deploying nocloud. This is because this provider doesn’t offer Alpine images.
Pass on additional storage for now. This guide will use the storage that is included with the VPS variant, with the possibility of future extension.
Replacing the OS
We’ll now replace the OS with Alpine Linux. Reboot your VPS in rescue mode (Your VPS > Boot > … > Reboot in rescue mode > Confirm), You’ll get an email message with the link to generate the rescue mode root password. Follow it, set the password and log into the machine while performing local forwarding of port 5900 (VNC).
ssh -L 5900:localhost:5900 root@XXX.XXX.XXX.XXX
Execute the following commands in rescue mode.
apt update
apt install -y qemu-system-x86 ovmf
cd /dev/shm
Download the newest release of the Alpine Linux extended image. As of wiriting this guide it is 3.23.4.
wget 'https://dl-cdn.alpinelinux.org/alpine/v3.23/releases/x86_64/alpine-extended-3.23.4-x86_64.iso'
Identify the main virtual disk (here: /dev/sdb). Create a 300 MiB boot partition and another one until the end of the disk.
fdisk /dev/sdb
# g n +300M n t 1 1 w
Run the Alpine image with qemu.
modprobe kvm
qemu-system-x86_64 -netdev type=user,id=mynet0,hostfwd=tcp::22222-:22 -device virtio-net-pci,netdev=mynet0 -m 4G -enable-kvm -drive file=/dev/sdb,format=raw,if=virtio -vga std -k en-gb -daemonize -cdrom alpine-extended-3.23.3-x86_64.iso -boot d -vnc localhost:0 -bios /usr/share/ovmf/OVMF.fd
Connect to the inner virtual machine from your PC using VNC and the previously set up port forwarding. E.g. with remmina:
remmina -c vnc://localhost
Installing Alpine on ZFS
Log in as root without a password. Run setup-alpine until it asks about disks to use, then exit using Ctrl+c. Then:
setup-devd udev
apk add e2fsprogs zfs
modprobe zfs
We’ll use ZFS. One dataset for the root filesystem and then one dataset for each nocloud user. First, let’s set up the pool. Replace PARTUUID with an appropriate name from ls -l /dev/disk/by-partuuid. You will be prompted for the disk encryption password.
zpool create -f -o ashift=12 \
-O acltype=posixacl \
-O relatime=on \
-O xattr=sa \
-O dnodesize=auto \
-O normalization=formD \
-O mountpoint=none \
-O canmount=off \
-O devices=off \
-R /mnt \
-O compression=lz4 \
-O encryption=aes-256-gcm \
-O keyformat=passphrase \
-O keylocation=prompt \
sys /dev/disk/by-partuuid/PARTUUID
Now we’ll create and mount the root and boot filesystems.
zfs create -o mountpoint=/ -o canmount=noauto sys/root
mount -t zfs sys/root /mnt
mkfs.vfat /dev/vda1
mkdir /mnt/boot
mount -t vfat /dev/vda1 /mnt/boot
Install the OS in the location where the filesystems are mounted.
setup-disk /mnt
Enter the root of the new OS and configure ZFS services to run on system startup.
chroot /mnt
rc-update add zfs-import sysinit
rc-update add zfs-mount sysinit
While you are in the chroot, add GRUB_TERMINAL=console to /etc/default/grub, Then, run grub-mkconfig -o /boot/grub/grub.cfg. This fixes a problem with the display while entering the encryption password.
Finally reboot the VM. If you were using a VPS provider’s rescue mode, switch back to normal mode now. On VM startup you’ll be prompted to enter the disk encryption password. You’ll have to use remote access to the VM’s console for that.
Firewall
Uncomment community in /etc/apk/repositories.
Install nftables. Allow SSH traffic.
apk add nftables nftables-rulesets
ln -s /usr/share/nftables.avail/50_sshd.nft /etc/nftables.d/
Allow HTTP(S) traffic. Create /etc/nftables.d/50_http.nft with the following contents.
table inet filter {
chain input {
tcp dport {80,443} accept comment "accept HTTP(S)"
}
}
Start nftables, enable them on startup.
doas rc-service nftables start
rc-update add nftables boot
Monitoring
Install prometheus, alertmanager and a few exporters.
doas apk add prometheus alertmanager prometheus-node-exporter prometheus-openrc-exporter
These services listen on all interfaces by default. We’ll make them listen on localhost only.
- Add
--web.listen-address=localhost:9090toprometheus_argsin/etc/conf.d/prometheus. - Add
--web.listen-address=localhost:9093 --cluster.listen-address=toalertmanager_argsin/etc/conf.d/alertmanager. - Add
--web.listen-address=localhost:9100toARGSin/etc/conf.d/node-exporter. - Add
-listen-address 'localhost:9816'tocommand_argsin/etc/conf.d/openrc-exporter.
You may now add supervisor=supervise-daemon to these files in /etc/conf.d that correspond to services you want to monitor with OpenRC exporter. Stop the service, edit the file, start the service.
Set the desired metric retention time and maximum size in /etc/conf.d/prometheus.
Configure prometheus targets in /etc/prometheus/prometheus.yml. Example:
global:
alerting:
alertmanagers:
- static_configs:
- targets:
- localhost:9093
rule_files:
- "/etc/prometheus/rules/*.yml"
- "/etc/prometheus/rules/*.yaml"
scrape_configs:
- job_name: "prometheus"
static_configs:
- targets: ["localhost:9090"]
- job_name: "node"
static_configs:
- targets: ["localhost:9100"]
- job_name: "openrc"
static_configs:
- targets: ["localhost:9816"]
Add your preferred alerting rules to /etc/prometheus/rules/. We recommend:
- node rues,
- OpenRC rules,
- and the following ZFS rules.
groups:
- name: custom
rules:
- alert: NodeZFSPoolDegraded
annotations:
description: "ZFS pool {{ $labels.zpool }}, at {{ $labels.instance }} is degraded."
summary: "ZFS pool is degraded."
expr: 'node_zfs_zpool_state{job="node", state="degraded"} > 0'
labels:
severity: critical
- alert: NodeZFSPoolFaulted
annotations:
description: "ZFS pool {{ $labels.zpool }}, at {{ $labels.instance }} is faulted."
summary: "ZFS pool is faulted."
expr: 'node_zfs_zpool_state{job="node", state="faulted"} > 0'
labels:
severity: critical
- alert: NodeZFSPoolUnavailable
annotations:
description: "ZFS pool {{ $labels.zpool }}, at {{ $labels.instance }} is unavailable."
summary: "ZFS pool is unavailable."
expr: 'node_zfs_zpool_state{job="node", state="unavail"} > 0'
labels:
severity: critical
Configure a receiver in /etc/alertmanager.yml. For example (SMTP):
global:
smtp_from: ops@example.com
smtp_smarthost: smtp.example.com:587
smtp_hello: nocloud
smtp_auth_username: ops@example.com
smtp_auth_password: hunter2
smtp_require_tls: true
route:
receiver: 'email'
group_by: ['alertname']
group_wait: 3m
group_interval: 10m
repeat_interval: 1h
receivers:
- name: email
email_configs:
- to: me@example.com
inhibit_rules:
- source_match:
severity: 'critical'
target_match:
severity: 'warning'
equal: ['alertname', 'dev', 'instance']
Now, start and add the prometheus services to startup.
rc-service prometheus start
rc-update add prometheus
rc-service alertmanager start
rc-update add alertmanager
rc-service node-exporter start
rc-update add node-exporter
rc-service openrc-exporter start
rc-update add openrc-exporter
You may now proceed to deploying nocloud.