Homelab : mon cloud personnel, sécurisé, sur un Raspberry Pi 5

Homelab : mon cloud personnel, sécurisé, sur un Raspberry Pi 5


DevSecOps
Docker DevSecOps Raspberry Pi Typescript

Brief

Un serveur personnel sur un Raspberry Pi 5 qui remplace cinq abonnements : le stockage de mes photos, celui de mes fichiers, le streaming musical, le streaming vidéo et un VPN. Il fait tourner 18 conteneurs Docker. Rien n’est exposé directement sur Internet, une double authentification protège l’entrée, les sauvegardes sont vérifiées, et la restauration a été éprouvée en conditions réelles. La partie musique repose sur aubi, une application que j’ai conçue et codée.

Sommaire

Le point de départ

Au départ, ça m’agaçait de payer tous les mois pour :

  • iCloud+ pour stocker mes photos ;
  • Google One pour stocker mes fichiers ;
  • Spotify pour écouter… des albums que j’ai déjà en CD et en vinyle ;
  • Netflix pour les films et les séries ;
  • un VPN commercial pour me protéger sur les Wi-Fi publics.

Mis bout à bout, cela représente environ 610 € par an, chaque année, pour louer l’accès à des choses que je possède. Mes données vivent chez cinq entreprises différentes, selon leurs conditions, et ma musique comme mes films disparaissent le jour où j’arrête de payer.

En face, le matériel m’a coûté 50 €, une seule fois : le Raspberry Pi 5 trouvé sur Leboncoin pour 45 €, et le disque de 500 Go chiné en brocante pour 5 €. Il faut ajouter à peu près 1 € d’électricité par mois. Le tout est rentabilisé en un seul mois d’abonnements.

Comparaison : cinq abonnements (cloud, streaming musical et vidéo, VPN), environ 610 € par an, contre un Raspberry Pi 5 d'occasion et un disque de brocante, 50 € une fois

Montants indicatifs : ils varient selon les offres choisies.

Au-delà de l’argent, je voulais que ce soit au moins aussi sûr que les services que je remplaçais. C’est d’ailleurs ce qui m’a demandé le plus de travail.

Objectifs

  1. Remplacer les cinq services sans perdre en confort : sauvegarde automatique des photos depuis le téléphone, accès aux fichiers partout, musique sur mobile avec l’écran verrouillé, films sur la télé comme sur le téléphone, et un VPN pour les Wi-Fi publics.
  2. Ne rien exposer : aucun port ouvert sur la box, et une authentification forte avant d’atteindre le moindre service.
  3. Tout décrire dans du code : la configuration complète tient dans un dépôt git, ce qui rend le serveur reconstructible.
  4. Pouvoir tout perdre sauf les données : le matériel doit être jetable, les données ne doivent pas l’être.

Architecture

Schéma d'architecture : Internet, tunnel Cloudflare, Traefik, Authelia, puis le Raspberry Pi 5 et ses services Docker ; accès VPN WireGuard en parallèle

Il n’existe que deux chemins pour entrer :

  • Le chemin web. Le Pi ouvre lui-même une connexion sortante vers Cloudflare (Cloudflare Tunnel). Une requête venue d’Internet emprunte ce tunnel jusqu’à Traefik, le reverse proxy, qui termine le HTTPS puis fait vérifier l’identité par Authelia avant de router vers le bon service. La box n’a aucun port ouvert. Ce choix tombe bien, car ma ligne est derrière un CGNAT qui m’empêcherait de toute façon d’en ouvrir.
  • Le chemin VPN. WireGuard donne un accès direct au réseau de la maison depuis mon téléphone ou mon ordinateur, en IPv6. Il fonctionne en tunnel complet : tout le trafic de l’appareil passe par la maison, et ses requêtes DNS par Pi-hole. Un petit script met à jour l’enregistrement DNS quand le préfixe IPv6 attribué par l’opérateur change.

Le stockage est séparé en deux. La carte SD porte le système et le moteur Docker. Le disque de 500 Go porte tout ce qui compte : les photos, les médias, les bases de données, les configurations et les sauvegardes.

