Building FreeBSD OVH VPS
This is based on 2016-11-06 Tim Chase blog page Installing FreeBSD on OVH. The method below is not using bsdinstall
, but instead is based on bootstraping FreeBSD ZFS image from running system with make install(kernel,world,distribution)
. So it assumes that your FreeBSD world is already built and ready to be deployed from /usr/src
and /usr/obj
, in case it’s not, prepare it with Step 0
below.
Step 0 - Optional, building FreeBSD world
Create /etc/make.conf
:
1
2
MK_PROFILE=no
WRKDIRPREFIX=/usr/obj
To build FreeBSD world from sources in /usr/src
into /usr/obj
, follow these steps, you will need git-lite
package for this:
1
2
3
4
5
6
7
8
9
10
11
12
# make sure /usr/src and /usr/obj exist, best to have these as separate zfs datasets, so zfs create..
# first time only, if no sources yet, do
# git clone -b releng/14.2 https://git.freebsd.org/src.git /usr/src/
cd /usr/src
git pull -v
# if building FreeBSD 14.2 world
git checkout origin/releng/14.2
# use all cpu cores, will take few hours anyway, depending on your CPU, better do this in tmux
export NCPU=$(sysctl -n hw.ncpu)
make -j$NCPU buildworld && make -j$NCPU buildkernel
Step 1 - Prepare OVH VPS
I will target 5€/month OVH VLE-2 VPS, which comes with 40G disk, 2GB RAM and 2vcores. So, start by installing Linux OS from OVH VPS Dashboard, I tested Debian and CentOS, does not really matter, we will overwrite it with FreeBSD ZFS image. When installation is done, reboot VPS in recovery mode. OVH will send you an email with ssh access details, IP and password. Log-in, and type lsblk
, there should be a block device sdb
of size 40G. This is our dd
output target in last Step 4
.
Step 2 - Prepare FreeBSD configuration for VPS
Here are my FreeBSD configuration files for VPS that I keep in folder /root/ovh
on my main FreeBSD host:
1
2
3
4
5
6
7
8
9
10
# ls -al /root/ovh/
total 48
-rw-r--r-- 1 root wheel 78 Jul 21 2023 fstab
-rwxr-xr-x 1 root wheel 2772 Feb 18 12:53 go2.sh
-rw-r--r-- 1 root wheel 107 Jul 21 2023 loader.conf
-r--r--r-- 1 root wheel 2945 Nov 13 2022 localtime
-rw-r--r-- 1 root wheel 370 Jul 21 2023 rc.conf
-rw-r--r-- 1 root wheel 25 Nov 13 2022 resolv.conf
-rw-r--r-- 1 root wheel 3843 Nov 13 2022 sshd_config
-rwx------ 1 root wheel 2347 Feb 18 13:16 vps_image_build.sh
Let’s examine them:
fstab
1
2
# Device Mountpoint FStype Options Dump Pass#
/dev/da0p2 none swap sw 0 0
go2.sh
- shell script to create user account with wheel
membership, add SSH access keys, and install some packages (I use doas
, not sudo), also prepare /usr/local/etc/doas.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
#!/bin/sh
make_user() {
NAME=$1
ID=$2
KEY=$3
OUT=`pw usershow $NAME 2>/dev/null`
if [ $? != 0 ]; then
echo "creating user $NAME"
pw groupadd -n $NAME -g $ID
pw useradd -n $NAME -u $ID -m -w no -M 750 -G wheel
fi
# always init .ssh dir and authkeys, even when user exists
mkdir -p /home/$NAME/.ssh
chmod 700 /home/$NAME/.ssh
chown -R $NAME:$NAME /home/$NAME/.ssh
printf "$KEY" > /home/$NAME/.ssh/authorized_keys
}
boot_repo() { # install some packages
ASSUME_ALWAYS_YES=true pkg install doas bash joe node_exporter python311
}
prep_envi() {
mkdir -p /usr/local/etc
echo "
# Permit members of the wheel group to perform actions as root.
permit :wheel
permit nopass user1 as root
" >/usr/local/etc/doas.conf
chmod 600 /usr/local/etc/doas.conf
}
set "user1" "10001" "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDrlq+MKiQxeiPQFaX2jXLtoRnbuWDveVg0uvuoCkFgZ key1@host1\nssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIGxgZjma6sFSNb/u1YlaeiIiNnmUSgb8UA1NKj9w2e9l key2@host2\n"
make_user "$@"
boot_repo
prep_envi
loader.conf
1
2
3
4
kern.geom.label.disk_ident.enable="0"
kern.geom.label.gptid.enable="0"
cryptodev_load="YES"
zfs_load="YES"
localtime
- copy this from my main host
1
cp /etc/localtime /root/ovh/
rc.conf
- main OS startup configuration
1
2
3
4
5
6
7
8
9
10
11
12
13
hostname=ovh-vps.uoga.net
zfs_enable="YES"
ifconfig_vtnet0=DHCP
dumpdev="AUTO"
ntpd_enable="YES"
sshd_enable="YES"
local_unbound_enable="YES"
local_unbound_tls="YES"
#pf_enable="YES"
#pflog_enable="YES"
#wireguard_enable="YES"
#wireguard_interfaces="wg0"
resolv.conf
- resolver configuration, OVH nameserver by default
1
nameserver 213.186.33.99
sshd_config
- optional, same as on my main host, so
1
cp /etc/ssh/sshd_config /root/ovh/
but then I add following part to the end of the file /root/ovh/sshd_config
1
2
3
4
5
6
7
8
9
10
KexAlgorithms curve25519-sha256@libssh.org,diffie-hellman-group-exchange-sha256
Protocol 2
HostKey /etc/ssh/ssh_host_ed25519_key
PasswordAuthentication no
ChallengeResponseAuthentication no
PubkeyAuthentication yes
PrintMotd no
VersionAddendum AIX
X11Forwarding no
vps_image_build.sh
- script to build VPS disk image. If you use memory disks, adjust the memorydisk number in mdconfig below to the first available on your host, I don’t use any, so I know mdconfig will create it as md0
. The script will generate disk image, chroot into it to prompt for initial root password, then run user bootstrap script chrooted as /root/go2.sh
to install some packages and your user account with SSH keys.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
#!/bin/sh
mort() {
echo "mort.." && exit
}
# variable containing ZFS pool name for OVH VPS, must be different from existing zpools on this host, or zpool create below will fail
export OVH_ZFS_POOL=rpoolovh
# folder with local assets, configuration files to include in ZFS image
export OVH_FOLDER=/root/ovh
# OVH VPS dd image file with ZFS partition
export OVH_IMAGE=/root/ovh/ovh.img
# OVH VPS image size, 2025 OVH VPS VLE-2 disk size is 40G
export OVH_DISK_SIZE=40G
# swap partition size, here 2G, so ZFS usable size will be 40G-2G=~38G
export OVH_SWAP_SIZE=2G
truncate -s $OVH_DISK_SIZE $OVH_IMAGE || mort
mdconfig -l || mort
mdconfig -f $OVH_IMAGE -u 0 || mort
mdconfig -l || mort
gpart create -s gpt md0 || mort
gpart add -t freebsd-boot -s 512K -i 1 md0 || mort
gpart add -t freebsd-swap -s $OVH_SWAP_SIZE -i 2 md0 || mort
gpart add -t freebsd-zfs -i 3 md0 || mort
gpart show md0 || mort
gpart bootcode -b /boot/pmbr -p /boot/gptzfsboot -i 1 md0 || mort
zpool create -R /mnt -O canmount=off -O mountpoint=/ -O atime=off -O compression=on ${OVH_ZFS_POOL} md0p3 || mort
zfs create -o mountpoint=none ${OVH_ZFS_POOL}/ROOT || mort
zfs create -o mountpoint=/ ${OVH_ZFS_POOL}/ROOT/default || mort
zpool set bootfs=${OVH_ZFS_POOL}/ROOT/default ${OVH_ZFS_POOL} || mort
zfs create ${OVH_ZFS_POOL}/home || mort
zfs create -o canmount=off ${OVH_ZFS_POOL}/usr || mort
zfs create ${OVH_ZFS_POOL}/usr/obj || mort
zfs create ${OVH_ZFS_POOL}/usr/src || mort
zfs create ${OVH_ZFS_POOL}/jail || mort
zfs create -o canmount=off ${OVH_ZFS_POOL}/var || mort
zfs create ${OVH_ZFS_POOL}/var/log || mort
zfs create ${OVH_ZFS_POOL}/var/tmp || mort
zfs create ${OVH_ZFS_POOL}/tmp || mort
cd /usr/src || mort
make installkernel DESTDIR=/mnt || mort
make installworld DESTDIR=/mnt || mort
make distribution DESTDIR=/mnt || mort
mkdir -p /mnt/usr/local/etc || mort
cp $OVH_FOLDER/go2.sh /mnt/root/go2.sh || mort
# use local resolver temporary, for chroot go2.sh inside image
cp /etc/resolv.conf /mnt/etc/
chroot /mnt passwd
chroot /mnt /root/go2.sh
cp $OVH_FOLDER/localtime $OVH_FOLDER/rc.conf $OVH_FOLDER/resolv.conf $OVH_FOLDER/fstab /mnt/etc/
cp $OVH_FOLDER/sshd_config /mnt/etc/ssh/
cp $OVH_FOLDER/loader.conf /mnt/boot/loader.conf
zpool export ${OVH_ZFS_POOL}
mdconfig -d -u 0
gzip --keep $OVH_IMAGE
Step 3 - execute /root/ovh/vps_image_build.sh
to generate disk image
Below is the output of the script, output from make installkernel, make installworld removed between lines ... ...
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
# /root/ovh/vps_image_build.sh
md0
md0 created
md0p1 added
md0p2 added
md0p3 added
=> 40 83886000 md0 GPT (40G)
40 1024 1 freebsd-boot (512K)
1064 4194304 2 freebsd-swap (2.0G)
4195368 79690672 3 freebsd-zfs (38G)
partcode written to md0p1
bootcode written to md0
--------------------------------------------------------------
>>> Install check kernel
--------------------------------------------------------------
--------------------------------------------------------------
>>> Installing kernel GENERIC on Tue Feb 18 13:56:53 CET 2025
--------------------------------------------------------------
cd /usr/obj/usr/src/amd64.amd64/sys/GENERIC; MACHINE_ARCH=amd64 MACHINE=amd64 CPUTYPE= CC="cc -target x86_64-unknown-freebsd14.2 --sysroot=/usr/obj/usr/src/amd64.amd64/tmp -B/usr/obj/usr/src/amd64.amd64/tmp/usr/bin" CXX="c++ -target x86_64-unknown-freebsd14.2 --sysroot=/usr/obj/usr/src/amd64.amd64/tmp -B/usr/obj/usr/src/amd64.amd64/tmp/usr/bin" CPP="cpp -target x86_64-unknown-freebsd14.2 --sysroot=/usr/obj/usr/src/amd64.amd64/tmp -B/usr/obj/usr/src/amd64.amd64/tmp/usr/bin" AS="as" AR="ar" ELFCTL="elfctl" LD="ld" LLVM_LINK="" NM=nm OBJCOPY="objcopy" RANLIB=ranlib STRINGS= SIZE="size" STRIPBIN="strip" PATH=/usr/obj/usr/src/amd64.amd64/tmp/bin:/usr/obj/usr/src/amd64.amd64/tmp/usr/sbin:/usr/obj/usr/src/amd64.amd64/tmp/usr/bin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/usr/sbin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/usr/bin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/bin:/usr/obj/usr/src/amd64.amd64/tmp/legacy/usr/libexec::/sbin:/bin:/usr/sbin:/usr/bin make KERNEL=kernel METALOG= install
mkdir -p /mnt/boot/kernel
install -p -m 444 -o root -g wheel kernel /mnt/boot/kernel/
mkdir -p /mnt/usr/lib/debug/boot/kernel
...
...
===> etc (installconfig)
===> etc/termcap (installconfig)
installing DIRS CONFSDIR
install -N /usr/src/etc -d -m 0755 -o root -g wheel /mnt/etc
install -N /usr/src/etc -C -o root -g wheel -m 644 termcap.small /mnt/etc/termcap.small
===> etc/sendmail (installconfig)
Changing local password for root
New Password:
Retype New Password:
creating user user1
Bootstrapping pkg from pkg+https://pkg.FreeBSD.org/FreeBSD:14:amd64/quarterly, please wait...
Verifying signature with trusted certificate pkg.freebsd.org.2013102301... done
Installing pkg-1.21.3...
Extracting pkg-1.21.3: 100%
Updating FreeBSD repository catalogue...
Fetching meta.conf: 100% 178 B 0.2kB/s 00:01
Fetching data.pkg: 100% 7 MiB 2.5MB/s 00:03
Processing entries: 100%
FreeBSD repository update completed. 35857 packages processed.
All repositories are up to date.
Updating database digests format: 100%
The following 12 package(s) will be affected (of 0 checked):
New packages to be INSTALLED:
bash: 5.2.37
doas: 6.3p12
gettext-runtime: 0.23
indexinfo: 0.3.1
joe: 4.6_1,1
libffi: 3.4.6
mpdecimal: 4.0.0
node_exporter: 1.8.2
perl5: 5.36.3_2
python311: 3.11.11
readline: 8.2.13_2
zfs-stats: 1.3.2
Number of packages to be installed: 12
The process will require 289 MiB more space.
49 MiB to be downloaded.
[1/12] Fetching indexinfo-0.3.1.pkg: 100% 6 KiB 5.9kB/s 00:01
[2/12] Fetching mpdecimal-4.0.0.pkg: 100% 156 KiB 159.3kB/s 00:01
[3/12] Fetching zfs-stats-1.3.2.pkg: 100% 10 KiB 10.6kB/s 00:01
[4/12] Fetching node_exporter-1.8.2.pkg: 100% 4 MiB 3.9MB/s 00:01
[5/12] Fetching perl5-5.36.3_2.pkg: 100% 15 MiB 1.8MB/s 00:09
[6/12] Fetching joe-4.6_1,1.pkg: 100% 500 KiB 512.2kB/s 00:01
[7/12] Fetching doas-6.3p12.pkg: 100% 24 KiB 24.6kB/s 00:01
[8/12] Fetching libffi-3.4.6.pkg: 100% 45 KiB 46.0kB/s 00:01
[9/12] Fetching readline-8.2.13_2.pkg: 100% 397 KiB 406.3kB/s 00:01
[10/12] Fetching bash-5.2.37.pkg: 100% 2 MiB 1.8MB/s 00:01
[11/12] Fetching gettext-runtime-0.23.pkg: 100% 236 KiB 241.2kB/s 00:01
[12/12] Fetching python311-3.11.11.pkg: 100% 27 MiB 2.0MB/s 00:14
Checking integrity... done (0 conflicting)
[1/12] Installing indexinfo-0.3.1...
[1/12] Extracting indexinfo-0.3.1: 100%
[2/12] Installing mpdecimal-4.0.0...
[2/12] Extracting mpdecimal-4.0.0: 100%
[3/12] Installing perl5-5.36.3_2...
[3/12] Extracting perl5-5.36.3_2: 100%
[4/12] Installing libffi-3.4.6...
[4/12] Extracting libffi-3.4.6: 100%
[5/12] Installing readline-8.2.13_2...
[5/12] Extracting readline-8.2.13_2: 100%
[6/12] Installing gettext-runtime-0.23...
[6/12] Extracting gettext-runtime-0.23: 100%
[7/12] Installing zfs-stats-1.3.2...
[7/12] Extracting zfs-stats-1.3.2: 100%
[8/12] Installing node_exporter-1.8.2...
[8/12] Extracting node_exporter-1.8.2: 100%
[9/12] Installing joe-4.6_1,1...
[9/12] Extracting joe-4.6_1,1: 100%
[10/12] Installing doas-6.3p12...
[10/12] Extracting doas-6.3p12: 100%
[11/12] Installing bash-5.2.37...
[11/12] Extracting bash-5.2.37: 100%
[12/12] Installing python311-3.11.11...
[12/12] Extracting python311-3.11.11: 100%
=====
Message from node_exporter-1.8.2:
--
If upgrading from a version of node_exporter <0.15.0 you'll need to update any
custom command line flags that you may have set as it now requires a
double-dash (--flag) instead of a single dash (-flag).
The collector flags in 0.15.0 have now been replaced with individual boolean
flags and the -collector.procfs` and -collector.sysfs` flags have been renamed
to --path.procfs and --path.sysfs respectively.
=====
Message from doas-6.3p12:
--
To use doas,
/usr/local/etc/doas.conf
must be created. Refer to doas.conf(5) for further details and/or follow
/usr/local/etc/doas.conf.sample as an example.
Note: In order to be able to run most desktop (GUI) applications, the user
needs to have the keepenv keyword specified. If keepenv is not specified then
key elements, like the user's $HOME variable, will be reset and cause the GUI
application to crash.
Users who only need to run command line applications can usually get away
without keepenv.
When in doubt, try to avoid using keepenv as it is less secure to have
environment variables passed to privileged users.
=====
Message from python311-3.11.11:
--
Note that some standard Python modules are provided as separate ports
as they require additional dependencies. They are available as:
py311-gdbm databases/py-gdbm@py311
py311-sqlite3 databases/py-sqlite3@py311
py311-tkinter x11-toolkits/py-tkinter@py311
Here are the contents of /root/ovh
folder after script finished:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ls -al /root/ovh/
total 7101956
drwxr-xr-x 2 root wheel 12 Feb 18 13:59 .
drwxr-x--- 7 root wheel 30 Feb 18 12:26 ..
-rw-r--r-- 1 root wheel 78 Jul 21 2023 fstab
-rwxr-xr-x 1 root wheel 2772 Feb 18 12:53 go2.sh
-rw-r--r-- 1 root wheel 107 Jul 21 2023 loader.conf
-r--r--r-- 1 root wheel 2945 Nov 13 2022 localtime
-rw-r--r-- 1 root wheel 42949672960 Feb 18 13:59 ovh.img
-rw-r--r-- 1 root wheel 1655817216 Feb 18 13:59 ovh.img.gz
-rw-r--r-- 1 root wheel 370 Jul 21 2023 rc.conf
-rw-r--r-- 1 root wheel 25 Nov 13 2022 resolv.conf
-rw-r--r-- 1 root wheel 3843 Nov 13 2022 sshd_config
-rwx------ 1 root wheel 2347 Feb 18 13:16 vps_image_build.sh
Step 4 - write disk image to VPS
Replace X.X.X.X
below with IP from OVH recovery mail, to restore VPS image via SSH:
1
ssh root@X.X.X.X "gunzip | dd of=/dev/sdb bs=1M" </root/ovh/ovh.img.gz
Reboot VPS from OVH Dashboard, and you should be able to login via SSH.
To later reimport pool image and temporarily mount it on /mnt on your main host:
1
mdconfig -f /root/ovh/ovh.img -u 0 && zpool import -R /mnt rpoolovh