Table of Contents
Background #
I'm someone who's deeply focused on privacy, love FOSS, and hate the idea of hosting my private data (mostly family photos) on third‑party systems, you know what they say: "If they don't charge you for the product, you are the product". I've also heard horror stories about people losing access to their Google accounts without cause and, even worse, having no way to appeal the ban.
For a long time, I would dump all our family's media onto an external HDD and mirror it to another. That worked well for a while, until I grew tired of managing the media dumps, and they grew tired of asking for access every time they wanted to see old pictures.
Self‑hosting my photo library had interested me for some time. I even picked up a couple of Odroid boards to experiment, but the lack of official Linux support and uneven ARM compatibility made them impractical for sustained use.
Enter the Optiplex #
About a year ago, I stumbled on a second‑hand Dell OptiPlex with an Intel Core i7-9700T
, 32 GB of RAM, and a compact x86_64 form factor. It wasn't shiny new, but it promised rock‑solid Linux compatibility and outperformed my little Odroids.
Since this was my first real home server, I kept things simple: Ubuntu, a VPN, Docker. For photos, I gave PhotoPrism a spin and liked it, though the free tier only supports one user and the mobile app felt undercooked. Manual imports were a bit of a drag.
Then I discovered Immich, which fit the bill much better: smooth iOS/Android apps let everyone back up directly to the server.
That combo (OptiPlex, VPN, Immich) ran like a champ for six months. Along the way, I added Navidrome for music, spun up a Kanboard instance for task tracking, and even started doing remote dev on the machine.
Upgrading Hardware #
Recently I scored a second‑hand Lenovo ThinkCentre with an Intel Core i7-10700T
, 32 GB RAM, and a 1 TB NVMe SSD. It was a steal, so I figured I'd use it for public‑facing stuff like a blog, git
server, maybe even a Minecraft server; while the OptiPlex handled private services. But then I realized the OptiPlex still had more life in it, so I'm swapping roles: ThinkCentre becomes the new lab server.
So here I am, building a fresh setup from scratch. My goals are the same: privacy, portability, easy maintenance, but I want better isolation and fewer manual tweaks. These are the paths I've weighed:
Ubuntu with Docker #
- Pros: Quick to boot up and familiar.
- Cons: Every container shares the host kernel; isolation isn't great, and you end up maintaining configs by hand.
Proxmox with VMs #
I tested Proxmox (my CPU has KVM support). It works solidly, but the UI and storage layers felt like overkill for my needs.
Ubuntu Host + Fedora CoreOS VM #
Running Fedora CoreOS inside QEMU/KVM on Ubuntu gave me clean separation, but managing KVM configs felt like extra overhead.
NixOS with Docker & KVM #
I've been eyeing NixOS for a while. The declarative model appeals to me "one config file to rule them all" they say and its minimal runtime footprint seems ideal.
Installing NixOS #
I've never set up NixOS before, so expect a few first‑time notes. I'll keep the deep dives to a minimum but capture the essentials.
Preparing the Install Media #
I grabbed an old trusty USB stick (been using it for years):
1$ curl -LO https://channels.nixos.org/nixos-24.11/latest-nixos-minimal-x86_64-linux.iso
2$ sudo dd if=latest-nixos-minimal-x86_64-linux.iso of=/dev/sda bs=4M conv=fsync status=progress
Booting the Live Environment #
Boot via UEFI, pick the default live option, and you'll land at a nixos@
shell. ip a
shows DHCP is up. To SSH in:
1[nixos@nixos:~]$ mkdir -p .ssh && curl -L https://github.com/y3k.keys -o .ssh/authorized_keys
Then, from my laptop:
1$ ssh -o "StrictHostKeyChecking no" nixos@<lan-IP>
Preparing the Storage Media #
Since I'm following the NixOS manual I'll just use what they recommend on their partitioning section.
First I need to identify what device I'll use:
1[nixos@nixos:~]$ lsblk
2NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
3loop0 7:0 0 1.1G 1 loop /nix/.ro-store
4sda 8:0 1 14.4G 0 disk
5├─sda1 8:1 1 1.2G 0 part /iso
6└─sda2 8:2 1 3M 0 part
7nvme0n1 259:0 0 953.9G 0 disk
8├─nvme0n1p1 259:1 0 1G 0 part
9├─nvme0n1p2 259:2 0 2G 0 part
10└─nvme0n1p3 259:3 0 950.8G 0 part
11 ├─vg-lv--root 254:0 0 32G 0 lvm
12 ├─vg-lv--swap 254:1 0 8G 0 lvm
13 ├─vg-lv--var 254:2 0 256G 0 lvm
14 └─vg-lv--home 254:3 0 654.8G 0 lvm
The /dev/sda
device is my USB stick, and I plan to install NixOS into the NVMe therefore I'll use nvme0n1
from now on.
Partitions and LVM #
The manual recommends to have a /boot
partition marked with ESP, a /swap
partition and a /
root one for everything else, I didn't quite like this setup so I'll go with something more similar to what I had before.
Note: Looks like having a single ESP partition as /boot
is a requirement for NixOS, therefore I merged /boot
and /boot/efi
into a single partition marked as ESP
.
1[nixos@nixos:~]$ sudo -i
2
3[root@nixos:~]# parted /dev/nvme0n1 -- mklabel gpt
4Warning: The existing disk label on /dev/nvme0n1 will be destroyed and all data on this disk will
5be lost. Do you want to continue?
6Yes/No? yes
7
8[root@nixos:~]# parted /dev/nvme0n1 -- mkpart ESP fat32 1MB 1G set 1 esp on
9
10[root@nixos:~]# parted /dev/nvme0n1 -- mkpart lvm 1GiB 100% set 2 lvm on
11
12[root@nixos:~]# parted /dev/nvme0n1 --list
13Model: SAMSUNG MZVLB1T0HBLR-00000 (nvme)
14Disk /dev/nvme0n1: 1024GB
15Sector size (logical/physical): 512B/512B
16Partition Table: gpt
17Disk Flags:
18
19Number Start End Size File system Name Flags
20 1 1049kB 1000MB 999MB ext4 ESP boot, esp
21 2 1074MB 1024GB 1023GB fat32 lvm lvm
22
23[root@nixos:~]# pvcreate /dev/nvme0n1p2
24 Physical volume "/dev/nvme0n1p2" successfully created.
25
26[root@nixos:~]# vgcreate vg /dev/nvme0n1p2
27 Volume group "vg" successfully created
28
29[root@nixos:~]# lvcreate -n swap -L 8G vg
30 Logical volume "swap" created.
31
32[root@nixos:~]# lvcreate -n root -L 64G vg
33 Logical volume "root" created.
34
35[root@nixos:~]# lvcreate -n home -l 100%FREE vg
36 Logical volume "home" created.
37
38[root@nixos:~]# mkfs.fat -F32 -n boot /dev/nvme0n1p1
39mkfs.fat 4.2 (2021-01-31)
40
41[root@nixos:~]# mkswap -L swap /dev/vg/swap
42Setting up swapspace version 1, size = 8 GiB (8589930496 bytes)
43LABEL=swap, UUID=e788698e-fd2b-465d-ad23-0c0d99a10bb5
44
45[root@nixos:~]# mkfs.ext4 -L nixos /dev/vg/root
46mke2fs 1.47.1 (20-May-2024)
47
48[root@nixos:~]# mkfs.ext4 -L home /dev/vg/home
49mke2fs 1.47.1 (20-May-2024)
Mounting everything #
1[root@nixos:~]# mount /dev/vg/root /mnt/
2
3[root@nixos:~]# mkdir /mnt/boot
4
5[root@nixos:~]# mount /dev/nvme0n1p1 /mnt/boot/
6
7[root@nixos:~]# swapon /dev/vg/swap
8
9[root@nixos:~]# mkdir /mnt/home
10
11[root@nixos:~]# mount /dev/vg/home /mnt/home/
12
13[root@nixos:~]# lsblk
14NAME MAJ:MIN RM SIZE RO TYPE MOUNTPOINTS
15loop0 7:0 0 1.1G 1 loop /nix/.ro-store
16sda 8:0 1 14.4G 0 disk
17├─sda1 8:1 1 1.2G 0 part /iso
18└─sda2 8:2 1 3M 0 part
19nvme0n1 259:0 0 953.9G 0 disk
20├─nvme0n1p1 259:1 0 953M 0 part /mnt/boot
21└─nvme0n1p2 259:2 0 952.9G 0 part
22 ├─vg-swap 254:0 0 8G 0 lvm [SWAP]
23 ├─vg-root 254:1 0 64G 0 lvm /mnt
24 └─vg-home 254:2 0 880.9G 0 lvm /mnt/home
Installing NixOS #
Once the storage device is set, we can finally give it a shot the installer.
1[root@nixos:~]# nixos-generate-config --root /mnt/
2writing /mnt/etc/nixos/hardware-configuration.nix...
3writing /mnt/etc/nixos/configuration.nix...
4For more hardware-specific settings, see https://github.com/NixOS/nixos-hardware.
This is my first time using Nix so the configuration file is a little bit daunting, I opened it with vim
and modified the options that I thought were useful like:
- Enable
systemd-boot
. - Enable
OpenSSH
. - Disable wireless.
- Disable pulseaudio.
- Set hostname.
- Set timezone.
- Disable X server.
- Disable CUPS.
- Diable touchpad.
- Disable Firefox.
- Create a normal user.
Once the first configuration was set, it was time to run the install command:
1[root@nixos:~]# nixos-install
2copying channel...
3building the configuration in /mnt/etc/nixos/configuration.nix...
4...
5installation finished!
6
7[root@nixos:~]# nixos-enter --root /mnt/ -c 'passwd y3k'
8setting up /etc...
9New password:
10Retype new password:
11passwd: password updated successfully
12
13[root@nixos:~]# reboot
Getting started with NixOS #
Everything seemed to be successful and I was greeted by the login screen and I could access with my normal user and password. My $HOME
was totally empty though, it was time to start setting this up to my liking, my first step was to ensure I could SSH into the server so I created my .ssh
directory and downloaded my public keys from GitHub again.
NeoVim as editor #
NixOS seems to ship nano
as the only editor, but I prefer neovim
so I used this opportunity to install my first package, adding the next config to the configuration.nix
file.
environment.systemPackages = with pkgs; [
neovim
];
environment.variables = {
EDITOR = "nvim";
};
Static IP #
I like to use static IPs for my home lab servers, this time was not going to be an exception, searching for a bit I found it how to configure it in the nix file.
networking = {
...
interfaces.eno1 = {
useDHCP = false;
ipv4.addresses = [{
address = "192.168.2.6";
prefixLength = 24;
}];
};
...
};
Installing zsh
with OhMyZsh #
programs.zsh = {
enable = true;
ohMyZsh = {
enable = true;
plugins = [ "common-aliases" ];
theme = "fishy";
};
};
After rebuilding the system I'm still able to SSH into my server with a static IP and over Tailscale, with tmux
, using zsh
as my shell and also neovim
available.
Using configuration.nix
is so different from any other package manager that I've used before like apt
or pacman
, I know I'm barely scratching the surface but I'm starting to really like this approach, hopefully I won't regret it once I start hitting edge cases.