Les services

BesoinServiceRemplace
Photos et vidéosImmich (avec PostgreSQL et Valkey)iCloud Photos
FichiersNextcloud, FilebrowserGoogle Drive
Musiqueaubi (fait maison)Spotify
Films et sériesJellyfinNetflix
VPNwg-easy (WireGuard)VPN commercial
Blocage des pubs et traqueursPi-hole, DNS du VPN—
PortailHomepage—
SupervisionUptime Kuma, Watchtower—
AdministrationPortainer—
EntréeTraefik, Authelia, Cloudflare Tunnel—

Au total, 18 conteneurs tournent sur un Raspberry Pi 5 de 8 Go. Les tâches lourdes ont été réglées pour cette machine. Par exemple, la reconnaissance d’images d’Immich est désactivée et sa génération de miniatures est plafonnée à deux cœurs.

Mon VPN, sans abonnement

Un VPN commercial sert surtout à deux choses : protéger sa connexion sur un Wi-Fi public, et bloquer une partie des publicités et des traqueurs. Le homelab fait les deux.

  • WireGuard (via wg-easy) en tunnel complet : depuis le téléphone ou l’ordinateur, tout le trafic part chiffré jusqu’à la maison, puis sort par ma connexion. Sur le Wi-Fi d’une gare ou d’un hôtel, personne sur le réseau ne voit ce que je consulte.
  • Pi-hole comme DNS des appareils connectés au VPN : les publicités et les traqueurs sont bloqués avant même d’être chargés, dans toutes les applications, y compris en 4G.
  • Chaque appareil a son propre profil, créé depuis une interface web et importé en scannant un QR code. Un appareil perdu se révoque en un clic.

Par contre, un VPN maison ne permet pas de « changer de pays », puisque je sors toujours par ma propre connexion. Ça ne me gêne pas : ce qui m’intéressait, c’était la sécurité sur les réseaux publics et le blocage des pubs.

Films : Jellyfin plutôt que Netflix

Jellyfin est un serveur multimédia libre. Il indexe mes films, séries, documentaires et vlogs, récupère les affiches et les résumés, retient où je me suis arrêté et propose des applications pour le téléphone, l’ordinateur et la télévision. Les fichiers sont lus tels quels autant que possible, car le Pi 5 n’a pas d’encodeur vidéo matériel. Comme le reste, Jellyfin est derrière Authelia.

Le catalogue ne change pas d’un mois à l’autre, puisque ce sont mes propres fichiers.

Sécurité : défense en profondeur

Les cinq couches de sécurité : réseau, transport, identité, conteneurs, données

L’idée est de ne jamais compter sur une seule protection : si l’une cède, la suivante prend le relais.

1. Réseau

  • Aucun port ouvert sur la box : le seul chemin web passe par le tunnel sortant.
  • Le VPN WireGuard sert pour l’accès direct et l’administration.
  • Sur le Pi, seuls HTTPS et le VPN écoutent, et Pi-hole uniquement sur l’adresse du réseau local.

2. Transport

  • HTTPS partout, avec des certificats Let’s Encrypt obtenus et renouvelés automatiquement par challenge DNS.
  • Des en-têtes de sécurité appliqués à tous les services : HSTS de deux ans, nosniff, interdiction des iframes, Referrer-Policy stricte.

3. Identité

  • Authelia est placé devant les services avec une politique de refus par défaut.
  • On accède aux services avec un SSO et une double authentification (TOTP).
  • Pourquoi la double authentification compte : un mot de passe peut fuiter, par réutilisation, par hameçonnage ou lors de la fuite d’un autre site. Avec la 2FA, il ne suffit plus : il faut aussi le code à six chiffres généré par une application sur mon téléphone, qui change toutes les 30 secondes. Et comme une seule connexion ouvre ensuite tout le homelab (SSO), la sécurité ne coûte presque rien en confort.
  • Les quelques services qui ont leur propre authentification (Immich, le VPN) sont traités explicitement, pas par oubli.

