Building a Home Server [0]: Hardware, Linux Installation, and Initial Configuration
Tags: linux, ubuntu, systems
Published: 2021-02-16 07:07:41

# Intro
Hi! In this series of posts, I'll bring you along on my journey to setup a home server. I intend to use this server for file sharing/backup, hosting `git` repos, and various other services. The main applications I have in mind right now are Nextcloud, Gitlab, Pi-hole, and Prometheus/Grafana. Since I'm planning to run everything under Docker, it should be relatively easy to add more services on demand.

# Hardware

So...I have a bad habit of rebuilding my gaming PC somewhat often. However, this means I have a lot of spare parts for the server! 

## Parts List:

**CPU:** AMD Ryzen 2700X

**CPU Cooler**: Noctua NH-D15 chromax.Black

**Motherboard:**: Asus ROG Strix X470-F

**RAM:** 4x8GB G.SKILL Trident RGB DDR4-3000

**Boot Drive:** Samsung 980 PRO 500 GB NVMe SSD

**Storage Drives:** | 2x Seagate BarraCuda 8TB HDD (will be run in RAID 1)

**Graphics Card:** EVGA GTX 1080 Reference (needed since no onboard CPU graphics)

**Power Supply:** EVGA SuperNOVA G1 80+ GOLD 650W

**Case:** NZXT H500i

**Case Fans:** 2x be quiet! Silent Wings 3 140mm

<br>

For my use case, consumer grade hardware should suffice. Also, I can't decide whether 8TB of storage is overkill or not enough! Guess we'll find out...

![drive cage](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/g3l6pfeovrcv22igh4eg.jpg) <figcaption>Drive cage containing the two Seagate 8TB HDDs</figcaption>

Assembling the server was straightforward aside from one minor hiccup with the fans. I had to replace one of the two 140mm fans on the CPU cooler with a 120 mm one since my RAM sticks were too tall. Damn those RGB heat spreaders! 

![server](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4ul3x91stsr7xvq26bus.jpg)<figcaption>The server in all of its post-gaming glory</figcaption>

# Initial Installation

## Linux Distribution
I've chosen to use Ubuntu Server for the OS since it's one of the most popular server distributions. I'm more familiar with CentOS, but given [the recent shift in that project](https://blog.centos.org/2020/12/future-is-centos-stream/), Ubuntu seems to be a more stable alternative.

