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.

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:

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.