O tym jak postawiłem hosting na RPI5 w domu.
Jak zamieniłem leżące w szufladzie Raspberry Pi 5 w domowy serwer pod portfolio i inne.
Szybka historia, jak to było z tym RPi?
Po przeprowadzce do nowego mieszkania skusiłem się na internet symetryczny 1/1 Gbit. Uznałem, że to świetna okazja, żeby wykorzystać kurzącego się Raspberry Pi 5, którego dostałem już jakiś czas temu na 21 urodziny. Chciałem postawić sobie proste portfolio + jakiś formularz kontaktowy (czyli de facto stronę, na której obecnie się znajdujesz). Z początku myślałem oczywiście nad kupnem VPS-a, ale uznałem - po co? Mogę przecież wykorzystać to, co mam już pod ręką. Tak narodził się pomysł użycia malinki do hostowania mało zasobożernych aplikacji. :)
Sprzęt
Pi 5, oficjalna czarna obudowa z wiatrakiem, zasilacz 27 W. Tyle wystarcza. Wziąłem aktywne chłodzenie, bo box ma chodzić 24/7, a cichy throttling pod obciążeniem to ostatnie, czego chcesz.
Na wstępie planowałem dorzucić dysk NVMe przez M.2 HAT - ale cóż... HAT spowodowałby, że muszę pożegnać się z oficjalną obudową i sprezentować sobie inną, a to już są koszty, których na ten moment ponosić nie chcę.
Na ten moment jadę więc na karcie microSD wsadzonej w malinkę - z jednym ważnym założeniem w głowie: karta to materiał eksploatacyjny, nie docelowy nośnik. Karty padają nie od czytania, tylko od ciągłych drobnych zapisów (logi, baza), a do tego mają ograniczoną liczbę cykli. Jeśli masz taką możliwość, to nawet jeden z tańszych SSD będzie do tego rozwiązania pasować lepiej. Ja docelowo prawdopodobnie będę się jednak starał umieścić tutaj dysk M.2. Na razie traktuję kartę jako tymczasowy dysk na dane.
System: Raspberry Pi OS Lite
Wybierając system, zależało mi na sporej wydajności. Normalnie wybrałbym zapewne dystrybucję Rocky, jednakże - jak widziałem po komentarzach - na dzień dzisiejszy średnio współgra z malinką. Padło więc na Raspberry Pi OS Lite (64-bit) - wersję bez pulpitu, bo to serwer, a GUI tylko zżerałoby zasoby i powiększało powierzchnię ataku.
Flashowanie zrobiłem za pomocą Raspberry Pi Imager, ustawiłem wszystkie potrzebne rzeczy, takie jak: hostname, użytkownika, locale, a w usługach włączyłem SSH z logowaniem tylko po kluczu i wkleiłem tam mój pubkey. W routerze ustawiłem, aby DHCP przypisywało stały, niezmienny adres IP. Dzięki temu mogę bez problemu logować się po SSH.
Sieć
Tutaj miałem najwięcej przemyśleń, jak to ogarnąć. Nie mam stałego publicznego adresu IPv4 (mam zmienny), a taka usługa u mojego dostawcy jest droga. Po większym researchu i burzy mózgów z moim przyjacielem Klaudiuszem (AI) zdecydowałem się na Cloudflare Tunnel. Przede wszystkim moja adresacja jest ukryta i nie musiałem otwierać żadnego portu na świat.
Domenę miałem już na Cloudflare, więc utworzenie tunelu to kilka kliknięć w panelu (Zero Trust → Networks → Connectors → Cloudflare Tunnels). Dostajesz token, którym uruchamiasz cloudflared - u mnie jako kontener, obok reszty.
Zarządzanie
Mogłem trzymać wszystko w gołym docker compose przez SSH i to byłaby kompletnie sensowna droga. Ale chciałem coś więcej... Panel z UI - przegląd kontenerów, logi, deploy z gita, jedno miejsce.
Tutaj wybrałem Komodo. Napisany w Ruście, lekki, deployuje compose z repo gita. Jako bazę użyłem FerretDB zamiast MongoDB. Alternatywą może być np. Dokploy, który znam - ale chciałem jednak wypróbować coś innego.
Reverse proxy
Mając tunel i panel, potrzebowałem jeszcze czegoś, co rozdzieli ruch między apki - czyli reverse proxy. Zamiast pisać i nadpisywać jeden centralny config przy każdej nowej stronie, poszedłem w caddy-docker-proxy. To Caddy, który podsłuchuje Dockera i sam buduje sobie konfigurację na podstawie etykiet doklejonych do kontenerów. Czyli każda apka sama deklaruje, na jakiej domenie ma siedzieć:
caddy: http://app.mojadomena.dev
caddy.reverse_proxy: "{{upstreams 8080}}"
Te dwie linijki to całe ustawienie - reszta dzieje się sama. http:// na początku jest celowe, bo TLS terminuje Cloudflare na brzegu, więc wewnątrz lata zwykły HTTP i Caddy nie próbuje ciągnąć własnych certów.
W Cloudflare ustawiłem wildcard - jeden wpis *.mojadomena i tyle, całe rozdzielanie po subdomenach robi już Caddy. Dwie rzeczy mnie tu ugryzły. Po pierwsze, wildcardowy rekord DNS nie tworzy się sam - dla zwykłego hosta Cloudflare dorabia go automatycznie, ale * musiałem dodać ręcznie. Po drugie, Caddy na nieznaną subdomenę domyślnie zwraca pustą stronę (200), a nie 404 - przez chwilę szukałem błędu, którego nie było, a wystarczyło dodać regułę catch-all zwracającą 404.
Efekt jest taki, że dodanie nowej apki to teraz nowy stack + dwie etykiety. Tunel, Cloudflare, Caddy - nie dotykam niczego.
Deploy
Skoro karta jest tymczasowa, to kod nie może na niej mieszkać na stałe. Trzymam go więc w prywatnym repo na GitHubie, a Komodo ściąga go właśnie stamtąd. Karta padnie - no to teoretycznie wypalam nową, podpinam repo i jestem z powrotem w kilka minut.
Kusiło mnie, żeby postawić własną Giteę na malince, ale szybko sobie odpuściłem - gdyby git siedział na tej samej karcie, jej śmierć zabrałaby mi i panel, i kod naraz. Trochę bez sensu jak na backup. GitHub jest poza moim sprzętem, a prywatne repo nic nie kosztuje.
Przy pierwszym deployu nadziałem się jeszcze na jedno: Komodo domyślnie próbuje ściągnąć obrazy (pull), a moje mają być budowane ze źródła. Stąd pull access denied - bo tych obrazów nie ma w żadnym rejestrze, dopóki ich tam sam nie wypchnę. Wystarczyło w ustawieniach stacka wyłączyć pre-pull i włączyć pre-build. Pierwszy build na arm64 na karcie jest woooolny, ale kolejne idą już z cache i trwają nie więcej niż kilka minut, więc da się żyć.
Na koniec chciałem firmowy adres - office@mojadomena. I tu jedna rada na zapas: nie stawiaj serwera pocztowego na domowym łączu. Port 25 wychodzący zwykle zablokowany, zmienne IP, zero reputacji - twoje maile i tak wylądują w spamie albo się odbiją. Nie warto.
Zrobiłem to inaczej i całkowicie za darmo: Cloudflare Email Routing odbiera pocztę i przekierowuje ją na mojego Gmaila, a do wysyłki używam opcji "Wyślij jako" w Gmailu.
Co z tego wyszło?
W zasadzie tyle. Za cenę prądu mam własny mały serwer z gigabitem, pełną kontrolą i bez ani jednego portu otwartego na świat. Strona, którą teraz czytasz, leci dokładnie z tej malinki stojącej u mnie na komodzie obok routera.
Jedyne, co bym zmienił, to nośnik - prędzej czy później wsadzę tu jednak dysk M.2 zamiast karty. Ale to akurat będzie godzina roboty, bo cały setup jest na tyle odtwarzalny, że wymiana karty niczego nie burzy. I to jest chyba najlepsze, co z tego całego dłubania wyniosłem. :)