4. Conteneurs

  • Les bases de données vivent sur des réseaux Docker internes, sans route vers Internet ni vers le proxy.
  • Les conteneurs sensibles tournent en no-new-privileges, et le socket Docker n’est monté qu’en lecture seule pour ceux qui ont seulement besoin de le lire.
  • Des plafonds CPU et RAM par service empêchent un service qui s’emballe de faire tomber les autres.

5. Données

  • Les secrets (mots de passe, jetons d’API) sont dans un .env en permissions 600, exclu du dépôt git. Un .env.example documente les variables sans les valeurs.
  • Chaque nuit, une sauvegarde dumpe les bases et archive les configurations. Chaque archive est vérifiée, et une rotation garde 14 jours d’historique.

Une démarche DevSecOps

La boucle DevSecOps : versionner, construire, déployer, surveiller, sauvegarder, restaurer

J’ai repris les mêmes réflexes qu’en entreprise, à plus petite échelle :

  • Infrastructure as Code. Tout le serveur est décrit dans un docker-compose.yml versionné, et chaque choix non évident y est expliqué en commentaire, pour mon « moi » de dans six mois.
  • Versions maîtrisées. Les briques critiques sont épinglées à une version précise. Watchtower signale les mises à jour disponibles mais ne les applique jamais à l’aveugle.
  • Déploiement ordonné. Chaque service a un healthcheck, et les dépendances attendent que leur base soit réellement prête avant de démarrer.
  • Observabilité. Uptime Kuma surveille la disponibilité de chaque service, et Homepage donne une vue d’ensemble.
  • Sécurité dès la conception. Refus par défaut, moindre privilège, secrets hors du code, surface d’exposition minimale.
  • Restauration testée. Une sauvegarde ne sert à rien si on ne sait pas la restaurer. J’ai eu l’occasion de tester la mienne plus tôt que prévu.

Cas réel : la panne de carte SD

Chronologie de la reprise après la panne de la carte SD, jusqu'au retour des 18 services

La carte SD de l’ancien Raspberry Pi 4 a fini par lâcher : la machine ne démarrait plus, et la carte était illisible. Heureusement, j’avais prévu ce cas dès le départ.

  1. Nouveau matériel : un Raspberry Pi 5, une alimentation officielle de 27 W et une carte neuve, flashée avec un accès SSH par clé uniquement.
  2. Disque remonté au même chemin : les photos, les médias, les configurations et le dépôt git étaient intacts.
  3. docker compose up : la stack est repartie à partir de sa description.
  4. Trois correctifs liés au nouveau système, plus récent :
    • Docker 29 refuse les anciennes versions de son API. Traefik ne voyait plus les conteneurs, et je l’ai corrigé côté démon Docker, sans toucher à la stack.
    • Le nouveau noyau n’a plus l’ancien iptables. J’ai basculé les règles du VPN sur nftables.
    • Raspberry Pi OS désactive par défaut le contrôle mémoire des conteneurs. Les plafonds de RAM étaient silencieusement ignorés : je les ai réactivés au démarrage.
  5. Vérification complète après un redémarrage : disque, réseau, VPN, 18 services sains.

Au final, ça m’a pris une après-midi, sans perdre une seule photo. La seule perte, c’est la base de Nextcloud, presque vide, qui était restée dans un volume Docker sur la carte SD. J’en retiens que tout ce qui compte doit être sur le disque, pas sur la carte SD.

aubi, mon lecteur de musique

Fonctionnement d'aubi : fichiers audio, scanner, API NestJS et PostgreSQL, streaming, application mobile

Pour remplacer Spotify, il me fallait un lecteur qui joue ma musique avec le confort d’une application de streaming. Je n’en ai pas trouvé qui me plaisait, donc j’ai codé le mien : aubi. Il est open source et disponible sur mon GitHub : github.com/Claquettes/aubi.

aubi lit les fichiers audio d’un dossier (MP3, FLAC, OGG, Opus, M4A, WAV) et les sert dans une application web qui fonctionne sur iOS et Android, directement dans le navigateur :

  • pochettes et couleurs dynamiques : chaque page prend les couleurs de sa pochette ;
  • playlists, favoris, reprise de lecture, avec des rubriques Musique, Concerts et Livres audio ;
  • statistiques d’écoute et un graphe des artistes reliés par leurs featurings ;
  • correction des tags depuis l’interface, écrite dans les fichiers eux-mêmes, avec une copie de sauvegarde du fichier d’origine ;
  • commandes sur l’écran verrouillé et dans le centre de notifications (Media Session).

