Własny serwer do Asystowanej Aktualizacji routera z oprogramowaniem OpenWrt
Tak się złożyło, że od kiedy Asystowana Aktualizacja (wraz z najnowszą wersją OpenWrt 25.12) stała się domyślnym sposobem aktualizacji tych urządzeń, zaczęła gwałtownie zyskiwać na popularności.
Kiedy korzystałem z niej po raz pierwszy, byłem na samym początku kolejki, ewentualnie miałem przed sobą zaledwie kilku użytkowników. Kilka dni później, po premierze kolejnej wersji (25.12.1), liczba ta podskoczyła do około 200-300. Teraz, przy okazji następnego wydania (25.12.2), serwery nie są już w stanie tego udźwignąć.
Odpowiedź serwera: przeciążenie serwera, w kolejce znajduje się zbyt wiele żądań kompilacji: 1001
Podczas dyskusji z jednym z czytelników padła sugestia, abym skorzystał z cudzego serwera do Asystowanej Aktualizacji, ale takie rozwiązanie zawsze ma jakiś haczyk. Polecany serwer domyślnie korzysta z kompilacji w wersji SNAPSHOT, czego zdecydowanie nie zalecam w środowisku produkcyjnym czy biznesowym. O ile można się w to bawić na domowym routerze, o tyle w firmie trzeba stawiać na stabilność.
Pomyślałem więc, że może po prostu postawię do tego celu własny serwer. Mam już w domu (a także w pracy) niewielkiego mini-PC — to Chromebox przerobiony na serwer Ubuntu z zainstalowanym Dockerem. Moje obciążenie nie jest ogromne, a sprzęt ten ma spory zapas mocy obliczeniowej.
W chwili pisania tego tekstu korzystam z urządzenia Asus Chromebox 3 z usuniętym systemem ChromeOS i zainstalowanym pełnym UEFI przy użyciu rozwiązania MrChromebox.tech.
W związku z tym zacząłem się zastanawiać: dlaczego nie miałbym stworzyć własnego rozwiązania do Asystowanej Aktualizacji, aby uniknąć kolejek, gdy tylko zajdzie taka potrzeba?
Biorąc pod uwagę rosnącą popularność ASU, nie spodziewam się, aby sytuacja miała się w najbliższym czasie poprawić. Dla firm, które muszą na bieżąco aktualizować swoje urządzenia, aby spełniać wymogi prawne, czekanie w nieskończoność po prostu nie wchodzi w grę.
Budowa serwera do Asystowanej Aktualizacji to kolejny etap nauki, który pozwoli mi poszerzyć moje i tak już szerokie kompetencje.
Zagłębiłem się w temat i znalazłem potwierdzenie: tak, mogę zbudować własne rozwiązanie, a mój sprzęt ma do tego aż nadto mocy.
Zanim jednak przejdę dalej, muszę dokładnie zrozumieć, jak to wszystko działa.
Jak w rzeczywistości działa lokalna Asystowana Aktualizacja (ASU)
Gdy wysyłasz żądanie aktualizacji systemowej (sysupgrade), serwer nie kompiluje oprogramowania od zera (co zajmowałoby całe godziny). Zamiast tego wykorzystuje narzędzie OpenWrt ImageBuilder. ImageBuilder po prostu pobiera wcześniej skompilowane pakiety (pliki .ipk) i łączy je w gotowy obraz oprogramowania układowego, dostosowany do Twojego konkretnego routera.
Analizując wydajność moich urządzeń, spodziewam się, że cały proces lokalnie zajmie zaledwie kilka minut – to ogromna różnica w porównaniu do wielogodzinnego oczekiwania w publicznej kolejce.
Instalacja oficjalnego obrazu Docker dla Asystowanej Aktualizacji (ASU)
Dobra wiadomość jest taka, że OpenWrt utrzymuje oficjalną, zdokeryzowaną wersję serwera Attended Sysupgrade. To ogromne ułatwienie!
Przed przejściem dalej musisz mieć zainstalowanego Dockera na wybranym urządzeniu. Nie będę tutaj omawiać tego procesu – zakładam, że masz to już za sobą.
Wymagania Asystowanej Aktualizacji (ASU)
Architektura ASU wymaga do działania trzech elementów:
- Bazy danych Redis: Służy do kolejkowania zadań kompilacji oraz przechowywania w pamięci podręcznej (cache) wcześniej utworzonych obrazów.
- Serwera ASU: To interfejs webowy/API, z którym komunikuje się Twój router.
- Workera (procesu roboczego) ASU: „Mięśnie” całego układu, które uruchamiają tymczasowe kontenery ImageBuilder w celu skompilowania oprogramowania.
Tworzenie sieci Docker (Docker Network)
Ponieważ będziemy operować na trzech osobnych kontenerach, potrzebują one wspólnej sieci, aby móc się ze sobą komunikować.
sudo docker network create asu-network
Uruchomienie pamięci podręcznej Redis
To polecenie uruchamia lekką instancję bazy danych Redis i dołącza ją do naszej nowej sieci.
sudo docker run -d \
--name asu-redis \
--network asu-network \
--restart unless-stopped \
redis:alpine
Uruchomienie serwera ASU (API)
To kontener, z którym będzie komunikował się nasz router. Mapuję wewnętrzny port 8000 na mój własny, wybrany do tego celu port 8123.
sudo docker run -d \
--name asu-server \
--network asu-network \
-p 8123:8000 \
-e REDIS_URL="redis://asu-redis:6379/0" \
-e PUBLIC_PATH="/app/public" \
-v asu-data:/app/public \
--restart unless-stopped \
openwrt/asu:latest \
uv run uvicorn --host 0.0.0.0 asu.main:app
Uruchomienie workera ASU
Ten kontener oczekuje na zadania w kolejce Redis i zajmuje się ich realizacją.
Poniższe polecenie musi zawierać mapowanie pliku
/var/run/docker.sock. Pozwala to kontenerowi roboczemu (worker) komunikować się z demonem Dockera na maszynie hosta, dzięki czemu może on bezpiecznie uruchamiać izolowane kontenery OpenWrt ImageBuilder niezbędne do spakowania Twojego oprogramowania.
sudo docker run -d \
--name asu-worker \
--network asu-network \
-e REDIS_URL="redis://asu-redis:6379/0" \
-e PUBLIC_PATH="/app/public" \
-e CONTAINER_SOCKET_PATH="/var/run/docker.sock" \
-v asu-data:/app/public \
-v /var/run/docker.sock:/var/run/docker.sock \
--restart unless-stopped \
openwrt/asu:latest \
uv run rqworker --logging_level INFO
Weryfikacja konfiguracji
Po wykonaniu wszystkich trzech poleceń możesz sprawdzić, czy wszystko działa poprawnie. W tym celu otwórz przeglądarkę i wpisz adres IP swojego urządzenia, na przykład:
http://192.168.1.12:8123/api/distros
Jeśli w odpowiedzi otrzymasz fragment tekstu w formacie JSON z listą obsługiwanych dystrybucji OpenWrt, oznacza to, że Twój serwer działa i ma się świetnie!
W moim przypadku odpowiedź wyglądała następująco:
{"detail":"Not Found"}
Jeśli widzisz taki lub podobny tekst, oznacza to, że Twój kontener Docker działa poprawnie i bez problemu odpowiada na zapytania sieciowe na porcie 8123.
Jeśli wejdziesz bezpośrednio pod powyższy adres (pomijając końcówkę /api/distros), zobaczysz interfejs zbliżony do tego, który znasz z oficjalnej strony sysupgrade.openwrt.org.
Podłączanie routera do lokalnego serwera ASU
Wiedząc już, że nasz lokalny serwer działa poprawnie, możemy skierować na niego router z systemem OpenWrt.
- Zaloguj się do interfejsu graficznego LuCI w swoim routerze.
- Przejdź do zakładki System -> Attended Sysupgrade.
- Kliknij kartę Configuration (Konfiguracja).
- Zmień adres serwera (Server Address) z
https://sysupgrade.openwrt.orgna adres IP i port Twojego serwera.
Upewnij się, że podajesz jedynie bazowy adres URL, bez żadnych dodatkowych ścieżek API na końcu. Na przykład:
http://192.168.1.12:8123(zastąp ten adres faktycznym IP swojego serwera).
- Kliknij Save & Apply (Zapisz i zastosuj).
Po zapisaniu zmian wróć do zakładki Overview (Przegląd) w sekcji Attended Sysupgrade i kliknij przycisk Search for firmware upgrade (Szukaj aktualizacji oprogramowania).
Gdy router nie może połączyć się z Twoim lokalnym ASU
Kiedy ustawiłem adres własnego serwera w OpenWrt i kliknąłem przycisk Search for firmware upgrade, utknąłem na komunikacie:
Searching...
Searching for an available sysupgrade of 25.12.1 - r32768-b21cfa8f8c
Moje logi Dockera pokazały wcześniejsze próby testowe, gdy uruchamiałem http://192.168.1.12:8123/api/distros w przeglądarce, ale nie było widać żadnych śladów komunikacji z routerem.
Z jakiegoś powodu router może blokować sam sobie dostęp do adresu IP w sieci LAN. To dość dziwne, ale muszę to sprawdzić.
Zalogowałem się przez SSH na mój router i wykonałem polecenie:
curl -I http://192.168.1.12:8123/api/v1/overview
LUB, jeśli nie masz zainstalowanego narzędzia curl:
wget -qO- http://192.168.1.12:8123/api/v1/overview
i otrzymałem:
HTTP/1.1 405 Method Not Allowed
date: Sun, 29 Mar 2026 07:56:52 GMT
server: uvicorn
allow: GET
content-length: 31
content-type: application/json
Oznacza to, że pomimo błędu HTTP/1.1 405 Method Not Allowed, mój router i serwer Ubuntu komunikują się ze sobą bez zarzutu. Żadne zapory sieciowe (firewalle) nie blokują połączenia, a routing sieciowy jest poprawny. Logi w kontenerze Docker (asu-server) również potwierdzają aktywność ze strony routera.
Czas wykonać kolejne polecenie.
owut check --verbose
To pozwoli potwierdzić, czy router komunikuje się z naszym własnym serwerem ASU, oraz wyświetli więcej informacji o przyczynach ewentualnego niepowodzenia.
Jeśli nie masz zainstalowanego pakietu owut w swoim systemie, dodaj go za pomocą poniższego polecenia:
apk update
apk add owut
Wszystko wyglądało dobrze, więc zbadałem połączenie LuCI w przeglądarce Chrome przy pomocy Narzędzi Deweloperskich (DevTools). Po kliknięciu przycisku wyszukiwania nowego oprogramowania znalazłem błędy dotyczące CORS (Cross-Origin Resource Sharing) oraz przekierowań 301.
Teoretycznie mógłbym pozostać przy SSH i wykonać polecenie owut upgrade, aby pójść dalej, ale zależy mi na naprawieniu interfejsu graficznego.
Problem polega na tym, że przeglądarka wymaga od serwera wysłania nagłówka Access-Control-Allow-Origin, aby potwierdzić, że ma on prawo do udostępniania danych. Oficjalny kontener Docker openwrt/asu obecnie domyślnie nie wysyła tego nagłówka, więc musimy to naprawić.
Zacznijmy od cofnięcia się o krok i tymczasowego usunięcia kontenera asu-server.
sudo docker stop asu-server
sudo docker rm asu-server
Na serwerze utworzę plik, który przejmie działanie istniejącej aplikacji i wstrzyknie odpowiednie nagłówki do przeglądarki.
nano cors_wrapper.py
Wklej poniższy kod do pliku.
from asu.main import app
from fastapi.middleware.cors import CORSMiddleware
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
Teraz uruchommy kontener wraz z naszym “wrapperem” (opakowaniem).
sudo docker run -d \
--name asu-server \
--network asu-network \
-p 8123:8000 \
-e REDIS_URL="redis://asu-redis:6379/0" \
-e PUBLIC_PATH="/app/public" \
-v asu-data:/app/public \
-v $(pwd)/cors_wrapper.py:/app/cors_wrapper.py:ro \
--restart unless-stopped \
openwrt/asu:latest \
uv run uvicorn --host 0.0.0.0 cors_wrapper:app
Teraz, gdy przeglądarka wysyła zapytanie do punktu końcowego /api/v1/overview bez ukośnika na końcu, serwer Pythona generuje przekierowanie 301 Redirect, aby go dodać. Poprzednio przeglądarka blokowała to przekierowanie z powodu braku nagłówków CORS. Obecnie CORSMiddleware gwarantuje, że każda odpowiedź — nawet przekierowanie — zawiera nagłówek Access-Control-Allow-Origin, co pozwala przeglądarce bezproblemowo podążać wyznaczoną ścieżką.
Teraz, po kliknięciu przycisku Search for firmware upgrade, natychmiast otrzymuję odpowiedź:
I wtedy kliknąłem Request firmware image, tylko po to, by zobaczyć komunikat: Error building the firmware image (Błąd podczas budowania obrazu oprogramowania).
Błąd budowania obrazu oprogramowania – nie znaleziono programu Podman
podman.errors.exceptions.NotFound: 404 Client Error: Not Found
Niedawno deweloperzy OpenWrt całkowicie przepisali backend ASU. Ze względów bezpieczeństwa (aby zapobiec nadaniu zbyt wysokich uprawnień procesom budowania), na sztywno zaprogramowali workera tak, by do uruchamiania kontenerów ImageBuilder używał wyłącznie narzędzia Podman zamiast Dockera.
Mimo że z powodzeniem uruchamiamy workera wewnątrz kontenera Docker, skrypt Pythona w tym kontenerze korzysta z biblioteki specyficznej dla Podmana (podman.version()). Łączy się on z udostępnionym przez nas gniazdem /var/run/docker.sock i oczekuje odpowiedzi od Podmana, ale zamiast tego odpowiada Docker. Skrypt prosi o ścieżkę API charakterystyczną dla Podmana, Docker zwraca błąd „404 Page Not Found” i cały proces kompilacji się wywala.
Dobra wiadomość jest taka, że nie musimy porzucać naszej konfiguracji opartej na Dockerze. Musimy jedynie zainstalować Podmana obok Dockera na serwerze Ubuntu i zamiast gniazda Dockera, przekazać kontenerowi roboczemu gniazdo Podmana.
sudo apt update
sudo apt install podman
Domyślnie Podman nie utrzymuje usługi działającej w tle, tak jak robi to Docker. Musimy zatem aktywować jego gniazdo nasłuchujące (listening socket), aby worker ASU mógł się z nim komunikować.
sudo systemctl enable --now podman.socket
Teraz musimy od nowa utworzyć workera ASU.
sudo docker stop asu-worker
sudo docker rm asu-worker
Następnie uruchom poprawione polecenie dla workera. Zwróć uwagę, że obie ścieżki na końcu zostały zmienione na /run/podman/podman.sock.
sudo docker run -d \
--name asu-worker \
--network asu-network \
-e REDIS_URL="redis://asu-redis:6379/0" \
-e PUBLIC_PATH="/app/public" \
-e CONTAINER_SOCKET_PATH="/run/podman/podman.sock" \
-v asu-data:/app/public \
-v /run/podman/podman.sock:/run/podman/podman.sock \
--restart unless-stopped \
openwrt/asu:latest \
uv run rqworker --logging_level INFO
Ponieważ LuCI ma tendencję do przechowywania ostatniego komunikatu o błędzie w pamięci podręcznej, kliknięcie przycisku Request firmware image spowoduje ponowne wyświetlenie poprzedniego błędu. Aby to obejść, musimy albo zrestartować router poleceniem reboot, albo wyczyścić pliki tymczasowe utworzone przez ASU (które zazwyczaj są usuwane podczas restartu).
Uruchom poniższe polecenie na swoim routerze OpenWrt:
rm -rf /tmp/owut* /tmp/attended*
Wróćmy do interfejsu webowego OpenWrt i kliknijmy przycisk Request firmware image jeszcze raz. Worker nawiąże teraz pomyślnie połączenie z Podmanem, pobierze ImageBuilder i skompiluje nasze niestandardowe oprogramowanie.
Tyle teorii!
Tak jak w moim przypadku, ponownie zakończyło się to niepowodzeniem.
invalid config provided: pasta networking is only supported for rootless mode
Ktoś ma ochotę na makaron (pasta)? 🍝
Pasta to dość osobliwie nazwany sterownik sieciowy, którego Podman używa do bezpiecznego łączenia kontenerów z Internetem.
Dzieje się tak, ponieważ w poprzednim kroku uruchomiliśmy Podmana za pomocą sudo systemctl. To aktywowało gniazdo Podmana na poziomie Administratora. Skrypt workera sprawdził gniazdo, zauważył, że działa ono z uprawnieniami root, wpadł w panikę, ponieważ spodziewał się użytkownika bez uprawnień roota (rootless), i wyrzucił błąd „500 Internal Server Error”.
Cofnijmy się raz jeszcze o krok.
Uruchom poniższe polecenie, aby wyłączyć gniazdo Podmana na poziomie root, które utworzyliśmy wcześniej.
sudo systemctl disable --now podman.socket
Teraz włączymy gniazdo dla Twojego standardowego użytkownika Ubuntu bez uprawnień administratora.
Nie używaj
sudodo tego polecenia! Musi ono zostać uruchomione z poziomu zwykłego użytkownika.
systemctl --user enable --now podman.socket
Jednak ze względu na to, że prawdopodobnie jesteśmy zalogowani do naszego serwera Ubuntu przez SSH, system próbuje oszczędzać zasoby i nie uruchamia dedykowanego menedżera systemd działającego w tle dla Twojego konkretnego konta użytkownika. Kiedy prosisz o uruchomienie usługi na poziomie użytkownika, takiej jak Podman, operacja kończy się niepowodzeniem, ponieważ instancja systemd użytkownika nie jest aktywna.
Musimy poinstruować Ubuntu, aby pozwoliło Twojemu kontu użytkownika (mój użytkownik to: darek, zamień to odpowiednio na swoją nazwę) na stałe uruchamiać usługi w tle, nawet po wylogowaniu. Nazywa się to “lingering” (pozostawanie w tle).
Zanim to zrobimy, usuńmy wszelkie blokady, które mogą znajdować się w systemie.
sudo rm -rf /run/user/1000/podman
Włączmy „Linger” (pozostawanie w tle) dla naszego użytkownika.
sudo loginctl enable-linger darek
Czasami sesje SSH tracą kluczową zmienną środowiskową, której systemd potrzebuje do znalezienia folderu Twojego użytkownika. Aby mieć pewność, wymuśmy jej ustawienie:
export XDG_RUNTIME_DIR=/run/user/$(id -u)
Skoro Twój użytkownik posiada już odpowiednie uprawnienia, a ścieżka została ustawiona, uruchommy ponownie gniazdo (Socket).
systemctl --user enable --now podman.socket
W końcu musimy podmienić ścieżkę do gniazda (socket) w naszym poleceniu Dockera dla kontenera workera.
Zacznijmy od usunięcia kontenera asu-worker.
sudo docker stop asu-worker
sudo docker rm asu-worker
I uruchom go ponownie z poprawną ścieżką do gniazda w trybie rootless.
Jeśli z jakiegoś powodu Twój identyfikator użytkownika (User ID) to nie
1000, będziesz musiał zmienić liczbę1000w drugiej linii parametru-v; jednak w 99% przypadków na Ubuntu jest to właśnie1000.
sudo docker run -d \
--name asu-worker \
--network asu-network \
-e REDIS_URL="redis://asu-redis:6379/0" \
-e PUBLIC_PATH="/app/public" \
-e CONTAINER_SOCKET_PATH="/run/podman/podman.sock" \
-v asu-data:/app/public \
-v /run/user/1000/podman/podman.sock:/run/podman/podman.sock \
--restart unless-stopped \
openwrt/asu:latest \
uv run rqworker --logging_level INFO
Uruchommy naszą szybką procedurę czyszczenia pamięci podręcznej na routerze OpenWrt.
rm -rf /tmp/owut* /tmp/attended*
Wróć do LuCI i ponownie naciśnij ten zielony przycisk.
Tym razem udało mi się przekroczyć próg 10%.
11:16:35 Successfully completed asu.build.build(BuildRequest(distro='openwrt', version='25.12.2', version_code='', target='...) job in 0:05:43.333466s on worker 172b8078680c4f64989ef57693b834a2
11:16:35 default: Job OK (f7b1b5f55f2de817f4d9ed18026a1ac8c31d664ef30a612d5430f14d899cb409)
11:16:35 Result is kept for 10800 seconds
Zajęło to nieco mniej niż 6 minut (na moim ASUS Chromebox 3) i mój lokalnie wygenerowany obraz był gotowy. Pozostawiłem zaznaczoną opcję zachowania konfiguracji, a po ponownym uruchomieniu wszystko działa świetnie!
W ten sposób stworzyłem własny, niezależny serwer ASU, który odciąży serwery OpenWrt i pozwoli mi na lepszą kontrolę nad całym procesem!















Komentarze i Reakcje