Self-hosting remark42 obok istniejącego Caddy na VPS
Spis treści
Wstęp#
Kontynuuję serię przeglądu systemów komentarzy pod Hugo. Po Self-hosting Cusdis z Caddy na VPS
przyszedł czas na remark42
. W przeciwieństwie do Cusdis (który ma zarządzaną wersję na cusdis.com) i Disqus (który jest wyłącznie SaaS), remark42 nie oferuje wariantu managed. Jedyna droga to self-hosting. Jeśli chcecie w ogóle spróbować remark42, ten wpis dotyczy dokładnie tego setupu.
Setup z poprzedniego wpisu rozrósł się z czasem. Do Caddy’ego dedykowanego dla Cusdisa (konfiguracja w Caddyfile, porty 80/443 na hoście) dołożyłem kolejne serwisy. Każdy musiał dopisywać się do tego samego Caddyfile, co przy większej liczbie usług robiło się uciążliwe i podatne na literówki. Przesiadłem się więc na lucaslorentz/caddy-docker-proxy
: jedna instancja Caddy, konfigurację proxy buduje sam z labeli Dockera na kontenerach. Cusdis też został przemigrowany pod ten wzorzec.
remark42 stawiam już w tym nowym układzie. Domyślne repo remark42 zakłada natomiast, że postawicie własny Caddy razem z serwisem, co dołożyłoby drugą instancję reverse-proxy na tym samym hoście. Tego chciałem uniknąć. Poniżej pokazuję jak podpiąć remark42 pod istniejący caddy-docker-proxy przez labele, czyli zero drugiego Caddy, zero konfliktu portów, pełen setup w kilkanaście minut. Efekt: obraz umputun/remark42:latest waży 91 MB, w spoczynku zjada 14 MiB RAM, baza BoltDB przy pierwszym komentarzu ma 64 KB. Liczby, które mogą zaskoczyć każdego kto pamięta Disqusa.
Punkt wyjścia: caddy-docker-proxy i sieć edge#
W moim setupie osobne compose.yaml uruchamia lucaslorentz/caddy-docker-proxy
obserwujący /var/run/docker.sock. Przy każdym starcie kontenera skanuje jego labele i buduje z nich konfigurację reverse proxy.
services:
caddy:
image: lucaslorentz/caddy-docker-proxy:2.9-alpine
ports:
- "40080:40080"
- "40081:40081"
- "40082:40082"
# ... pozostałe zarezerwowane porty z zakresu wysokich
environment:
CADDY_INGRESS_NETWORKS: edge
networks:
- edge
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- caddy_data:/data
- caddy_config:/config
networks:
edge:
name: edge
Każdy inny serwis dołącza się do sieci edge i deklaruje przez labele na jakim porcie ma go publikować Caddy. Cusdis po migracji na ten wzorzec wygląda tak:
services:
cusdis:
image: djyde/cusdis:latest
networks:
- edge
labels:
caddy: ":40080"
caddy.reverse_proxy: "{{upstreams 3000}}"
Caddy dokłada wtedy blok „słuchaj na :40080, przekierowuj do kontenera cusdis:3000". Ten sam wzór stosuję dla remark42.
Wdrożenie krok po kroku#
1. Adaptacja compose.yaml#
Oficjalny compose remark42 publikuje port 8080 bezpośrednio na host i zakłada Caddyfile na hoście. Oba te założenia u mnie nie pasują. Przerabiam serwis w tym samym wzorcu co Cusdis po migracji: dołączenie do sieci edge, labele Caddy zamiast ports.
services:
remark42:
image: umputun/remark42:latest
container_name: blog-remark42
env_file: .env
volumes:
- remark42_data:/srv/var
restart: unless-stopped
mem_limit: 200m
networks:
- edge
labels:
caddy: ":40081"
caddy.reverse_proxy: "{{upstreams 8080}}"
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
volumes:
remark42_data:
networks:
edge:
name: edge
external: true
Dwa detale warte wskazania:
container_name: blog-remark42, bo Docker Compose bez tego wpisu generuje nazwy w formacie<projekt>-<serwis>-<indeks>. Przy standardowej konfiguracji wychodzi z tego coś w stylublog-remark42-remark42-1, długo i bez logiki. Nadpisaniecontainer_namedaje krótką, przewidywalną nazwę. Świadomy koszt: nie można wtedy skalować serwisu, ale remark42 z BoltDB i tak nie skaluje się horyzontalnie./srv/varjako volume mount, bo oficjalne przykłady remark42 potrafią pokazywać/var/lib/remark42, a binarka zapisuje wyłącznie do/srv/var. Przy złym mount dane leciałyby efemerycznie przy każdym restarcie mimo obecności wolumenu. Warto sprawdzić przy każdej instalacji.
2. Konfiguracja .env#
remark42 czyta konfigurację z zmiennych środowiskowych. Minimalny zestaw dla self-hostingu wygląda tak:
# .env
REMARK_URL=https://remark42.example.com
SITE=blog
SECRET=<32+ znaków losowych, np. z openssl rand -hex 32>
ALLOWED_HOSTS=self,https://example.com
# Anonymous auth, najniższa frykcja dla czytelnika, bez OAuth
AUTH_ANON=true
# Wypełnić po pierwszym zalogowaniu (patrz krok 4)
# ADMIN_SHARED_ID=
REMARK_URL to publiczna domena instancji remark42 (musi mieć TLS). ALLOWED_HOSTS to biała lista domen, z których widget może odwoływać się do API. Dodajcie tam domenę bloga który będzie osadzał komentarze. SECRET służy do podpisywania JWT-ów, wygenerujcie mocny random i trzymajcie go w .env poza gitem.
Auth zostawiam na start jako anonymous. Czytelnik podaje nick, remark42 tworzy deterministyczny identyfikator z hasha nazwy. Bez OAuth, bez emaila, minimalna frykcja. OAuth GitHub/Google można dołożyć potem gdy analityka pokaże że warto.
3. Uruchomienie#
Wystarczy standardowe:
docker compose up -d
docker compose logs --tail=20 remark42
W logach szukajcie potwierdzenia że serwis znalazł swoją bazę i wystartował:
[INFO] start server on port :8080
[INFO] bolt store for sites [{FileName:./var/blog.db SiteID:blog}]
[INFO] anonymous access enabled
[INFO] activate http rest server on :8080
Weryfikacja z zewnątrz:
curl https://remark42.example.com/ping
# oczekiwane: pong
4. Bootstrap admina#
ADMIN_SHARED_ID to identyfikator użytkownika, który dostanie uprawnienia administracyjne. Format <provider>_<hex> (dla anon: anonymous_<sha1(nick)>). Trzeba się najpierw zalogować, żeby ten identyfikator w ogóle powstał.
remark42 nie ma osobnego panelu logowania z formularzem. Konto powstaje przez widget osadzony na stronie albo bezpośrednio przez endpoint auth. Najprościej użyć drugiego wariantu, w pasku adresu przeglądarki:
https://remark42.example.com/auth/anonymous/login?user=admin&aud=blog
To ustawi JWT cookie i przekieruje do prostego endpointu. Sam identyfikator znajdziecie w payloadzie JWT (middle-part cookie, base64 z JSON-em) w polu user.id. Wklejcie do .env:
ADMIN_SHARED_ID=anonymous_a1b2c3...
I zrestartujcie serwis, żeby zmienna weszła w życie:
docker compose up -d --force-recreate
Kolejne logowanie tego samego nicka wypluje już JWT z attrs.admin: true.
5. TRUSTED_PROXY: produkcyjne bezpieczeństwo#
Po uruchomieniu remark42 loguje ostrzeżenie:
[WARN] --trusted-proxy not set: forwarding headers are trusted from any client
and can be spoofed to bypass rate limiting / vote dedup
remark42 identyfikuje klienta po nagłówku X-Forwarded-For, żeby móc egzekwować per-IP rate limit i dedup głosów. Bez TRUSTED_PROXY ufa temu nagłówkowi od dowolnego źródła, więc atakujący mógłby wpisać sfałszowany IP w request i obejść liczniki.
Sprawdźcie CIDR sieci Docker w której siedzi Caddy:
docker network inspect edge --format '{{range .IPAM.Config}}{{.Subnet}}{{end}}'
# przykładowo: 172.21.0.0/16
Dodajcie do .env:
TRUSTED_PROXY=172.21.0.0/16
Po docker compose up -d --force-recreate WARN znika.
Ważne zastrzeżenie: to sama flaga nie jest pełną obroną. TRUSTED_PROXY w remark42 chroni głównie przed atakami, w których ktoś próbuje uderzyć w kontener z pominięciem Caddy’ego. Pełna ochrona przed spoofingiem X-Forwarded-For wymaga też, żeby reverse proxy przed remark42 kasowało przychodzący nagłówek, a nie tylko doklejało do niego swój wpis. Docsy remark42 opisują to w kontekście autorskiego Reproxy, ale reguły są identyczne dla Caddy’ego: Configure with Reproxy
.
Wyniki#
Zmierzyłem podstawowe metryki tuż po starcie i po dodaniu pierwszego komentarza:
| Metryka | Wartość |
|---|---|
Rozmiar obrazu umputun/remark42:latest | 91 MiB |
| RAM (spoczynek) | 14 MiB / 200 MiB limit |
| RAM (po pierwszym komentarzu) | 13 MiB, mieści się w szumie |
| CPU (spoczynek) | 0.01–2.4% |
| BoltDB (pusta) | 40 KB |
| BoltDB (po pierwszym komentarzu) | 64 KB (+24 KB) |
Całe /srv/var (z avatarami, backupami) | 72 KB |
Cold-start (docker compose up -d) | ~1.0 s |
| Ping po starcie | ~250 ms (dominuje sieć, nie remark42) |
Kilka rzeczy które sam mnie zaskoczyły. Po pierwsze, 14 MiB RAM w spoczynku dla pełnego serwera komentarzy z bazą to bardzo mało. remark42 jest statycznie linkowaną binarką Go z wbudowanym serwerem HTTP i BoltDB w tym samym procesie. Zero runtime overhead, brak Node.js, brak PHP-FPM, brak zewnętrznej bazy. Cały stan aplikacji siedzi w jednym pliku .db.
Po drugie, 24 KB w bazie za jeden komentarz. Blog z tysiącem komentarzy zmieści się w mniej niż 30 MB, a backup to docker run --rm -v remark42_data:/data -v $PWD:/backup alpine tar czf /backup/$(date +%F).tar.gz -C /data . i śpicie spokojnie.
Multi-tenant: jedna instancja na kilka blogów#
remark42 obsługuje wiele stron natywnie, przez listę w SITE i odpowiednio poszerzony ALLOWED_HOSTS:
SITE=blog,docs,portfolio
ALLOWED_HOSTS=self,https://blog.example.com,https://docs.example.com,https://portfolio.example.com
Każdy blog w swoim widgetcie ustawia własne site_id. remark42 trzyma osobne wątki komentarzy per site, wszystko w tej samej instancji BoltDB. To realny plus dla kogoś kto ma kilka projektów: jeden VPS, jeden backup, jeden panel adminowy.
Warto od razu widzieć trade-off: awaria remark42 to awaria komentarzy na wszystkich blogach naraz. ADMIN_SHARED_ID jest globalny, więc jeden zestaw adminów moderuje wszystko. Jeśli to nie problem, wielotenancy w remark42 jest tak prosta jak w tej sekcji.
Kiedy remark42 self-hosted ma sens#
remark42 to inna filozofia niż Cusdis, mimo że oba są open source i „privacy-first". Cusdis nie ma sesji, każdy komentarz to formularz do zatwierdzenia przez admina i cross-domain działa bez zabaw z cookies. remark42 buduje pełną tożsamość komentującego (JWT, reputacja, upvote/downvote, edycja własnych wpisów w oknie czasowym), więc daje bogatszy UX kosztem większego widgetu (kilkanaście razy cięższego niż Cusdis) i pewnej konfiguracyjnej gimnastyki przy cross-domain.
Wybieram remark42 gdy zależy mi na:
- Wątkowaniu i głosach: dyskusje techniczne zyskują na strukturze.
- Tożsamości komentujących między wpisami: reputacja i możliwość blokady konkretnego usera.
- Możliwości edycji własnego komentarza: czytelnicy nie boją się kliknąć „Send" z literówką.
Wybieram Cusdis (albo Giscus) gdy priorytetem jest surowa wydajność widgetu albo unikanie sesji. Szczegółowe porównanie liczbowe będzie w osobnym wpisie zamykającym serię.
Podsumowanie#
Postawienie remark42 obok istniejącego caddy-docker-proxy sprowadza się do trzech decyzji: adaptacji compose.yaml (labele zamiast portów), wypełnienia .env (SECRET, SITE, ALLOWED_HOSTS, AUTH_ANON) i bootstrapu admina przez URL loginu. Cała procedura zajmuje kilkanaście minut i daje serwis, który zjada 14 MiB RAM i trzyma stan w jednym pliku bazy.
Trzy rzeczy które warto zapamiętać:
- Volume mount pilnujcie:
/srv/var, nie/var/lib/remark42. Popularna pomyłka w oficjalnych przykładach. TRUSTED_PROXYw produkcji jest obowiązkowy, jeśli nie chcecie żeby ktoś ominął rate limiting przezX-Forwarded-For.caddy-docker-proxy+ labele to komfortowy wzorzec dla wielu serwisów na jednym VPS. Czytelnie widać w compose każdego kontenera na jakim porcie i pod jakim proxy siedzi.
W kolejnym wpisie z serii pokażę integrację widgetu remark42 z Hugo, łącznie z rozwiązaniem cross-domain, bez konieczności hostowania remark42 pod subdomeną bloga.
Źródła i dalsze materiały#
- remark42 – dokumentacja – oficjalne docsy projektu
- remark42 na GitHubie – repo Umputuna
lucaslorentz/caddy-docker-proxy– Caddy który czyta konfigurację z labeli Dockera- Self-hosting Cusdis z Caddy na VPS
– poprzedni wpis serii, standalone setup Caddy z
Caddyfile(punkt wyjścia sprzed migracji nacaddy-docker-proxy)
$ komentarze --wczytaj