Côté technique : React + TypeScript pour l’interface, NestJS + PostgreSQL pour le serveur, le streaming audio sert les requêtes Range au fil de l’eau, et le tout tourne dans Docker derrière Traefik et Authelia, comme le reste du homelab.

Une anecdote de développement : sur iPhone, la musique se coupait quand l’écran se verrouillait. La cause : pour animer le lecteur au rythme du son, l’audio passait par un analyseur Web Audio, et iOS met ce contexte en pause en arrière-plan sans jamais le relancer. Le correctif désactive l’analyse sur iOS, et l’animation y repasse en CSS seul. Il a suivi le même chemin que le reste : branche, pull request, merge, redéploiement.

Bilan

  • ≈ 28 000 photos et vidéos dans Immich ;
  • ≈ 2 600 titres dans aubi ;
  • 18 services, 0 port ouvert sur la box ;
  • 50 € de matériel d’occasion, contre ≈ 610 € d’abonnements par an ;
  • 5 abonnements en moins ;
  • 1 vraie panne, encaissée en une après-midi.

Limites et prochaines étapes

Il reste encore des choses à améliorer :

  • Les sauvegardes sont sur le même disque que les données. Elles protègent contre une erreur ou une corruption, pas contre la mort du disque. La prochaine étape est une copie hors site, dans l’esprit de la règle 3-2-1.
  • La carte SD reste un point faible. Je prévois de déplacer les données Docker sur le disque, puis de démarrer le Pi sur un SSD NVMe.
  • Pas de CI pour l’infrastructure. Une validation automatique de la configuration (lint du compose, scan des images) aurait sa place.
  • Les limites du matériel. La reconnaissance d’images d’Immich reste désactivée, car trop lourde pour le Pi.

Stack technique

  • Matériel : Raspberry Pi 5 (8 Go) d’occasion, disque de 500 Go en USB 3 chiné en brocante, Raspberry Pi OS 64 bits.
  • Conteneurs : Docker, Docker Compose.
  • Entrée et sécurité : Cloudflare Tunnel, Traefik, Let’s Encrypt, Authelia (SSO, TOTP), WireGuard (wg-easy).
  • Services : Immich, Nextcloud, Filebrowser, Jellyfin, Pi-hole, Homepage, Uptime Kuma, Portainer, Watchtower.
  • Données : PostgreSQL, Valkey et Redis, sauvegardes pg_dumpall et tar vérifiées.
  • aubi : React, TypeScript, Vite, NestJS, PostgreSQL, Howler.js, PWA.

English version

Brief

A personal server on a Raspberry Pi 5 that replaces five subscriptions: photo storage, file storage, music streaming, video streaming and a VPN. It runs 18 Docker containers. Nothing is directly exposed to the Internet, two-factor authentication guards the entrance, backups are verified, and recovery has been proven in real conditions. The music part relies on aubi, an app I designed and built.

Table of contents

Where it started

It started because I was tired of paying every month for:

  • iCloud+ to store my photos;
  • Google One to store my files;
  • Spotify to listen to… albums I already own on CD and vinyl;
  • Netflix for movies and TV shows;
  • a commercial VPN to stay safe on public Wi-Fi.

That adds up to roughly €610 a year, every year, to rent access to things I own. My data lived with five different companies under their terms, and my music and movies would vanish the day I stopped paying.

On the other side, the hardware cost me €50, once: the Raspberry Pi 5 came from a second-hand listing for €45, and the 500 GB drive from a flea market for €5. Add roughly €1 of electricity per month. It pays for itself in a single month of subscriptions.

Comparison: five subscriptions (cloud, music and video streaming, VPN), about €610 a year, versus a second-hand Raspberry Pi 5 and a flea-market drive, €50 once

