You have spent fifty-two lessons administering srv-tramontana. You know how to set up a reverse proxy with TLS, tune PostgreSQL, encrypt a volume, harden a systemd unit and rebuild the whole machine with Ansible in fifty minutes. And there is a reasonable doubt that almost everybody has by this point: how much of this is "enterprise server stuff" and how much really applies anywhere?
This lesson answers that question by building something that is yours: a media server for your home. You are going to set up Jellyfin on your own hardware, with redundant storage, hardware acceleration for transcoding, shares for the family's devices, access from outside and backups. And you are going to discover that it is exactly the same work: the same UUIDs in fstab, the same key in keyrings, the same hardened unit, the same smartctl, the same Ansible.
With two differences that matter at home and do not in the data centre: electricity consumption and noise. And with a legal warning that is worth reading before you start.
Contents
- Legal warning: what content is legitimate
- Objective, requirements and design decisions
- Choosing the hardware
- Storage: RAID, Btrfs and the truth about backups
- Jellyfin versus Plex and Emby
- Installation and a hardened systemd unit
- Hardware acceleration for transcoding
- Organising the library and naming files
- Sharing: Samba for everything, NFS for Linux
- Access from outside the house: three options and their risk
- Downloads and automation
- Backups of the library and of the configuration
- Consumption, noise and temperature
- Automation with Ansible
- Operation: updates, disk health and what to do when one fails
Legal warning: what content is legitimate
It comes first because it is the first thing.
A media server is a neutral tool: it serves the files you give it. What determines its legality is the origin of the content, and in Spain the situation is reasonably clear:
| Content | Situation |
|---|---|
| Photos and videos you have recorded yourself | Yours, without any doubt |
| A copy of a DVD or Blu-ray you have bought | Grey area: private copying exists, but circumventing the copy protection is prohibited |
| Music from a CD you have bought, ripped | Private copy, generally accepted |
| Content bought in digital stores without DRM | Yours, according to the licence |
| Works in the public domain or under a free licence | Legitimate |
| Downloads of protected works from P2P networks | Not legitimate |
| Content from a subscription, downloaded by circumventing the DRM | Not legitimate |
This course does not at any point cover obtaining content protected by copyright. Everything that follows assumes the library contains your own material, legally acquired, or free material. If you have 4 TB of family videos, discs you have bought and classic cinema in the public domain, this project is exactly for you.
And an additional note: if you share the server with people outside your household, even with content you have bought, you are engaging in public communication, which is a different thing from private copying.
Objective, requirements and design decisions
Objective. A home server that serves the family library to televisions, phones, tablets and laptops inside the house; reachable from outside securely; with storage that tolerates the failure of one disk; with backups of what is irreplaceable; and with an electricity bill that does not hurt.
Design decisions, taken before buying anything:
| Decision | Choice | Why |
|---|---|---|
| Operating system | Ubuntu Server 24.04 LTS | The same as the course: everything you have learned applies |
| Graphical interface | None | It consumes RAM and adds attack surface for nothing |
| Media server | Jellyfin | Free, no account, no telemetry (section 5) |
| Storage | Btrfs RAID 1 over two disks | Checksums, snapshots and flexible expansion |
| Access from outside | VPN (08-04) | Zero exposed ports |
| Internal sharing | Samba, and NFS only if there are Linux clients | Universal compatibility |
| Management | Ansible | Rebuildable like srv-tramontana |
And a decision worth taking explicitly: this server is your second lab. Being yours and critical to nobody, it is where you can try things you would not try in production. That is one of the best reasons for building it.
We will call the machine srv-home, with the fixed IP 192.168.1.20 on the home network.
Choosing the hardware
| Option | Indicative price | Consumption | Transcoding | Storage | Noise |
|---|---|---|---|---|---|
| Raspberry Pi 5 (8 GB) | €80-120 | 4-8 W | Very limited; no hardware H.265 decoding | USB, no native SATA | Silent |
| Mini PC with an Intel N100 | €130-200 | 6-15 W | Excellent: modern Quick Sync, AV1 | 1-2 M.2 + SATA | Very low |
| Commercial NAS (Synology, QNAP) | €300-700 | 15-30 W | Varies by model | 2-4 bays | Low |
| Repurposed PC | €0 | 40-90 W | Depends on the graphics | Many bays | High |
| Second-hand server | €100-300 | 60-150 W | Usually no GPU | Many bays | Very high |
The recommendation is the mini PC with an Intel N100 or similar, and for a reason that becomes visible in the table in section 13: consumption. A server that is switched on 24 hours a day for a year consumes 8,760 hours of its average power. The difference between 10 W and 60 W is 438 kWh a year, some €105 in Spain at €0.24/kWh. In three years, the difference in electricity pays for the whole mini PC and then some.
About the Raspberry Pi 5, which is the most popular option and deserves an honest qualification: it is magnificent for a library of music, photos or video that the clients play directly without conversion. But the Pi 5 removed the hardware H.265 decoder that the Pi 4 had, and it has no video encoder. If any client needs transcoding — an old television, a phone on mobile data, an unsupported format — the Pi will do it in software, with a result that ranges from slow to unusable. If your library is H.264 and your clients are modern, the Pi is perfect; if you have H.265 4K material and an old television, it is not.
About repurposing an old PC: it is free and it is the worst option in the medium term. It consumes a lot, it makes noise, its fans are ten years old and its power supply is the part that fails most in a machine that is permanently switched on. As a lab for learning, perfect. As a permanent server, it works out expensive.
On RAM: 4 GB is enough for Jellyfin serving two or three clients. 8 GB gives headroom for the filesystem cache, which is what makes browsing the library feel fluid.
Storage: RAID, Btrfs and the truth about backups
How much space you really need
| Content | Typical size |
|---|---|
| A song in FLAC | 30-50 MB |
| A complete album in FLAC | 400-600 MB |
| A photo from a modern phone | 3-8 MB |
| A RAW photo from a camera | 25-45 MB |
| A 1080p H.264 series episode | 1.2-2.5 GB |
| A 1080p H.265 series episode | 400-900 MB |
| A 1080p H.264 film | 4-12 GB |
| A 1080p H.265 film | 2-5 GB |
| A 4K HDR film | 25-60 GB |
| A complete Blu-ray copy | 25-45 GB |
A realistic family library — 20 years of photos, the music you have bought, a couple of hundred films and a few series — comes to around 4-8 TB. The practical rule: work out what you think you need and double it, because 4K and RAW photos grow faster than intuition suggests.
Why redundancy is worth it here
In 05-04 you set up LVM without RAID, because srv-tramontana is a VM whose storage is already redundant on the host and because its value is in the backups, not in the disk. At home the situation is the opposite: the disks are physical and they are yours, and there is irreplaceable content — the family photos — that cannot be downloaded or bought again.
| Level | Disks | Usable capacity | Tolerates | Comment |
|---|---|---|---|---|
| RAID 0 | 2+ | 100% | Nothing | It doubles the risk. Not at home |
| RAID 1 | 2 | 50% | 1 disk | The recommendation: simple and sufficient |
| RAID 5 | 3+ | (n−1)/n | 1 disk | Long, risky rebuild with large disks |
| RAID 6 | 4+ | (n−2)/n | 2 disks | For 6+ disks |
| RAID 10 | 4+ | 50% | 1 per pair | Fast, expensive in disks |
RAID 5 with disks of 8 TB or more deserves a warning. Rebuilding the array after changing a disk can take more than a day, during which the other disks work flat out reading every sector. That is precisely the moment when another disk is most likely to fail, and if it does, everything is lost. With two disks and RAID 1, the rebuild is a straightforward copy and the risk is far lower.
mdadm versus Btrfs and ZFS
mdadm + ext4 |
Btrfs | ZFS | |
|---|---|---|---|
| In the Ubuntu kernel | Yes | Yes | Yes (DKMS module) |
| Detects silent corruption | No | Yes: data and metadata checksums | Yes |
| Repairs it automatically | No | Yes, with RAID 1 | Yes |
| Snapshots | With LVM | Native, instantaneous | Native |
| Adding a disk of a different size | No | Yes | Difficult |
| Transparent compression | No | Yes (zstd) | Yes |
| RAID 5/6 | Solid | DO NOT USE: still considered unstable | Solid (RAIDZ) |
| RAM consumption | Low | Low | High: ~1 GB per TB with deduplication |
| Licence | GPL | GPL | CDDL: not distributed in the kernel |
The choice is Btrfs in RAID 1, and the decisive argument is checksumming. A classic RAID protects against the failure of a whole disk, which is noisy and obvious. It does not protect against silent corruption: a bit that flips on the disk through magnetic degradation, a firmware error, a faulty cable. mdadm cannot detect it — it has two different copies and does not know which is the good one — and that corrupted bit propagates into your backups. Btrfs keeps a checksum of every block: it detects the error, knows the other copy is correct and repairs it.
For family photos that are going to sit on a disk for thirty years, that is not a technical detail.
$ sudo apt install btrfs-progs smartmontools
# 1. Check the state of the disks BEFORE using them, even new ones
$ sudo smartctl -a /dev/sda | grep -E 'Model|Power_On_Hours|Reallocated'
Device Model: WDC WD80EFPX-68C4ZN0
9 Power_On_Hours 0x0032 100 100 000 Old_age Always - 4
5 Reallocated_Sector_Ct 0x0033 100 100 140 Pre-fail Always - 0
# 2. Create the filesystem in RAID 1 (data AND metadata)
$ sudo mkfs.btrfs -L media -d raid1 -m raid1 /dev/sda /dev/sdb
Label: media
UUID: 8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45
Number of devices: 2
Data,RAID1: 8.00TiB
Metadata,RAID1: 8.00GiB
# 3. Mount by UUID, never by /dev/sdX (05-04)
$ sudo mkdir -p /srv/media
$ blkid /dev/sda | grep -oP 'UUID="\K[^"]+'
8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45# /etc/fstab
# compress=zstd:3 -> compresses what is compressible; already compressed
# video is detected and skipped, so it costs nothing
# noatime -> do not write the access time on every read:
# fewer writes and a longer disk life
# nofail -> IF THE DISK IS NOT THERE, THE SYSTEM STILL BOOTS.
# Without this, a disconnected disk leaves the machine
# in emergency mode, with no network and no SSH: you
# have to go there with a keyboard and a monitor. At
# home, essential.
# x-systemd.device-timeout=10 -> do not wait 90 s for an absent disk
UUID=8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45 /srv/media btrfs \
defaults,compress=zstd:3,noatime,nofail,x-systemd.device-timeout=10 0 0$ sudo mount -a # ALWAYS after touching fstab (course convention)
$ findmnt /srv/media
TARGET SOURCE FSTYPE OPTIONS
/srv/media /dev/sda btrfs rw,noatime,compress=zstd:3,nofail
$ sudo btrfs filesystem usage /srv/media
Overall:
Device size: 14.55TiB
Device allocated: 2.02TiB
Used: 2.01TiB
Free (estimated): 6.26TiB (min: 6.26TiB)
Data,RAID1: Size:1.01TiB, Used:1.00TiB
/dev/sda 1.01TiB
/dev/sdb 1.01TiBNotice that Device size shows 14.55 TiB (both disks) but Free estimates 6.26 TiB: in RAID 1 every block is written twice. df -h gives confusing figures with Btrfs; always use btrfs filesystem usage.
The scrub: the operation that justifies Btrfs
# Verifies ALL the checksums and repairs what it can from the other copy
$ sudo btrfs scrub start -B /srv/media
scrub done for 8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45
Total to scrub: 2.01TiB
Rate: 178.42MiB/s
Error summary: csum=2
Corrected: 2
Uncorrectable: 0Corrected: 2. Two blocks were corrupt on one disk and Btrfs has repaired them with the good copy from the other. With mdadm those two blocks would still be there, corrupt, and they would probably have ended up in the backups. That is the entire argument, in one line of output.
The scrub is scheduled monthly with a timer, following the pattern from 05-05:
# /etc/systemd/system/btrfs-scrub.timer
[Unit]
Description=Monthly Btrfs scrub of /srv/media
[Timer]
OnCalendar=Sun *-*-01..07 03:00:00 # first Sunday of each month
Persistent=true
RandomizedDelaySec=1h
[Install]
WantedBy=timers.target# /etc/systemd/system/btrfs-scrub.service
[Unit]
Description=Btrfs scrub
[Service]
Type=oneshot
ExecStart=/usr/bin/btrfs scrub start -B -c idle -n 5 /srv/media
# -c idle -n 5: lowest I/O priority, so as not to disturb anybody who is
# watching a film at three in the morning
Nice=19
IOSchedulingClass=idleRAID is not a backup
It has to be said in plain words, because it is the most expensive domestic mistake:
RAID protects against the failure of a disk. It protects against nothing else.
| Threat | Does RAID 1 protect? |
|---|---|
| A disk breaks | Yes |
| Silent corruption of a block | Yes, with Btrfs; no with mdadm |
| You delete a folder by mistake | No: it is deleted from both disks |
| A file-encrypting attack hits the machine | No: it encrypts both disks |
| The power supply fails and takes the disks with it | No |
| Power surge, fire, flood, theft | No |
| An update that corrupts the filesystem | No |
Of the seven rows, RAID covers one and a half. That is why section 12 exists.
Jellyfin versus Plex and Emby
| Jellyfin | Plex | Emby | |
|---|---|---|---|
| Licence | GPL, free | Proprietary | Proprietary (used to be free) |
| Requires a cloud account | No | Yes, even for local use | Optional |
| Telemetry | None | Yes, and there have been controversies | Yes |
| Paid features | None: everything included | Plex Pass for HW, mobile, skipping intros | Emby Premiere |
| Hardware acceleration | Free | Requires a subscription | Requires a subscription |
| TV apps | Android TV, webOS, Tizen, Roku, Kodi | More, and more polished | In between |
| Works if the internet goes down | Yes, completely | Authentication can fail | Yes |
| Interface maturity | Good, somewhat rougher | Excellent | Good |
| Automatic metadata | Yes | Yes, slightly better | Yes |
The choice is Jellyfin, and there are three reasons that weigh more than a slightly less polished interface:
- It does not depend on anybody. Plex demands an account in its cloud even to watch a film from the sofa. The day Plex changes its business model, raises prices or shuts down — things that have happened to products of this kind — your library carries on working with Jellyfin exactly as before. It is the practical definition of why free software matters, and this lesson is a good place to see it.
- Hardware acceleration is free. In Plex and Emby it is a paid feature, and it is precisely the function that decides whether the server works or does not.
- No telemetry. What your family watches at home does not leave the house.
Installation and a hardened systemd unit
The official repository with the key in keyrings, exactly as in 05-03 — no apt-key, which is obsolete, and no piping a script into bash:
$ sudo install -m 0755 -d /etc/apt/keyrings
$ curl -fsSL https://repo.jellyfin.org/jellyfin_team.gpg.key | \
sudo gpg --dearmor -o /etc/apt/keyrings/jellyfin.gpg
$ sudo chmod 0644 /etc/apt/keyrings/jellyfin.gpg
$ cat <<EOF | sudo tee /etc/apt/sources.list.d/jellyfin.sources
Types: deb
URIs: https://repo.jellyfin.org/ubuntu
Suites: noble
Components: main
Architectures: amd64
Signed-By: /etc/apt/keyrings/jellyfin.gpg
EOF
$ sudo apt update && sudo apt install jellyfin
$ systemctl is-active jellyfin
activeThe mandatory check that the key is what it claims to be, before trusting the repository:
$ gpg --show-keys /etc/apt/keyrings/jellyfin.gpg | head -2
pub rsa4096 2018-11-08 [SC]
04B7DA6B1FE10D5E76D9D5A6F5B2F0D5A8E5D6C3That fingerprint is checked against the one published in the official documentation. It is a step almost everybody skips, and it is the only real defence against a compromised repository.
The hardened unit
The package ships a working but not very restrictive unit. Following the course's drop-in convention, it is not edited: it is complemented.
# /etc/systemd/system/jellyfin.service.d/override.conf
[Service]
# --- Filesystem isolation ---
ProtectSystem=strict
ProtectHome=true
PrivateTmp=true
# Only these paths are writable
ReadWritePaths=/var/lib/jellyfin /var/log/jellyfin /var/cache/jellyfin /etc/jellyfin
# The library, READ ONLY: Jellyfin has no reason whatsoever to be able
# to delete your films. It is the most important restriction of all.
ReadOnlyPaths=/srv/media
# --- Privileges ---
NoNewPrivileges=true
PrivateDevices=false # false: it needs /dev/dri for the GPU
DeviceAllow=/dev/dri/renderD128 rw
DeviceAllow=/dev/dri/card0 rw
CapabilityBoundingSet=
AmbientCapabilities=
RestrictSUIDSGID=true
# --- Kernel and memory ---
ProtectKernelTunables=true
ProtectKernelModules=true
ProtectKernelLogs=true
ProtectControlGroups=true
ProtectProc=invisible
MemoryDenyWriteExecute=false # .NET uses JIT compilation: not possible
LockPersonality=true
RestrictRealtime=true
RestrictNamespaces=true
# --- Network ---
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
IPAddressAllow=localhost 192.168.1.0/24 10.8.0.0/24
IPAddressDeny=any
# --- System calls ---
SystemCallFilter=@system-service
SystemCallFilter=~@privileged @resources @obsolete
SystemCallArchitectures=native
# --- Resource limits (cgroups, 07-05) ---
# Stops a runaway transcode from locking up the machine
CPUQuota=350%
MemoryMax=3G
IOWeight=50$ sudo systemctl daemon-reload && sudo systemctl restart jellyfin
$ systemd-analyze security jellyfin
→ Overall exposure level for jellyfin.service: 2.4 OKFrom 8.2 (dangerous) to 2.4, in the same league as the 1.6 of tramontana.service. It does not go lower for two legitimate reasons: it needs access to /dev/dri for the GPU, and .NET requires executable memory for its JIT compiler.
ReadOnlyPaths=/srv/media deserves a paragraph. Jellyfin reads the library; it does not write it. With this line, neither a bug in the application nor a remote vulnerability can delete twenty years of photos. It is a one-line defence with enormous value, and it is exactly the same reasoning you applied to tramontana.service in 05-05.
And the check that it works:
$ sudo -u jellyfin touch /srv/media/test
touch: cannot touch '/srv/media/test': Read-only file systemHardware acceleration for transcoding
What transcoding is and why it matters so much
When the client can play the file as it is, Jellyfin does direct play: it sends the bytes without touching them, at almost zero CPU cost. When it cannot — because the codec, the resolution, the container or the bandwidth do not suit it — Jellyfin transcodes: it decodes the video and encodes it again on the fly.
Transcoding a 1080p H.265 film to H.264 in software consumes several cores at 100% in real time. On a two-core mini PC, that means a single viewer saturates the machine and the video stutters. With hardware acceleration, the same work is done by a dedicated block of the chip at a CPU cost of 5%, and three or four streams can be served at once.
It is the difference between a server that works and one that does not. That is why the hardware has to be checked before it is bought.
| Technology | Manufacturer | Quality | Notes |
|---|---|---|---|
| Quick Sync (VAAPI/QSV) | Intel iGPU | Very good | The best consumption/performance ratio |
| NVENC/NVDEC | NVIDIA | Very good | A limit on simultaneous streams on consumer cards |
| VAAPI on AMD | AMD | Good | Somewhat more uneven support |
| V4L2 M2M | Raspberry Pi 4 | Acceptable | Not on the Pi 5 |
| Software (libx264) | CPU | Best quality | Not viable in real time on modest machines |
Checking whether the machine has it
$ sudo apt install vainfo intel-media-va-driver-non-free
# 1. Does the render device exist?
$ ls -l /dev/dri/
crw-rw---- 1 root video 226, 0 Aug 18 09:02 card0
crw-rw---- 1 root render 226, 128 Aug 18 09:02 renderD128
# 2. What can it do?
$ vainfo --display drm --device /dev/dri/renderD128 2>/dev/null | \
grep -E 'Driver version|VAProfileH264High|VAProfileHEVCMain|VAProfileAV1'
vainfo: Driver version: Intel iHD driver for Intel(R) Gen Graphics - 24.1.0
VAProfileH264High : VAEntrypointVLD (decodes)
VAProfileH264High : VAEntrypointEncSliceLP (encodes)
VAProfileHEVCMain : VAEntrypointVLD
VAProfileHEVCMain10 : VAEntrypointVLD
VAProfileAV1Profile0 : VAEntrypointVLDHow to read it: VAEntrypointVLD means hardware decoding; VAEntrypointEncSlice means encoding. You need both to transcode properly. This chip decodes H.264, H.265 (including 10-bit) and AV1, and encodes H.264: perfect for what is needed.
renderD128 belongs to the render group, so the service user has to be in it:
$ sudo usermod -aG render,video jellyfin
$ id jellyfin
uid=996(jellyfin) gid=996(jellyfin) groups=996(jellyfin),44(video),993(render)
$ sudo systemctl restart jellyfinAnd this connects with the hardened unit: DeviceAllow=/dev/dri/renderD128 rw is what lets the service, with PrivateDevices restricted, still see the GPU. Without that line, group membership is worth nothing — a detail that causes hours of bafflement.
Configuring and verifying
In the Jellyfin dashboard: Dashboard > Playback > Hardware acceleration = VAAPI, device /dev/dri/renderD128, enabling the decoding of H.264, HEVC, HEVC 10-bit and VP9, and ticking "Enable enhanced hardware decoding" and "Allow encoding in HEVC format".
The real verification is done by playing something that forces the conversion:
# With a playback in progress that is transcoding
$ ps -eo pcpu,comm | grep ffmpeg
6.2 ffmpeg
$ sudo intel_gpu_top -s 1000 | head -8
Freq MHz IRQ RC6 Power W IMC MiB/s
req act % gpu pkg rd wr
850 842 1284 12 3.21 8.94 412.1 188.4
ENGINES BUSY MI_SEM MI_WAIT
Render/3D 4.12% |█ | 0% 0%
Video 78.44% |███████████████ | 0% 0%
VideoEnhance 2.10% | | 0% 0%Video 78.44% with ffmpeg at 6.2% CPU: the GPU is doing the work. If ffmpeg appeared at 190% CPU and the video engine at 0%, the acceleration would not be working despite being ticked in the dashboard.
The comparison, measured:
| In software | With VAAPI | |
|---|---|---|
ffmpeg CPU (1080p H.265→H.264) |
185% | 6% |
| Simultaneous streams possible | 1, with stutters | 3-4 |
| Electricity consumption during the conversion | +35 W | +7 W |
| Visual quality at the same bit rate | Slightly better | Slightly worse |
And the best optimisation: not transcoding
Everything above is the safety net. The right strategy is for the conversion not to be needed:
- Store the material as H.264 in an MP4 container if your clients are varied: that is what everything plays directly.
- Use H.265 only if all your devices support it; it saves half the space.
- Embedded subtitles force transcoding even if the video is compatible: graphical formats (Blu-ray PGS) mean they have to be "burned" onto the image. External
.srtsubtitles are sent separately and force nothing. It is the number one cause of unexpected conversions. - The audio is usually the hidden culprit: a DTS-HD track the television does not understand forces the audio to be converted, even if the video passes through directly.
Organising the library and naming files
Jellyfin identifies content by querying public metadata databases, and to get it right it needs names it can interpret. With the correct structure, recognition is around 99%; with free-form names, you spend hours correcting things by hand.
/srv/media/
├── movies/
│ ├── The Third Man (1949)/
│ │ ├── The Third Man (1949) - 1080p.mkv
│ │ ├── The Third Man (1949).en.srt
│ │ └── poster.jpg
│ └── Metropolis (1927)/
│ └── Metropolis (1927) - 1080p.mkv
├── series/
│ └── Series Name (2019)/
│ ├── Season 01/
│ │ ├── Series Name S01E01.mkv
│ │ └── Series Name S01E02.mkv
│ └── Season 02/
│ └── Series Name S02E01.mkv
├── music/
│ └── Artist/
│ └── Album (2004)/
│ ├── 01 - First Track.flac
│ └── 02 - Second Track.flac
└── photos/
└── 2026/
└── 2026-07 Pyrenees Holiday/| Rule | Correct example | Problematic example |
|---|---|---|
| One folder per film, with the year | The Third Man (1949)/ |
movies/thirdman.mkv |
| The year in brackets | Metropolis (1927) |
Metropolis 1927 |
Series with SxxExx |
Series S01E03.mkv |
Series 1x3.mkv |
Season NN folders |
Season 01/ |
Series 1/ |
| Subtitles with a language code | film.en.srt |
subs.srt |
| Music: a folder per artist and album | Artist/Album (2004)/ |
Everything in one directory |
The year is the most important part: it distinguishes between remakes and films with the same title, which is the origin of most misidentifications.
And a practical recommendation: do not rename by hand. filebot (commercial) or tinyMediaManager (free) do the job. Or, with the tools from Module 3:
# See what it would do before doing it (the course's --dry-run convention)
$ for f in /srv/media/incoming/*.mkv; do
new="$(basename "$f" | sed -E 's/\.(1080p|720p|x264|WEB-DL)//gI; s/\./ /g')"
printf '%s\n -> %s\n' "$f" "$new"
donePermissions, following the group model from 05-01:
$ sudo groupadd -f media
$ sudo usermod -aG media jellyfin
$ sudo usermod -aG media "$USER"
$ sudo chown -R root:media /srv/media
$ sudo find /srv/media -type d -exec chmod 2775 {} + # SGID: inherits group
$ sudo find /srv/media -type f -exec chmod 0664 {} +The SGID bit on the directories (the 2 in 2775) makes everything created inside inherit the media group, without depending on the umask of whoever creates it. It is the same mechanism from 05-02.
Sharing: Samba for everything, NFS for Linux
Jellyfin serves the playback, but you also need access to the files: copying photos from a phone, adding content from a laptop, making backups.
| Samba (SMB) | NFS | |
|---|---|---|
| Clients | Windows, macOS, Linux, Android, iOS, TV | Linux and Unix, mainly |
| Authentication | Its own username and password | By the machine's IP (NFSv3) or Kerberos (v4) |
| Performance on the LAN | Very good | Slightly better, lower latency |
| Unix permissions | Emulated | Native |
| Configuration | Medium | Simple, if you trust the network |
| Encryption | Yes, SMB3 | Only with Kerberos or over a VPN |
| Home use | The default option | For mounting on other Linux machines |
The recommendation: Samba as the base, because a phone, a television and a Windows machine speak it without installing anything, and NFS only if you have Linux clients that are going to mount the library permanently.
# /etc/samba/smb.conf (after copying the original to .bak-2026-08-18)
[global]
workgroup = HOME
server string = Media server
security = user
map to guest = never # NO guests at all
# SMB1 is broken by design (WannaCry spread through it).
# SMB2 as a minimum; SMB3 adds encryption.
server min protocol = SMB2
client min protocol = SMB2
server smb encrypt = desired
# Listen ONLY on the LAN and the VPN, never on every interface
interfaces = 127.0.0.1 192.168.1.0/24 10.8.0.0/24
bind interfaces only = yes
hosts allow = 127.0.0.1 192.168.1.0/24 10.8.0.0/24
hosts deny = 0.0.0.0/0
# No printers: it removes attack surface and log noise
load printers = no
printing = bsd
printcap name = /dev/null
disable spoolss = yes
log level = 1
log file = /var/log/samba/log.%m
max log size = 1000
[media]
path = /srv/media
comment = Family library
browseable = yes
read only = yes # by default, READ ONLY
# Only these users can write
write list = @media
valid users = @media
create mask = 0664
directory mask = 2775
force group = media
vfs objects = recycle # recycle bin: saves you from deletions
recycle:repository = .recycle
recycle:keeptree = yes
recycle:versions = yes
[photos]
path = /srv/media/photos
valid users = @media
read only = no
create mask = 0664
directory mask = 2775
force group = media$ sudo apt install samba
$ testparm -s >/dev/null && echo "valid configuration" # Samba's nginx -t
valid configuration
# Samba users are INDEPENDENT of the system ones, even when the
# name is the same. They have to be created explicitly.
$ sudo smbpasswd -a marta
New SMB password: ********
$ sudo smbpasswd -e marta
$ sudo systemctl restart smbd nmbd
$ smbclient -L //192.168.1.20 -U marta
Sharename Type Comment
--------- ---- -------
media Disk Family library
photos DiskThree defensible decisions in that configuration: map to guest = never (anonymous access is the cause of most Samba incidents), read only = yes by default with an explicit write list, and the recycle bin, which has saved more family photos than any other option in this file.
And NFS, for the Linux laptop:
$ sudo apt install nfs-kernel-server
$ echo '/srv/media 192.168.1.0/24(ro,sync,no_subtree_check,root_squash)' | \
sudo tee -a /etc/exports
$ sudo exportfs -ra
$ sudo exportfs -v
/srv/media 192.168.1.0/24(ro,sync,no_subtree_check,root_squash)root_squash turns the client's root into nobody: without it, anybody with root on a laptop on the network would have root over your files. And ro, because Samba with real authentication is already there for writing.
And the firewall, following 06-03 — the LAN only:
$ sudo ufw allow from 192.168.1.0/24 to any app Samba
$ sudo ufw allow from 192.168.1.0/24 to any port 8096 proto tcp comment 'Jellyfin'
$ sudo ufw allow from 192.168.1.0/24 to any port 2049 proto tcp comment 'NFS'
$ sudo ufw status | head -6
Status: active
To Action From
-- ------ ----
Samba ALLOW 192.168.1.0/24
8096/tcp (Jellyfin) ALLOW 192.168.1.0/24
2049/tcp (NFS) ALLOW 192.168.1.0/24Access from outside the house: three options and their risk
You want to watch your library from a hotel. There are three ways, and they are not equivalent.
| Expose with a reverse proxy | VPN | Reverse tunnel | |
|---|---|---|---|
| Ports open on the router | 80 and 443 | One UDP | None |
| Attack surface | Jellyfin, exposed to the internet | The tunnel, with no TCP port | Depends on the provider |
| Requires a public IP | Yes | Yes | No: it works behind CGNAT |
| Configuration on each device | No | Yes, once | Depends |
| A Jellyfin flaw compromises you | Yes | No: it is not reachable | Yes |
| Access to other services at home | Only the published one | All of them | Only the published one |
| Complexity | Medium (08-01) | Medium (08-04) | Low |
| Dependence on third parties | None | None | High |
The recommendation is the VPN, and it is the next lesson. The reasoning:
Exposing Jellyfin to the internet means that any Jellyfin vulnerability is a vulnerability of your house. It is a large application, with its own authentication and a history of security flaws like any software of its size. Automated scanners find an open port 443 within hours. With the VPN, an attacker who does not have your private key cannot even check whether Jellyfin exists.
And there is an additional argument that usually settles the matter: with the VPN you also reach Samba, the router's admin panel and anything else you set up later, without publishing anything new.
If you decide to expose it anyway — there is a legitimate case: sharing with relatives who are not going to install a VPN — the non-negotiable minimum is:
# /etc/nginx/sites-available/jellyfin — only if you decide to expose it
server {
listen 443 ssl;
http2 on;
server_name media.mydomain.example;
ssl_certificate /etc/letsencrypt/live/media.mydomain.example/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/media.mydomain.example/privkey.pem;
ssl_protocols TLSv1.2 TLSv1.3;
add_header Strict-Transport-Security "max-age=63072000" always;
add_header X-Content-Type-Options "nosniff" always;
# Rate limit against brute force on the login form
limit_req zone=login burst=5 nodelay;
client_max_body_size 20M;
location / {
proxy_pass http://127.0.0.1:8096;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
# WebSocket: Jellyfin uses it for the playback state
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
# No buffering: this is streaming video
proxy_buffering off;
}
}It is literally the configuration from 08-01 with two additions — WebSocket and proxy_buffering off — and it demonstrates the point of this lesson: the work is the same. On top of that, fail2ban with a filter for the Jellyfin log, and strong passwords for every account, including the child's.
Downloads and automation
Briefly, because it is an area where the technical and the legal cross over.
There is an ecosystem known as arr — Sonarr, Radarr, Lidarr, Prowlarr — that automates the searching, downloading and organising of content. It is usually deployed with Docker Compose, with what you know from 07-05:
# ~/media/compose.yml (illustrative fragment)
services:
sonarr:
image: lscr.io/linuxserver/sonarr:latest
environment:
- PUID=1000
- PGID=1002 # the media group
- TZ=Europe/Madrid
volumes:
- ./sonarr-config:/config
- /srv/media/series:/series
ports:
- "127.0.0.1:8989:8989" # localhost ONLY: you reach it over the VPN
restart: unless-stoppedTwo observations that apply regardless of the use:
ports: "127.0.0.1:8989:8989"is the critical detail. Docker creates its owniptablesrules and bypassesufw, as you saw in 07-05: publishing plain8989:8989exposes the port to the whole network even thoughufwsays otherwise. Specifying the address avoids it.- These tools are perfectly legitimate for managing your own content, replacing a damaged file with your own backup, or downloading freely licensed material. Using them to obtain protected works is not, and this course does not cover that part.
A useful and unambiguous domestic use: the automatic backup of the photos on your phone. Immich or Nextcloud, in Docker, replace the cloud photo service with one of your own, and they are perhaps the best argument for having a server at home.
Backups of the library and of the configuration
Here 05-08 applies exactly, and there is a distinction to be made that saves money:
| Content | Irreplaceable | Strategy |
|---|---|---|
| Family photos and videos | Yes, absolutely | Full 3-2-1, off site |
| Scanned documents | Yes | Full 3-2-1 |
| Jellyfin configuration (users, watched state, lists) | Expensive to redo | A daily, small backup |
| Music ripped from your own CDs | Recoverable with effort | A local copy; off site optional |
| Films from your own discs | Recoverable with a lot of effort | RAID only; a copy if there is spare space |
Copying 6 TB of films to the cloud costs money every month and does not add much, because the original discs are still on the shelf. Copying 400 GB of family photos off site is non-negotiable, and it costs little. Making that distinction is the decision that makes the project sustainable.
#!/usr/bin/env bash
#
# home_backup.sh - Backups of the home media server
#
# Strategy:
# - Irreplaceable (photos, documents) -> restic, off site
# - Jellyfin configuration -> restic, daily
# - Films and music -> local Btrfs snapshot
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/common.sh"
readonly HOME_MEDIA="${HOME_MEDIA:-/srv/media}"
readonly HOME_SNAP="${HOME_SNAP:-/srv/media/.snapshots}"
export RESTIC_REPOSITORY="${RESTIC_REPOSITORY:-b2:mydomain-media:/}"
umask 077
local_snapshot() {
local name="${HOME_SNAP}/$(date +%F)"
[[ -d "$name" ]] && { log "today's snapshot already exists"; return 0; }
# Read-only snapshot: it takes no space until something changes
btrfs subvolume snapshot -r "$HOME_MEDIA" "$name" \
|| die 73 "could not create the snapshot"
log "snapshot created: $name"
# Retention: keep 7 daily ones
local surplus
mapfile -t surplus < <(find "$HOME_SNAP" -maxdepth 1 -type d -name '20*' \
| sort -r | tail -n +8)
local s
for s in "${surplus[@]:-}"; do
[[ -n "$s" ]] || continue
btrfs subvolume delete "$s" && log "old snapshot deleted: $s"
done
}
offsite_backup() {
require_command restic
RESTIC_PASSWORD="$(pass home/restic)" \
restic backup \
"${HOME_MEDIA}/photos" \
"${HOME_MEDIA}/documents" \
/var/lib/jellyfin \
/etc/jellyfin \
--exclude-caches \
--exclude '*/metadata/*' \
--tag home \
|| die 74 "restic failed"
RESTIC_PASSWORD="$(pass home/restic)" \
restic forget --keep-daily 7 --keep-weekly 4 --keep-monthly 12 --prune
}
verify() {
# An unverified backup is not a backup (05-08)
RESTIC_PASSWORD="$(pass home/restic)" \
restic check --read-data-subset=2% || die 74 "the restic verification failed"
log "verification correct"
}
main() {
require_command btrfs
local_snapshot
offsite_backup
verify
log "backup complete"
}
main "$@"Btrfs snapshots deserve an explanation, because they are the protection against human error that RAID does not give you. A snapshot is an instantaneous copy of the current state that takes up no space: it shares every block with the original and only diverges when something changes. If tomorrow you delete a photo folder by mistake, it is in /srv/media/.snapshots/2026-08-18/ without having cost a single byte.
$ sudo btrfs subvolume list /srv/media
ID 258 gen 4412 top level 5 path .snapshots/2026-08-16
ID 259 gen 4488 top level 5 path .snapshots/2026-08-17
ID 260 gen 4501 top level 5 path .snapshots/2026-08-18
# Recovering something deleted by mistake: it is a simple copy
$ sudo cp -a /srv/media/.snapshots/2026-08-17/photos/2026-07 /srv/media/photos/And the warning: a snapshot is not a backup. It lives on the same disks. It protects against human error, not against a hardware failure or a fire. That is why the script does both things.
Consumption, noise and temperature
Three factors that in a data centre are somebody else's problem and in your living room are yours.
The annual electricity cost
| Machine | Average power | kWh/year | Cost at €0.24/kWh |
|---|---|---|---|
| Raspberry Pi 5 + 1 SSD | 7 W | 61 | €15 |
| Mini PC N100 + 2 HDDs (spun down) | 13 W | 114 | €27 |
| Mini PC N100 + 2 HDDs (active) | 24 W | 210 | €50 |
| A 4-bay NAS | 28 W | 245 | €59 |
| Repurposed PC | 65 W | 569 | €137 |
| Second-hand server | 110 W | 964 | €231 |
The difference between the mini PC and the repurposed PC is €110 a year. That is the argument from section 3 turned into money.
$ sudo apt install powertop lm-sensors
$ sudo sensors-detect --auto >/dev/null
# Calibrate first (it takes a few minutes and measures each state)
$ sudo powertop --calibrate
$ sudo powertop --auto-tune # applies the recommended settings
$ sudo powertop --time=60 --csv=/tmp/power.csv >/dev/null
$ grep -A6 'Power est' /tmp/power.csv | head -8
The battery reports a discharge rate of 12.4 W
Usage Events/s Category Description
6.2 ms/s 14.1 Process ffmpeg
1.1 ms/s 8.4 Process jellyfinpowertop --auto-tune enables the power saving of USB, PCIe and SATA. A warning: it can switch off USB ports that are in occasional use. To make it persistent, a systemd service, not an rc.local:
# /etc/systemd/system/powertop.service
[Unit]
Description=PowerTOP power-saving settings
After=multi-user.target
[Service]
Type=oneshot
ExecStart=/usr/sbin/powertop --auto-tune
RemainAfterExit=true
[Install]
WantedBy=multi-user.targetSpinning the disks down when they are not in use
A 3.5-inch HDD consumes 6-9 W spinning and 0.5 W stopped. With two disks, spinning them down when nobody is watching anything saves some €30 a year — and, above all, it reduces the background noise.
$ sudo apt install hdparm
# -B 127: aggressive power management but WITHOUT parking the head every
# other minute (values < 128 allow spin-down; 254 disables it)
# -S 120: stop after 120 x 5 s = 10 minutes of inactivity
$ sudo hdparm -B 127 -S 120 /dev/sda# /etc/hdparm.conf — persistent across reboots
/dev/disk/by-id/ata-WDC_WD80EFPX-68C4ZN0_WD-CA0J1234 {
apm = 127
spindown_time = 120
}The trade-off has to be understood: every start from stopped wears the disk more than several hours of spinning, and it adds 5-8 seconds of waiting when the library is opened. With a spin-down time of 10 minutes and normal use in the evenings, the balance is favourable. With 5 minutes and somebody browsing the library on and off, the disk starts and stops twenty times a night, and that shortens its life. Never go below 10 minutes.
/dev/disk/by-id/ is used and not /dev/sda for the same reason as UUIDs in fstab: the sdX names change order between boots.
Temperature
$ sensors
coretemp-isa-0000
Package id 0: +42.0°C (high = +100.0°C, crit = +100.0°C)
$ sudo smartctl -A /dev/sda | grep -i temperature
194 Temperature_Celsius 0x0022 118 105 000 Old_age Always - 38| Component | Ideal | Acceptable | Worrying |
|---|---|---|---|
| CPU at idle | < 45 °C | < 60 °C | > 75 °C |
| CPU transcoding | < 70 °C | < 85 °C | > 90 °C |
| HDD | 30-40 °C | 25-45 °C | > 50 °C or < 20 °C |
| NVMe SSD | < 55 °C | < 70 °C | > 75 °C |
The large-scale reliability studies are clear: hard disks last less above 45 °C and also below 20 °C. In a closed cabinet in the living room, with no ventilation, two disks go past 50 °C easily. A 120 mm fan at low revolutions is practically inaudible and brings the temperature down by 8-10 °C.
Automation with Ansible
# ~/home-infra/roles/media/defaults/main.yml
---
media_mount_point: /srv/media
media_uuid: 8f3a2c11-4d5e-4a91-b7c2-1e9d0f8a3b45
media_group: media
media_gid: 1002
media_lan_net: 192.168.1.0/24
media_vpn_net: 10.8.0.0/24
media_samba_users: [marta, luis]
media_hw_accel: true
media_gpu_device: /dev/dri/renderD128
media_hdparm_spindown: 120
media_subdirs: [movies, series, music, photos, documents]# ~/home-infra/roles/media/tasks/main.yml
---
- name: Install the base packages
ansible.builtin.apt:
name:
- btrfs-progs
- smartmontools
- samba
- hdparm
- powertop
- restic
state: present
update_cache: true
tags: [packages]
- name: Hardware acceleration packages
ansible.builtin.apt:
name: [vainfo, intel-media-va-driver-non-free]
state: present
when: media_hw_accel | bool
tags: [gpu]
- name: Jellyfin repository key in keyrings
ansible.builtin.get_url:
url: https://repo.jellyfin.org/jellyfin_team.gpg.key
dest: /etc/apt/keyrings/jellyfin.asc
mode: '0644'
tags: [packages]
- name: Jellyfin repository
ansible.builtin.deb822_repository:
name: jellyfin
types: deb
uris: https://repo.jellyfin.org/ubuntu
suites: "{{ ansible_distribution_release }}"
components: main
architectures: amd64
signed_by: /etc/apt/keyrings/jellyfin.asc
register: jellyfin_repo
tags: [packages]
- name: Install Jellyfin
ansible.builtin.apt:
name: jellyfin
state: present
update_cache: "{{ jellyfin_repo.changed }}"
tags: [packages]
# --- Storage ---
- name: Check that the filesystem exists before mounting it
ansible.builtin.command: "blkid -U {{ media_uuid }}"
register: blk
changed_when: false
failed_when: blk.rc != 0
- name: Mount the library by UUID with nofail
ansible.posix.mount:
path: "{{ media_mount_point }}"
src: "UUID={{ media_uuid }}"
fstype: btrfs
opts: defaults,compress=zstd:3,noatime,nofail,x-systemd.device-timeout=10
state: mounted
- name: The library group
ansible.builtin.group:
name: "{{ media_group }}"
gid: "{{ media_gid }}"
state: present
- name: Directory structure with SGID
ansible.builtin.file:
path: "{{ media_mount_point }}/{{ item }}"
state: directory
owner: root
group: "{{ media_group }}"
mode: '2775'
loop: "{{ media_subdirs }}"
- name: Add jellyfin to the necessary groups
ansible.builtin.user:
name: jellyfin
groups: "{{ [media_group] + (['render', 'video'] if media_hw_accel else []) }}"
append: true
notify: Restart jellyfin
- name: Hardening drop-in for jellyfin
ansible.builtin.template:
src: jellyfin-override.conf.j2
dest: /etc/systemd/system/jellyfin.service.d/override.conf
owner: root
group: root
mode: '0644'
notify:
- Reload systemd
- Restart jellyfin
- name: Samba configuration
ansible.builtin.template:
src: smb.conf.j2
dest: /etc/samba/smb.conf
owner: root
group: root
mode: '0644'
backup: true
validate: 'testparm -s %s' # Samba's nginx -t (08-01)
notify: Restart samba
- name: Firewall - the LAN and the VPN only
community.general.ufw:
rule: allow
src: "{{ item.0 }}"
port: "{{ item.1 }}"
proto: tcp
loop: "{{ [media_lan_net, media_vpn_net] | product(['445', '8096']) | list }}"
tags: [firewall]
- name: Scrub, SMART and backup timers
ansible.builtin.copy:
src: "{{ item }}"
dest: "/etc/systemd/system/{{ item }}"
mode: '0644'
loop:
- btrfs-scrub.service
- btrfs-scrub.timer
- home-backup.service
- home-backup.timer
notify: Reload systemd
- name: Enable the timers
ansible.builtin.systemd:
name: "{{ item }}"
enabled: true
state: started
daemon_reload: true
loop: [btrfs-scrub.timer, home-backup.timer, smartd.service]
# --- Verification ---
- name: Verify that hardware acceleration is visible
ansible.builtin.command: "vainfo --display drm --device {{ media_gpu_device }}"
register: va
changed_when: false
failed_when: "'VAProfileH264High' not in va.stdout"
when: media_hw_accel | bool
tags: [verify]
- name: Verify that Jellyfin responds
ansible.builtin.uri:
url: "http://127.0.0.1:8096/health"
status_code: 200
retries: 5
delay: 3
tags: [verify]That validate: 'testparm -s %s' is the same pattern as nginx -t -c %s in 08-01, and it protects against the same problem: an invalid configuration that stops the service from starting. The pattern repeats because it is correct, not by coincidence.
Operation: updates, disk health and what to do when one fails
Updates
# Unattended security updates (06-06)
$ sudo apt install unattended-upgrades
$ sudo dpkg-reconfigure -plow unattended-upgradesWith one difference from srv-tramontana: here it is worth allowing the automatic reboot, because there is no maintenance window to negotiate and nobody to warn.
# /etc/apt/apt.conf.d/50unattended-upgrades
Unattended-Upgrade::Automatic-Reboot "true";
Unattended-Upgrade::Automatic-Reboot-Time "05:30";
Unattended-Upgrade::Automatic-Reboot-WithUsers "false";Automatic-Reboot-WithUsers "false" avoids rebooting if somebody is logged in — but it does not detect somebody watching a film, so 05:30 is a deliberate choice.
Jellyfin is updated with apt like any other package. Before a major update, take a snapshot:
$ sudo btrfs subvolume snapshot -r /var/lib/jellyfin \
/srv/media/.snapshots/jellyfin-pre-$(date +%F)Disk health
$ sudo systemctl enable --now smartd
# A short test weekly, a long one monthly, and an email alert
$ sudo tee /etc/smartd.conf <<'EOF'
DEVICESCAN -a -o on -S on -n standby,q \
-s (S/../.././02|L/../(01|15)/./03) \
-W 4,45,50 \
-m root -M exec /usr/share/smartmontools/smartd-runner
EOF
$ sudo systemctl restart smartd| Parameter | Meaning |
|---|---|
-n standby,q |
Do not wake a stopped disk in order to check it |
-s (S/../.././02|L/../(01|15)/./03) |
A short test daily at 02:00; a long one on the 1st and 15th at 03:00 |
-W 4,45,50 |
Warn if it rises 4 °C suddenly, if it goes past 45 °C, critical at 50 °C |
The attributes that really predict a failure, according to the large-scale reliability studies:
$ sudo smartctl -A /dev/sda | \
awk '$1 ~ /^(5|187|188|197|198)$/ {printf "%-28s %s\n", $2, $10}'
Reallocated_Sector_Ct 0
Reported_Uncorrect 0
Command_Timeout 0
Current_Pending_Sector 0
Offline_Uncorrectable 0| Attribute | What it indicates | Action threshold |
|---|---|---|
| 5 Reallocated_Sector_Ct | Bad sectors reallocated | > 0: watch it. Growing: replace it |
| 197 Current_Pending_Sector | Suspect sectors not yet reallocated | > 0: replace it soon |
| 198 Offline_Uncorrectable | Unreadable sectors | > 0: replace it now |
| 187 Reported_Uncorrect | Uncorrectable errors | > 0: watch it |
| 188 Command_Timeout | Commands that timed out | Usually the cable, not the disk |
A disk with a growing Current_Pending_Sector is dying, even if SMART says PASSED. SMART's overall verdict is notoriously optimistic: look at the attributes, not at the summary.
When a disk fails
# The symptom: errors in the journal
$ sudo journalctl -k --since today | grep -iE 'ata[0-9]|I/O error|medium error'
kernel: ata2.00: exception Emask 0x0 SAct 0x0 SErr 0x0 action 0x0
kernel: blk_update_request: I/O error, dev sdb, sector 1928374656
$ sudo btrfs device stats /srv/media
[/dev/sda].write_io_errs 0
[/dev/sda].read_io_errs 0
[/dev/sda].corruption_errs 0
[/dev/sdb].write_io_errs 142
[/dev/sdb].read_io_errs 2891
[/dev/sdb].corruption_errs 18Replacement procedure with Btrfs RAID 1:
# 1. DO NOT unmount or power off. With RAID 1 the system keeps working.
# First, check that there is a backup of what is irreplaceable.
$ RESTIC_PASSWORD=$(pass home/restic) restic snapshots --tag home | tail -3
# 2. If the disk STILL responds: hot replacement (the ideal case).
# Btrfs copies what it can from the old disk and the rest from the mirror.
$ sudo btrfs replace start -f /dev/sdb /dev/sdc /srv/media
$ sudo btrfs replace status /srv/media
0.8% done, 0 write errs, 0 uncorr. read errs
# 3. If the disk NO LONGER responds: mount degraded and add a new one
$ sudo mount -o degraded,compress=zstd:3 UUID=8f3a2c11-... /srv/media
$ sudo btrfs device add /dev/sdc /srv/media
$ sudo btrfs device remove missing /srv/media
$ sudo btrfs balance start -dconvert=raid1 -mconvert=raid1 /srv/media
# 4. Verify that the redundancy has been restored
$ sudo btrfs filesystem df /srv/media
Data, RAID1: total=1.01TiB, used=1.00TiB
Metadata, RAID1: total=8.00GiB, used=6.12GiB
# 5. A full scrub to confirm that everything is intact
$ sudo btrfs scrub start -B /srv/mediaThree warnings about this procedure:
btrfs replaceis preferable toadd+remove: it is faster and it keeps the redundancy for more of the process.- Degraded mode has a limit: with a two-disk RAID 1, a second failure during the rebuild loses everything. It is the point of greatest risk in the array's life cycle, and the reason why off-site backups exist.
- Replace the disk when SMART warns you, not when it fails. A hot replacement of a disk that can still be read is a formality; a rebuild from a dead disk is a risk.
The domestic routine
| Frequency | Task | Tool |
|---|---|---|
| Automatic | Security updates | unattended-upgrades |
| Automatic | Daily backup of the irreplaceable | home_backup.sh + timer |
| Automatic | Daily snapshot | Btrfs |
| Daily | Short SMART test | smartd |
| Weekly (2 min) | Review the warnings and the space | btrfs filesystem usage |
| Monthly (10 min) | Btrfs scrub and long SMART test | Timers |
| Quarterly (20 min) | Restore a file from the backup | restic restore |
| Annually | Check the dust, the fans and the temperatures | Physically |
The quarterly row is the same as in 08-02 and for the same reason: a backup that has never been restored is a file about which we assume things. At home it is skipped even more often than at work.
Common Mistakes and Tips
- Believing that RAID is a backup. It protects against the failure of a disk and against nothing else: not deletions, not encrypting malware, not fires.
- Using Btrfs in RAID 5 or 6. They are still considered unstable. RAID 1, or
mdadmif you need parity. - Mounting without
nofail. A disconnected disk leaves the machine in emergency mode, with no network and no SSH, and you have to go there with a keyboard and a monitor. - Mounting by
/dev/sdX. The order changes between boots. Always the UUID. - Not checking hardware acceleration before buying. It is what decides whether the server works. The Pi 5 has no H.265 decoder.
- Forgetting
DeviceAllowin the hardened unit. The user is in therendergroup and still cannot see the GPU, and the bafflement lasts hours. - Not putting
ReadOnlyPathsover the library. It is one line that stops a bug in the application from deleting twenty years of photos. - Allowing guests in Samba.
map to guest = never. Anonymous access is the cause of most incidents. - Leaving SMB1 enabled. It is broken by design; it is how WannaCry spread.
- NFS without
root_squash. Anybody with root on a laptop on the network has root over your files. - Publishing ports with Docker believing that
ufwcovers them. Docker writes its own rules and bypasses them. Use127.0.0.1:port:port. - Exposing Jellyfin directly to the internet. A vulnerability of its own becomes a vulnerability of your house. Use the VPN from 08-04.
- Setting the disk spin-down time below 10 minutes. They start and stop twenty times a night, and that kills them sooner.
- Trusting SMART's
PASSEDverdict. It is optimistic. Look at attributes 5, 197 and 198. - Putting the server in a closed cabinet. Above 45 °C, disks last significantly less.
- Copying 6 TB of films to the cloud. It costs money every month and the original discs are still on the shelf. Copy what is irreplaceable.
- A tip on method. Treat this server with the same rigour as the one at work — Ansible, verified backups, hardened units — and you will gain two things: a server that lasts, and a lab where you can practise without risk.
Exercises
Exercise 1
A member of the family complains that films "stutter" on the living-room television, but they play perfectly on the tablet. Diagnose the problem from beginning to end and solve it.
Exercise 2
Design the complete backup strategy for a home server with 6 TB in use: 400 GB of photos and documents, 800 GB of music ripped from your own CDs and 4.8 TB of films from discs you have bought. Justify each decision with its cost.
Exercise 3
Write a script home_check.sh following the course's conventions that checks the health of the home server and returns 0, 1 or 2.
Solutions
Solution 1
Step-by-step diagnosis. The starting fact is valuable: the same content plays well on one device and badly on another, so the problem is not in the file or in the disk, but in the relationship between the file and that particular client.
# 1. Is it transcoding? The Jellyfin dashboard says so, and so does this:
$ ps -eo pid,pcpu,args | grep '[f]fmpeg' | head -1
8814 191.2 /usr/lib/jellyfin-ffmpeg/ffmpeg -i /srv/media/movies/...
-c:v libx264 -preset veryfast -b:v 3000000 ...191.2% CPU and -c:v libx264: it is transcoding in software, saturating both cores. That is the immediate cause of the stuttering.
# 2. Why is it transcoding? Analyse the file
$ ffprobe -v error -show_entries stream=index,codec_type,codec_name,width,height,channels \
-of csv=p=0 "/srv/media/movies/Deep Water (2019)/Deep Water (2019).mkv"
0,video,hevc,3840,2160
1,audio,dts,8
2,subtitle,hdmv_pgs_subtitle
# 3. What does the television support? In the Jellyfin log, after playing:
$ sudo journalctl -u jellyfin --since "10 min ago" | grep -i 'transcod\|reason'
[INF] Transcoding reason: VideoCodecNotSupported, AudioCodecNotSupported,
SubtitleCodecNotSupportedThree accumulated reasons, and they have to be attacked separately:
| Reason | Detail | Solution |
|---|---|---|
VideoCodecNotSupported |
HEVC 4K; the television only does H.264 1080p | Hardware acceleration or a second version |
AudioCodecNotSupported |
DTS 7.1; the television does AC3 5.1 | Transcode the audio only (cheap) |
SubtitleCodecNotSupported |
PGS: a graphical subtitle that has to be burned onto the image | Extract it to an external .srt |
And the check that rules out the other usual suspect:
# 4. Is it the network? The living-room Wi-Fi versus a cable
$ iperf3 -c 192.168.1.45 -t 10 -R
[ 5] 0.00-10.00 sec 58.2 MBytes 48.8 Mbits/sec48.8 Mbit/s is enough for a 1080p stream (around 10 Mbit/s) but not for 4K without conversion (40-80 Mbit/s). So the network is a real secondary factor: even if we fixed the codecs, 4K over that Wi-Fi would be touch and go.
Resolution, in four measures ordered by benefit/effort ratio:
# --- MEASURE 1: enable hardware acceleration (immediate impact) ---
$ vainfo --display drm --device /dev/dri/renderD128 | grep -c EncSlice
6
$ sudo usermod -aG render,video jellyfin
$ sudo systemctl restart jellyfin
# And in the dashboard: Playback > VAAPI, /dev/dri/renderD128,
# ticking HEVC, HEVC 10-bit, VP9 and "HEVC encoding"
# Verify the effect
$ ps -eo pcpu,args | grep '[f]fmpeg' | awk '{print $1}'
5.8
$ sudo intel_gpu_top -s 1000 | grep Video
Video 74.12% |██████████████ |From 191% CPU to 5.8%. The stuttering disappears immediately.
# --- MEASURE 2: extract the subtitles to an external file ---
# Graphical PGS subtitles force the VIDEO to be transcoded even when the
# codec is compatible: they have to be "burned" onto the image. The .srt
# ones are sent separately and the client draws them.
$ ffmpeg -i "Deep Water (2019).mkv" -map 0:s:0 -c:s srt "Deep Water (2019).en.srt"# --- MEASURE 3: convert only the problematic audio, without touching the video ---
# -c:v copy does not re-encode the video: it is seconds, not hours, and
# with no loss of image quality whatsoever.
$ ffmpeg -i input.mkv -c:v copy -c:s copy \
-c:a ac3 -b:a 640k -ac 6 output.mkv# --- MEASURE 4: an Ethernet cable or powerline to the living room ---
$ iperf3 -c 192.168.1.45 -t 10 -R
[ 5] 0.00-10.00 sec 1.09 GBytes 938 Mbits/secThe result, measured before and after:
| Metric | Before | After |
|---|---|---|
ffmpeg CPU |
191% | 5.8% |
| Stutters in 30 min of playback | 14 | 0 |
| Simultaneous streams possible | 1, with stutters | 3-4 |
| Consumption during playback | 48 W | 17 W |
| Direct play | No | Yes, if there is a 1080p H.264 version |
And the underlying lesson, which goes beyond this case: the best transcode is the one that does not happen. For a library with heterogeneous clients, the definitive strategy is to keep two versions of the important material — the original 4K HEVC for the modern television and a 1080p H.264 version with AC3 audio for everything else — which Jellyfin offers automatically. It costs 30% more space and eliminates the conversion completely.
Solution 2
The principle that organises the whole strategy: not all content is worth the same. Copying everything with the same criterion is expensive and, paradoxically, usually ends with nothing being copied at all.
| Category | Volume | Can it be recovered? | Cost of recovering it | Value |
|---|---|---|---|---|
| Photos and documents | 400 GB | Never | Infinite | Maximum |
| Music from your own CDs | 800 GB | Yes, by ripping them again | ~40 h of work | Medium |
| Films from your own discs | 4.8 TB | Yes, by copying them again | ~200 h of work | Low |
| Jellyfin configuration | 2 GB | Yes, by reconfiguring | ~4 h | Medium |
A tiered strategy, applying 3-2-1 where it belongs:
TIER 1 - Irreplaceable (402 GB): photos, documents, configuration
Copy 1: local Btrfs RAID 1 (the original)
Copy 2: encrypted external USB disk, monthly, in a drawer
Copy 3: restic in the cloud, daily, encrypted end to end
-> 3 copies, 2 media, 1 off site. Full 3-2-1.
TIER 2 - Recoverable with effort (800 GB): music
Copy 1: local Btrfs RAID 1
Copy 2: the same external USB disk, monthly
-> No cloud copy: 800 GB a month is not worth 40 h of work.
TIER 3 - Recoverable (4.8 TB): films
Copy 1: local Btrfs RAID 1
Copy 2: the original discs on the shelf (you already have it)
-> No additional copy. RAID covers the failure of one disk and the
originals cover the rest.The economic calculation, which is the argument:
| Strategy | Volume in the cloud | Annual cost (at €0.005/GB/month) | Comment |
|---|---|---|---|
| Copy everything | 6,000 GB | €360/year | Unsustainable at home |
| Tier 1 only | 402 GB | €24/year | Recommended |
| Nothing in the cloud | 0 GB | €0 | A fire takes it all |
Twenty-four euros a year protect what is irreplaceable. Three hundred and sixty would also protect some films that are on the shelf. The difference between those two figures is the reason why many people end up with no backup at all: they try to copy everything, see the price and give up.
Implementation:
# --- Tier 1: restic in the cloud, daily ---
$ export RESTIC_REPOSITORY="b2:mydomain-media:/"
$ restic init # once only
# The passphrase, in pass (06-05). If it is lost, the backup is
# UNREADABLE: there is no possible recovery. It also goes on paper, at a
# relative's house, inside a sealed envelope.
$ pass generate home/restic 40# --- Tiers 1 and 2: encrypted external USB disk, monthly ---
# LUKS (06-05): if you lose the disk, the data is not readable
$ sudo cryptsetup luksFormat /dev/sdd1
$ sudo cryptsetup open /dev/sdd1 backup-external
$ sudo mkfs.btrfs -L backup /dev/mapper/backup-external
# The monthly copy. --delete is used WITH CARE: if the source is
# unmounted, rsync would delete the whole destination. The prior check
# is not optional.
$ mountpoint -q /srv/media || { echo "source not mounted; aborting"; exit 1; }
$ sudo rsync -aHAX --delete --info=progress2 \
/srv/media/{photos,documents,music}/ /mnt/backup/
$ sudo umount /mnt/backup && sudo cryptsetup close backup-externalVerification, without which none of the above counts:
# Automatic, weekly
$ restic check --read-data-subset=5%
# Manual, quarterly: really restore a file and OPEN it
$ restic restore latest --include '/srv/media/photos/2019' --target /tmp/test
$ sha256sum /tmp/test/srv/media/photos/2019/IMG_4412.jpg \
/srv/media/photos/2019/IMG_4412.jpg
a3f1... /tmp/test/srv/media/photos/2019/IMG_4412.jpg
a3f1... /srv/media/photos/2019/IMG_4412.jpgThe recovery table, which is what you have to have written down and kept off the server:
| Scenario | What is lost | Where it is recovered from | Time |
|---|---|---|---|
| A disk fails | Nothing | RAID 1: hot replacement | 6-12 h of rebuilding |
| You delete a folder | Nothing | The day's Btrfs snapshot | 2 minutes |
| A file becomes corrupt | Nothing | Btrfs repairs it by itself in the scrub | Automatic |
| Encrypting malware hits the machine | Tier 3 | Cloud (tier 1) + USB (1 and 2) + original discs | 2-3 days |
| Fire or theft | Tiers 2 and 3 | Cloud: tier 1 only | 1-2 days for 400 GB |
That last row is the moment of truth for the whole strategy, and it is honest: in a fire the films and the music are lost. That is consciously accepted, because the original discs would probably burn too and the cost of avoiding it is €336 a year. What matters — the family's photos — survives, which is exactly what it was designed for.
Solution 3
#!/usr/bin/env bash
#
# home_check.sh - Health check of the home media server
#
# Exit codes:
# 0 = correct 1 = warning 2 = critical
#
# Designed to run from a daily timer, silent when all is well:
# only a state != 0 produces a notification.
#
set -euo pipefail
readonly SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/lib/common.sh"
readonly HOME_MEDIA="${HOME_MEDIA:-/srv/media}"
readonly HOME_DISKS="${HOME_DISKS:-/dev/sda /dev/sdb}"
readonly HOME_SPACE_THRESHOLD="${HOME_SPACE_THRESHOLD:-85}"
readonly HOME_TEMP_THRESHOLD="${HOME_TEMP_THRESHOLD:-45}"
readonly HOME_BACKUP_HOURS="${HOME_BACKUP_HOURS:-36}"
readonly HOME_SCRUB_DAYS="${HOME_SCRUB_DAYS:-45}"
umask 027
overall_status=0
report() {
local level="$1" message="$2"
case "$level" in
ok) log "OK $message" ;;
warn) error "WARN $message"; (( overall_status < 1 )) && overall_status=1 ;;
critical) error "CRITICAL $message"; overall_status=2 ;;
esac
return 0
}
check_mount() {
if ! mountpoint -q "$HOME_MEDIA"; then
report critical "$HOME_MEDIA is NOT mounted"
return 0
fi
report ok "library mounted"
# With 'nofail', an absent disk does NOT stop the machine booting: the
# presence of both devices has to be checked explicitly.
local n
n="$(btrfs filesystem show "$HOME_MEDIA" | grep -c '^\s*devid')"
if (( n < 2 )); then
report critical "only $n device(s): the RAID 1 is DEGRADED"
else
report ok "RAID 1 with $n devices"
fi
}
check_space() {
mountpoint -q "$HOME_MEDIA" || return 0
# 'df' lies with Btrfs in RAID: btrfs's own output is used
local free_b total_b used_pct
free_b="$(btrfs filesystem usage -b "$HOME_MEDIA" | \
awk '/Free \(estimated\)/{gsub(/[^0-9]/,"",$3); print $3}')"
total_b="$(btrfs filesystem usage -b "$HOME_MEDIA" | \
awk '/Device size/{gsub(/[^0-9]/,"",$3); print $3/2}')"
used_pct=$(( 100 - (free_b * 100 / total_b) ))
if (( used_pct >= 95 )); then
report critical "library at ${used_pct}% ($(format_bytes "$free_b") free)"
elif (( used_pct >= HOME_SPACE_THRESHOLD )); then
report warn "library at ${used_pct}%"
else
report ok "space at ${used_pct}% ($(format_bytes "$free_b") free)"
fi
}
check_btrfs_errors() {
mountpoint -q "$HOME_MEDIA" || return 0
local total
total="$(btrfs device stats "$HOME_MEDIA" | awk '{s+=$2} END {print s+0}')"
if (( total > 0 )); then
report critical "btrfs has accumulated $total I/O or corruption errors"
btrfs device stats "$HOME_MEDIA" | awk '$2>0 {print " " $0}' >&2
else
report ok "no I/O or corruption errors"
fi
}
check_scrub() {
mountpoint -q "$HOME_MEDIA" || return 0
local when days
when="$(btrfs scrub status "$HOME_MEDIA" | \
awk -F': ' '/Scrub started/{print $2}')" || true
if [[ -z "${when:-}" ]]; then
report warn "there is no record of any scrub having run"
return 0
fi
days=$(( ( $(date +%s) - $(date -d "$when" +%s) ) / 86400 ))
if (( days > HOME_SCRUB_DAYS )); then
report warn "last scrub $days days ago"
else
report ok "last scrub $days days ago"
fi
}
check_disks() {
require_command smartctl
local d
for d in $HOME_DISKS; do
[[ -b "$d" ]] || { report critical "$d does not exist"; continue; }
# -n standby: do NOT wake a stopped disk just to look at it
local output
output="$(smartctl -A -H -n standby "$d" 2>/dev/null)" || true
if grep -q 'STANDBY' <<<"$output"; then
report ok "$d in standby (not woken up)"
continue
fi
# The attributes that predict failures, not the overall verdict
local realloc pending unreadable temp
realloc="$(awk '$1==5 {print $10+0}' <<<"$output")"
pending="$(awk '$1==197 {print $10+0}' <<<"$output")"
unreadable="$(awk '$1==198 {print $10+0}' <<<"$output")"
temp="$(awk '$1==194 {print $10+0}' <<<"$output")"
if (( ${unreadable:-0} > 0 || ${pending:-0} > 0 )); then
report critical "$d: $pending pending, $unreadable unreadable: REPLACE IT"
elif (( ${realloc:-0} > 0 )); then
report warn "$d: $realloc reallocated sectors: watch it"
else
report ok "$d with no bad sectors"
fi
if (( ${temp:-0} >= 50 )); then
report critical "$d at ${temp}C"
elif (( ${temp:-0} >= HOME_TEMP_THRESHOLD )); then
report warn "$d at ${temp}C (ideal < 45)"
fi
done
}
check_services() {
local s
for s in jellyfin smbd smartd; do
systemctl is-active --quiet "$s" \
&& report ok "$s active" \
|| report critical "$s is NOT active"
done
# That it responds, not just that the process exists
if curl -sf -m 5 -o /dev/null http://127.0.0.1:8096/health; then
report ok "jellyfin responds"
else
report critical "jellyfin does not respond on 8096"
fi
}
check_backups() {
local marker=/var/lib/home-backup/last-successful
if [[ ! -f "$marker" ]]; then
report critical "there is no record of any successful backup"
return 0
fi
local hours
hours=$(( ( $(date +%s) - $(stat -c %Y "$marker") ) / 3600 ))
if (( hours > HOME_BACKUP_HOURS * 2 )); then
report critical "last successful backup ${hours} h ago"
elif (( hours > HOME_BACKUP_HOURS )); then
report warn "last successful backup ${hours} h ago"
else
report ok "last successful backup ${hours} h ago"
fi
}
check_acceleration() {
[[ -e /dev/dri/renderD128 ]] || { report warn "no GPU reachable"; return 0; }
if vainfo --display drm --device /dev/dri/renderD128 2>/dev/null | \
grep -q VAEntrypointEncSlice; then
report ok "hardware acceleration available"
else
report warn "the GPU does not expose hardware encoding"
fi
}
main() {
require_command btrfs
require_command curl
check_mount
check_space
check_btrfs_errors
check_scrub
check_disks
check_services
check_backups
check_acceleration
case "$overall_status" in
0) log "home media server correct" ;;
1) error "check completed with WARNINGS" ;;
2) error "check completed in a CRITICAL state" ;;
esac
return "$overall_status"
}
main "$@"$ shellcheck ~/scripts/home_check.sh && echo "no warnings"
no warnings
$ ~/scripts/home_check.sh; echo "status: $?"
[2026-08-18 08:00:03] OK library mounted
[2026-08-18 08:00:03] OK RAID 1 with 2 devices
[2026-08-18 08:00:03] OK space at 28% (6.3 TiB free)
[2026-08-18 08:00:04] OK no I/O or corruption errors
[2026-08-18 08:00:04] OK last scrub 12 days ago
[2026-08-18 08:00:04] OK /dev/sda in standby (not woken up)
[2026-08-18 08:00:04] OK /dev/sdb in standby (not woken up)
[2026-08-18 08:00:05] OK jellyfin active
[2026-08-18 08:00:05] OK smbd active
[2026-08-18 08:00:05] OK smartd active
[2026-08-18 08:00:05] OK jellyfin responds
[2026-08-18 08:00:05] OK last successful backup 6 h ago
[2026-08-18 08:00:06] OK hardware acceleration available
[2026-08-18 08:00:06] home media server correct
status: 0Five design decisions that deserve justification:
-n standbyinsmartctl. Without that option, the daily check wakes the disks every morning, cancelling out the energy saving from section 13 and adding a daily spin-up cycle to each disk. A monitoring script that degrades what it monitors is a bad script.- Checking the number of devices, not just the mount. With
nofail, a disconnected disk does not stop the machine booting and the system works normally, degraded and without redundancy. It is a silent failure: without this check, you find out on the day the second one fails. - Not using
dfwith Btrfs in RAID. It gives misleading figures because it does not understand that every block is written twice.btrfs filesystem usage -bis used instead. - The SMART attributes, not the verdict.
smartctl -HsaysPASSEDon disks that are going to fail next week. Attributes 5, 197 and 198 are the ones that predict. - A marker file for the backups, written by
home_backup.shonly after verifying. Checking that the timer ran is not enough: it could have run and failed. What matters is that there was a successful backup.
# /etc/systemd/system/home-check.timer
[Unit]
Description=Daily check of the home media server
[Timer]
OnCalendar=*-*-* 08:00:00
Persistent=true
[Install]
WantedBy=timers.target# /etc/systemd/system/home-check.service
[Unit]
Description=Health check of the home media server
# Notify ONLY on failure: silence when all is well
OnFailure=notify-home@%n.service
[Service]
Type=oneshot
User=operator
ExecStart=/home/operator/scripts/home_check.shWith Type=oneshot and no redirections, the output goes to the journal and only a non-zero code triggers OnFailure. It is the course's "silence when all is well" convention, and at home it matters even more: a daily email saying "everything fine" stops being read within two weeks, and with it the ones that do matter stop being read too. It is the alert fatigue of 08-06, in a domestic version.
Conclusion
You have built a complete server from scratch, and along the way you have found the answer to the question this lesson opened with: there was nothing specific to "enterprise servers". You mounted by UUID with nofail because in 05-04 you learned that an absent disk can leave you without remote access. You put the repository key in keyrings because in 05-03 you learned that apt-key is obsolete. You hardened the systemd unit until the exposure came down to 2.4, with a ReadOnlyPaths over the library that stops a bug in the application from deleting twenty years of photos. You validated the Samba configuration with testparm -s before applying it, which is the same nginx -t from 08-01 under another name. And you left it all in Ansible, rebuildable like srv-tramontana.
You have taken justified decisions instead of copying recipes. Btrfs in RAID 1 instead of mdadm, because checksumming detects and repairs the silent corruption that a classic RAID propagates into the backups — and you saw it working in a scrub that corrected two blocks. Jellyfin instead of Plex, because it does not depend on anybody's cloud and its hardware acceleration is not a paid feature. The VPN instead of exposing the service to the internet, because a Jellyfin vulnerability must not be a vulnerability of your house. And a tiered backup strategy that protects what is irreplaceable for €24 a year instead of trying to protect everything for €360 and ending up protecting nothing.
You have also learned two things that work does not teach you. That electricity consumption is an architectural decision: €110 a year of difference between a mini PC and a repurposed PC, which over three years pays for the whole machine. And that RAID covers exactly one of the seven threats in the table, while Btrfs snapshots, verified backups and the original discs on the shelf cover the other six. With the sentence to take away: RAID protects against the failure of a disk and against nothing else.
In 08-04 you build the WireGuard VPN server that this lesson has recommended twice. You are going to understand why WireGuard is a network interface and not a daemon, why AllowedIPs is routing and access control at the same time — the most misunderstood concept in the whole tool — and how to decide between split tunnelling and full tunnelling with judgement instead of out of habit. By the end you will have access to Tramontana's internal network from anywhere without exposing a single TCP port, you will be able to close PostgreSQL and the statistics panel to the outside because they will no longer need to be open, and along the way your library at home will be reachable from a hotel with a single UDP port open on the router. And you will see why a VPN, for all its virtues, does not turn an internal network into a secure network.
Linux Course: From Beginner to System Administrator
Module 1: Introduction to Linux
- What Is Linux?
- History of Linux
- Linux Distributions
- Installing Linux
- First Contact with the System
- The Linux File System Structure
Module 2: Basic Linux Commands
- Introduction to the Command Line
- Getting Help and System Documentation
- Navigating the File System
- File and Directory Operations
- Viewing and Editing Files
- Hard and Symbolic Links
- File Permissions and Ownership
Module 3: Advanced Command-Line Skills
- The Shell Environment: Variables, Aliases and History
- Using Wildcards and Regular Expressions
- Searching Files and Content: find, locate and grep
- Pipes and Redirection
- Text Processing: cut, sort, uniq, sed and awk
- Process Management
- Scheduling Tasks with Cron
- Networking Commands
Module 4: Shell Scripting
- Introduction to Shell Scripting
- Variables and Data Types
- Script Input, Output and Arguments
- Control Structures
- Functions and Libraries
- Debugging and Error Handling
- Production Scripts: Best Practices
Module 5: System Administration
- User and Group Management
- sudo and Special Permissions
- Package Management
- Disk Management
- systemd and Service Management
- System Logs: journald and syslog
- System Monitoring and Performance Tuning
- Backup and Restore
Module 6: Networking and Security
- Network Configuration
- SSH and Remote Access
- Firewalls and Perimeter Security
- Intrusion Detection Systems
- Secrets Management and TLS Certificates
- Securing Linux Systems
Module 7: Advanced Topics
- The Boot Process and System Recovery
- Advanced Diagnostics: strace, perf and eBPF
- Linux Kernel Tuning
- Virtualization with Linux
- Linux Containers and Docker
- Automation with Ansible
- High Availability and Load Balancing