## Creating and Booting into the Ubuntu Install Media
* Download the [Ubuntu Server ISO](https://ubuntu.com/download/server) from the official website.
* Flash the ISO onto a USB drive ([balenaEtcher](https://www.balena.io/etcher/) is a good tool for this).
* Plug the USB drive into the server and boot into the install media.
  * Ensure that the server has an Internet connection so that packages and updates can be downloaded during the install. Ethernet is preferred for ease of use and stability.
  * **NOTE:** You may have to change BIOS settings if the server doesn't boot into the USB automatically. Refer to your motherboard's docs for more information.

## Installation Process
The [official installation guide](https://ubuntu.com/tutorials/install-ubuntu-server#1-overview) documents the process well, so I won't list all the steps here. Here are some steps I want to emphasize:

**Configure storage**: 
* Make sure that you select the correct disk to install the OS on. For me, the OS disk will be the Samsung 500GB NVMe SSD (this will vary depending on your specific hardware configuration). 
* I'm choosing to configure the two storage disks later manually.

**Profile setup**: 
* The username and password created in this step will be used to login to the server after installation, so keep note of them! 
* The newly created user will have `sudo` privileges.
* I'm using `positron` as the hostname for my server, in case you see it later.

**SSH Setup**: 
* Make sure to install OpenSSH so you can login to the machine remotely. 
* You can also choose to import SSH keys from Github/Launchpad. If you do this, it's better to disable password authentication for SSH since it won't be needed. If anything goes wrong, you can always login to the server directly as long as you have physical access.

**Snaps**:
* I'm not installing any extra packages during the install since I'll do that later using Ansible.

After the installer does its thing, reboot the server and login with the username and password you created earlier.

# Initial Configuration
## Remote Login
Get the IP address of the server so we can login remotely:
```
jaswraith@positron:~$ ip addr
1: lo: <LOOPBACK,UP,LOWER_UP> mtu 65536 qdisc noqueue state UNKNOWN group default qlen 1000
    link/loopback 00:00:00:00:00:00 brd 00:00:00:00:00:00
    inet 127.0.0.1/8 scope host lo
       valid_lft forever preferred_lft forever
    inet6 ::1/128 scope host 
       valid_lft forever preferred_lft forever
2: enp7s0: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc mq state UP group default qlen 1000
    link/ether 04:92:26:c0:e1:28 brd ff:ff:ff:ff:ff:ff
    inet 192.168.1.140/24 brd 192.168.1.255 scope global dynamic enp7s0
       valid_lft 85934sec preferred_lft 85934sec
    inet6 fe80::692:26ff:fec0:e128/64 scope link 
       valid_lft forever preferred_lft forever
```
The IPv4 address for interface `enp7s0` is the first part of the `inet` field: `192.168.1.140`. Your server's interface name might be different depending on your networking hardware.

Now we can login from a different machine using `ssh`:
```
jaswraith@photon[~]=> ssh jaswraith@192.168.1.140
Welcome to Ubuntu 20.04.2 LTS (GNU/Linux 5.4.0-65-generic x86_64)

...

jaswraith@positron:~$ 
```
Note that I didn't have to enter my password since: 
* I imported my public SSH key from Github during installation.
* I have the corresponding private key on my laptop (`photon`) under `/home/jaswraith/.ssh/id_ed25519`

I'm adding the entry `192.168.1.140 positron` to `/etc/hosts` on my laptop so I can login using the hostname instead of the IP address:
```
jaswraith@photon[~]=> cat /etc/hosts
# Static table lookup for hostnames.
# See hosts(5) for details.
127.0.0.1   localhost
::1         localhost
127.0.1.1   photon.localdomain  photon

192.168.1.140   positron
```

The network router on the local network needs to be configured to reserve the server's IP address so that it doesn't change after a reboot. My router is running [dd-wrt](https://dd-wrt.com/), so I'll use that as an example (refer to your own router's documentation on how to set a non-expiring DHCP lease).

**dd-wrt** specific steps:
* Navigate to `192.168.1.1` in a browser
* Click on the "Services" tab
* Under the "DHCP Server" section, click on "Add" under "Static Leases"
* Enter the server's MAC address, IP address, and hostname (you can get this info from the router's "Status" page or by running `ip addr` on the server). Leave the "Client Lease Expiration" field blank so that the DHCP lease never expires. 

![dd-wrt](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j4er51xsdddp1azqgur1.png)<figcaption>Adding a non-expiring DHCP lease for the server on dd-wrt</figcaption>

## System Upgrade
Upgrade all installed packages on the server using `apt`    : 
```
jaswraith@positron:~$ sudo apt update
Hit:1 http://us.archive.ubuntu.com/ubuntu focal InRelease
...
Reading package lists... Done
Building dependency tree
Reading state information... Done
13 packages can be upgraded. Run 'apt list --upgradable' to see them.

jaswraith@positron:~$ sudo apt upgrade
Reading package lists... Done
Building dependency tree
Reading state information... Done
Calculating upgrade... Done
The following packages will be upgraded:
  dirmngr friendly-recovery gnupg gnupg-l10n gnupg-utils gpg gpg-agent gpg-wks-client gpg-wks-server gpgconf gpgsm gpgv open-vm-tools
13 upgraded, 0 newly installed, 0 to remove and 0 not upgraded.
Need to get 3,133 kB of archives.
After this operation, 114 kB of additional disk space will be used.
Do you want to continue? [Y/n] y
Get:1 http://us.archive.ubuntu.com/ubuntu focal-updates/main amd64 gpg-wks-client amd64 2.2.19-3ubuntu2.1 [97.6 kB]

...
```

* `apt update` downloads the latest package information from all configured sources on the server. It also informs you how many packages can be upgraded.
* `apt upgrade` actually upgrades the packages.

## Storage Drive Configuration
Time for the fun stuff, let's configure those storage drives!

Check if the drives are recognized by the system with `lsblk`:
```
jaswraith@positron:~$ sudo lsblk
NAME                      MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
loop0                       7:0    0  55.4M  1 loop /snap/core18/1944
loop1                       7:1    0  31.1M  1 loop /snap/snapd/10707
loop2                       7:2    0  69.9M  1 loop /snap/lxd/19188
sda                         8:0    0   7.3T  0 disk 
sdb                         8:16   0   7.3T  0 disk 
nvme0n1                   259:0    0 465.8G  0 disk 
├─nvme0n1p1               259:1    0   512M  0 part /boot/efi
├─nvme0n1p2               259:2    0     1G  0 part /boot
└─nvme0n1p3               259:3    0 464.3G  0 part 
  └─ubuntu--vg-ubuntu--lv 253:0    0   200G  0 lvm  /
```
The two storage drives are listed as `sda` and `sdb`. **NOTE:** Drive names can be different depending on the type and amount of drives in your system.

### Creating a RAID Array
I'm going to run the two storage drives in a [RAID 1 array](https://en.wikipedia.org/wiki/Standard_RAID_levels#RAID_1). Essentially, the two drives will be exact copies of one another and can act as a single device on the system. If one drive happens to fail, the data is still accessible from the good drive. The array can be repaired by replacing the bad drive and running a few commands. The downside is that the array can only be as large as the smallest member disk (8TB in my case).

On Linux, [`mdadm`](https://en.wikipedia.org/wiki/Mdadm) is the utility used to manage software RAID arrays. Use the following command to create the array `md0` from the two disks:
```
jaswraith@positron:~$ sudo mdadm --create --verbose /dev/md0 --level=1 --raid-devices=2 /dev/sda /dev/sdb
mdadm: Note: this array has metadata at the start and
    may not be suitable as a boot device.  If you plan to
    store '/boot' on this device please ensure that
    your boot-loader understands md/v1.x metadata, or use
    --metadata=0.90
mdadm: size set to 7813894464K
mdadm: automatically enabling write-intent bitmap on large array
Continue creating array? yes
mdadm: Defaulting to version 1.2 metadata
mdadm: array /dev/md0 started.
```
**NOTE:** Make sure you use the correct drive names and **DON'T** use the OS drive! 

Check that the array was successfully created:
```
jaswraith@positron:~$ sudo mdadm -D /dev/md0
/dev/md0:
           Version : 1.2
     Creation Time : Tue Feb 16 02:51:43 2021
        Raid Level : raid1
        Array Size : 7813894464 (7451.91 GiB 8001.43 GB)
     Used Dev Size : 7813894464 (7451.91 GiB 8001.43 GB)
      Raid Devices : 2
     Total Devices : 2
       Persistence : Superblock is persistent

     Intent Bitmap : Internal

       Update Time : Tue Feb 16 02:56:33 2021
             State : clean, resyncing 
    Active Devices : 2
   Working Devices : 2
    Failed Devices : 0
     Spare Devices : 0

Consistency Policy : bitmap

     Resync Status : 0% complete

              Name : positron:0  (local to host positron)
              UUID : dfe41661:16eb9d7f:ed891288:904746b0
            Events : 58

    Number   Major   Minor   RaidDevice State
       0       8        0        0      active sync   /dev/sda
       1       8       16        1      active sync   /dev/sdb

```

The resync process is going to take a **loooong** time. We can track the progress by running `watch cat /proc/mdstat`:
```
jaswraith@positron:~$ watch cat /proc/mdstat

Every 2.0s: cat /proc/mdstat                                                                                   positron: Tue Feb 16 03:03:05 2021

Personalities : [linear] [multipath] [raid0] [raid1] [raid6] [raid5] [raid4] [raid10]
md0 : active raid1 sdb[1] sda[0]
      7813894464 blocks super 1.2 [2/2] [UU]
      [>....................]  resync =  1.4% (116907456/7813894464) finish=739.5min speed=173457K/sec
      bitmap: 59/59 pages [236KB], 65536KB chunk

unused devices: <none>
```
740 minutes?! This is going to take over **12 hours**!

We can tweak some kernel parameters with `sysctl` to make this go faster:
```
jaswraith@positron:~$ sudo sysctl -w dev.raid.speed_limit_min=100000
dev.raid.speed_limit_min = 100000
jaswraith@positron:~$ sudo sysctl -w dev.raid.speed_limit_max=500000
dev.raid.speed_limit_max = 500000
```

Or I guess not...turns out that the reported speed of ~170000K/s is right around the documented read speed for my Seagate drives (190MB/s). Fortunately, we can still write to the `md0` device during this process.

### Configuring the `md0` Array
Use `mkfs` to format `md0`:
```
jaswraith@positron:~$ sudo mkfs.ext4 -F /dev/md0
mke2fs 1.45.5 (07-Jan-2020)
/dev/md0 contains a ext4 file system
        created on Tue Feb 16 04:07:25 2021
Creating filesystem with 1953473616 4k blocks and 244187136 inodes
Filesystem UUID: 3e8
b5548-14c5-4fac-8e90-98cdbd71123f
Superblock backups stored on blocks: 
        32768, 98304, 163840, 229376, 294912, 819200, 884736, 1605632, 2654208, 
        4096000, 7962624, 11239424, 20480000, 23887872, 71663616, 78675968, 
        102400000, 214990848, 512000000, 550731776, 644972544, 1934917632

Allocating group tables: done                            
Writing inode tables: done                            
Creating journal (262144 blocks): done
Writing superblocks and filesystem accounting information: done 
```
**NOTE:** The `-F` flag is needed since `md0` is not a partition on a block device. 

I opted to use the classic `ext4` filesystem type instead of other ones (`xfs`, `zfs`, etc.).

In order to use the newly created filesystem, we have to mount it. Create a mountpoint with `mkdir` and use `mount` to mount it:
```
jaswraith@positron:~$ sudo mkdir -p /data
jaswraith@positron:~$ sudo mount /dev/md0 /data
```
Confirm that the filesystem has been mounted:
```
jaswraith@positron:~$ df -h /data
Filesystem      Size  Used Avail Use% Mounted on
/dev/md0        7.3T   93M  6.9T   1% /data
```

The `/etc/mdadm/mdadm.conf` file needs to be edited so that the array is automatically assembled after boot. We can redirect the output of `mdadm --detail --scan --verbose` using `tee -a` to accomplish this:
```
jaswraith@positron:~$ sudo mdadm --detail --scan --verbose | sudo tee -a /etc/mdadm/mdadm.conf 
ARRAY /dev/md0 metadata=1.2 name=positron:0 UUID=dfe41661:16eb9d7f:ed891288:904746b0
```
Confirm that the `md0` array configuration has been added to the end of `/etc/mdadm/mdadm.conf`:
```
jaswraith@positron:~$ cat /etc/mdadm/mdadm.conf 
# mdadm.conf
#
# !NB! Run update-initramfs -u after updating this file.
# !NB! This will ensure that initramfs has an uptodate copy.
#
# Please refer to mdadm.conf(5) for information about this file.

...

ARRAY /dev/md0 level=raid1 num-devices=2 metadata=1.2 name=positron:0 UUID=dfe41661:16eb9d7f:ed891288:904746b0
   devices=/dev/sda,/dev/sdb
```

We also need to regenerate the initramfs image with our RAID configuration:
```
jaswraith@positron:~$ sudo update-initramfs -u
update-initramfs: Generating /boot/initrd.img-5.4.0-65-generic
```

The last step is to add an entry for `md0` in `/etc/fstab` so that the array is automatically mounted after boot:
```
jaswraith@positron:~$ echo '/dev/md0 /data ext4 defaults,nofail,discard 0 0' | sudo tee -a /etc/fstab
/dev/md0 /data ext4 defaults,nofail,discard 0 0

jaswraith@positron:~$ cat /etc/fstab
# /etc/fstab: static file system information.

...

/dev/md0 /data ext4 defaults,nofail,discard 0 0
```
Note that I used the same mountpoint for `md0` that was created eariler (`/data`).

## Conclusion
So we've completed the intial setup of the server and created a RAID 1 array for storage. This server looks ready to run some services! Stay tuned for the next post where we'll define and run those services using Docker and Ansible.