Indicative amounts: they depend on the chosen plans.

Beyond the money, I wanted it to be at least as safe as the services I was replacing. That is actually what took the most work.

Goals

  1. Replace all five services without losing comfort: automatic photo backup from the phone, files available anywhere, music on mobile with the screen locked, movies on the TV and the phone, and a VPN for public Wi-Fi.
  2. Expose nothing: no open port on the router, and strong authentication before reaching any service.
  3. Describe everything as code: the whole configuration lives in a git repository, which makes the server rebuildable.
  4. Be able to lose everything but the data: the hardware must be disposable, the data must not be.

Architecture

Architecture diagram: Internet, Cloudflare Tunnel, Traefik, Authelia, then the Raspberry Pi 5 and its Docker services; WireGuard VPN access alongside

There are only two ways in:

  • The web path. The Pi itself opens an outbound connection to Cloudflare (Cloudflare Tunnel). An Internet request rides that tunnel to Traefik, the reverse proxy, which terminates HTTPS and has Authelia check the user’s identity before routing to the right service. The router has no open port at all, which suits me: my line sits behind a CGNAT that would not let me open one anyway.
  • The VPN path. WireGuard gives direct access to the home network from my phone or laptop, over IPv6. It runs as a full tunnel: all of the device’s traffic goes through home, and its DNS queries through Pi-hole. A small script updates the DNS record whenever the ISP changes the IPv6 prefix.

Storage is split in two. The SD card holds the system and the Docker engine. The 500 GB drive holds everything that matters: photos, media, databases, configuration and backups.

18 containers run on an 8 GB Raspberry Pi 5. Heavy jobs are tuned for this machine: for example, Immich’s image recognition is disabled and its thumbnail generation is capped at two cores.

My own VPN, no subscription

A commercial VPN mostly does two things: protect your connection on public Wi-Fi, and block some ads and trackers. The homelab does both.

  • WireGuard (through wg-easy) as a full tunnel: from the phone or laptop, all traffic travels encrypted to my home, then goes out through my own connection. On station or hotel Wi-Fi, nobody on the network can see what I browse.
  • Pi-hole as the DNS server for VPN clients: ads and trackers are blocked before they even load, in every app, including on 4G.
  • Each device has its own profile, created from a web UI and imported by scanning a QR code. A lost device is revoked in one click.

That said, a home VPN will not let you “change country”, since traffic always leaves through my own connection. That is fine by me: what I wanted was security on public networks and ad blocking.

Movies: Jellyfin instead of Netflix

Jellyfin is a free media server. It indexes my movies, shows, documentaries and vlogs, fetches posters and synopses, remembers where I stopped, and has apps for phone, computer and TV. Files are played as-is whenever possible, since the Pi 5 has no hardware video encoder. Like everything else, Jellyfin sits behind Authelia.

The catalog does not change from one month to the next, since these are my own files.

Security: defense in depth

The five security layers: network, transport, identity, containers, data

The idea is to never rely on a single protection: if one fails, the next one takes over.

  1. Network: no open port on the router, an outbound tunnel for the web, and a WireGuard VPN for direct access.
  2. Transport: HTTPS everywhere with Let’s Encrypt certificates issued and renewed automatically through a DNS challenge. Security headers apply to every service: two-year HSTS, nosniff, no framing, strict Referrer-Policy.
  3. Identity: Authelia sits in front of the services with a default-deny policy, SSO and TOTP two-factor authentication. A password can leak through reuse, phishing or another site’s breach. With 2FA it is no longer enough: you also need the six-digit code from an app on my phone, which changes every 30 seconds. Since one login then opens the whole homelab (SSO), the extra security costs almost nothing in comfort.
  4. Containers: databases live on internal Docker networks with no route to the Internet. Sensitive containers run with no-new-privileges, the Docker socket is mounted read-only where it only needs to be read, and CPU and RAM caps keep a runaway service from taking the others down.
  5. Data: secrets live in a .env file with 600 permissions, kept out of git. A nightly backup dumps the databases and archives the configuration, and every archive is verified, with 14 days of rotation.

A DevSecOps approach

The DevSecOps loop: version, build, deploy, monitor, back up, restore

I used the same habits as at work, on a smaller scale:

  • Infrastructure as Code: the whole server is described in a versioned docker-compose.yml, with every non-obvious choice explained in a comment.
  • Controlled versions: critical components are pinned. Watchtower reports available updates but never applies them blindly.
  • Ordered deployment: every service has a healthcheck, and services wait until their database is actually ready.
  • Observability: Uptime Kuma watches every service, and Homepage gives an overview.
  • Security by design: default deny, least privilege, secrets out of the code, minimal attack surface.
  • Tested recovery: a backup is useless if you cannot restore it. I got to test mine sooner than planned.

Real incident: the SD card failure

Recovery timeline after the SD card failure, until all 18 services were back

The old Raspberry Pi 4’s SD card eventually died: the machine would not boot, and the card was unreadable. Luckily, I had planned for this from the start.

  1. New hardware: a Raspberry Pi 5, an official 27 W power supply and a fresh card, flashed with key-only SSH access.
  2. The drive was remounted at the same path: photos, media, configuration and the git repository were all intact.
  3. docker compose up: the stack came back from its own description.
  4. Three fixes caused by the newer system:
    • Docker 29 rejects old API versions, so Traefik could no longer see the containers. I fixed it on the Docker daemon side, without touching the stack.
    • The new kernel no longer ships legacy iptables, so I switched the VPN rules to nftables.
    • Raspberry Pi OS disables container memory control by default, so the RAM caps were being silently ignored. I re-enabled it at boot.
  5. Full check after a reboot: drive, network, VPN, all 18 services healthy.

In the end, it took me one afternoon, without losing a single photo. The only loss was Nextcloud’s almost empty database, which had stayed in a Docker volume on the SD card. My takeaway: everything that matters belongs on the drive, not on the SD card.

aubi, my music player

How aubi works: audio files, scanner, NestJS API and PostgreSQL, streaming, mobile app

To replace Spotify, I needed a player for my music with the comfort of a streaming app. I could not find one I liked, so I built my own: aubi. It is open source and available on my GitHub: github.com/Claquettes/aubi.

aubi reads audio files from a folder (MP3, FLAC, OGG, Opus, M4A, WAV) and serves them through a web app that runs on iOS and Android, right in the browser:

  • artwork-driven colors: every page takes on the colors of its album cover;
  • playlists, favorites, resume playback, with Music, Concerts and Audiobooks sections;
  • listening statistics and a graph of artists linked by their collaborations;
  • tag editing from the UI, written into the files themselves, with a backup copy of the original file;
  • lock-screen and notification-center controls (Media Session).

Under the hood: React + TypeScript on the front end, NestJS + PostgreSQL on the back end, audio streamed through Range requests, all running in Docker behind Traefik and Authelia like the rest of the homelab.

A development story: on iPhone, music stopped when the screen locked. To animate the player to the beat, audio was routed through a Web Audio analyser, and iOS suspends that context in the background and never resumes it. The fix disables the analysis on iOS, where the animation falls back to pure CSS. It shipped the same way as everything else: branch, pull request, merge, redeploy.

Results

  • ≈ 28,000 photos and videos in Immich;
  • ≈ 2,600 tracks in aubi;
  • 18 services, 0 open ports on the router;
  • €50 of second-hand hardware, versus ≈ €610 of subscriptions per year;
  • 5 fewer subscriptions;
  • 1 real outage, absorbed in an afternoon.

Limitations and next steps

  • Backups sit on the same drive as the data. They protect against mistakes and corruption, not against the drive dying. Next step: an off-site copy, in the spirit of the 3-2-1 rule.
  • The SD card is still a weak spot. I plan to move Docker data onto the drive, then boot the Pi from an NVMe SSD.
  • No CI for the infrastructure yet. Automatic configuration checks (compose linting, image scanning) would belong here.
  • Hardware limits. Immich’s image recognition stays disabled because it is too heavy for the Pi.
© 2026 Mathieu Ponton | Co-Founder & ingénieur logiciel @ Apogée Consult | Lyon, France