Wstęp#

Kiedy szukałem systemu komentarzy, szybko zdałem sobie sprawę, że większość dostępnych rozwiązań albo śledzi użytkowników, albo wymaga ciężkiego JavaScriptu, albo po prostu kosztuje. Cusdis wyróżnia się na tym tle. Jest lekki, open-source i od początku zaprojektowany z myślą o self-hostingu.

W tym wpisie pokazuję, jak wdrożyć Cusdis na dowolnym VPS, używając Dockera i Caddy jako reverse proxy, z poprawnie skonfigurowanym CORS-em.

Dlaczego Cusdis?#

Zanim przejdę do konfiguracji, kilka słów o tym, co skłoniło mnie do wyboru właśnie tego narzędzia:

  • Prywatność — brak śledzenia przez strony trzecie
  • Lekkość — widget waży około 5 KB po gzipie
  • Kontrola — dane zostają na Twoim serwerze
  • Zerowy koszt — żadnych miesięcznych subskrypcji
  • Prostota — baza SQLite i minimalne wymagania sprzętowe

Po co w ogóle reverse proxy?#

Cusdis to aplikacja Node.js z wbudowanym serwerem HTTP i technicznie rzecz biorąc, mógłby sam odpowiadać na żądania z Internetu. Ale nie powinieneś tak robić, i zaraz wyjaśnię dlaczego.

Problem z CORS-em#

Jeśli osadzasz komentarze Cusdis na swojej domenie, a sama aplikacja Cusdis działa pod innym adresem, przeglądarka zablokuje połączenie bez odpowiednich nagłówków CORS. Żeby to naprawić bezpośrednio w Cusdis, musiałbyś edytować kod źródłowy i przebudowywać aplikację. W Caddy wystarczy jeden wpis w pliku konfiguracyjnym.

Problem z SSL-em#

Wbudowany serwer Node.js nie potrafi samodzielnie uzyskać certyfikatów SSL. Musiałbyś ręcznie konfigurować certbot, wstrzykiwać ścieżki do certyfikatów i pisać skrypty automatycznego odnawiania co 90 dni. Caddy robi to wszystko automatycznie — wystarczy podać nazwę domeny.

Problem z odpornością na ataki#

Serwery takie jak Caddy czy Nginx są napisane z myślą o walce z ruchem sieciowym. Caddy powstał w Go, który świetnie radzi sobie z atakami DDoS, wolnymi klientami jak Slowloris, czy tysiącami równoczesnych połączeń. Node.js jest jednowątkowy — jego zadaniem jest logika biznesowa, a nie ochrona przed złośliwym ruchem.

Problem z plikami statycznymi#

Reverse proxy potrafi kompresować odpowiedzi w formacie Gzip lub Brotli i cache’ować pliki statyczne. Serwowanie statyki przez Node.js niepotrzebnie obciąża główny wątek CPU, który powinien obsługiwać komentarze.

Podsumowując: Caddy działa jak pancerna tarcza przed Cusdisem. Zajmuje się HTTPS, nagłówkami, ochroną i kompresją. Do Cusdisa trafiają tylko czyste żądania biznesowe.

Wymagania#

  • VPS z Dockerem i Docker Compose
  • Domena lub subdomena wskazująca na ten VPS
  • Podstawowa znajomość terminala

Struktura projektu#

Zaczynam od stworzenia dedykowanego katalogu:

mkdir -p /cusdis && cd /cusdis

W tym katalogu będą trzy pliki:

  • compose.yaml — definicja serwisów Docker
  • .env — konfiguracja i sekrety
  • Caddyfile — konfiguracja reverse proxy z CORS

Konfiguracja Docker Compose#

Tworzę plik compose.yaml:

services:
  cusdis:
    image: djyde/cusdis:latest
    env_file: .env
    volumes:
      - cusdis_data:/data
    restart: unless-stopped

  caddy:
    image: caddy:alpine
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./Caddyfile:/etc/caddy/Caddyfile
      - caddy_data:/data
      - caddy_config:/config
    restart: unless-stopped

volumes:
  cusdis_data:
  caddy_data:
  caddy_config:

Cusdis nasłuchuje wewnętrznie na porcie 3000. Caddy przejmuje ruch zewnętrzny i automatycznie obsługuje HTTPS.

Konfiguracja środowiska#

Tworzę plik .env z losowo wygenerowanymi sekretami:

JWT_SECRET=$(openssl rand -base64 32)
PASSWORD=$(openssl rand -base64 16)

cat > .env << EOF
PORT=8000
USERNAME=admin
PASSWORD=${PASSWORD}
JWT_SECRET=${JWT_SECRET}
DB_TYPE=sqlite
DB_URL=file:/data/db.sqlite
NEXTAUTH_URL=https://comments.twojadomena.pl
EOF

echo "Hasło admina: ${PASSWORD}"

Ważne: Zapisz wygenerowane hasło przed zamknięciem terminala — będzie potrzebne do pierwszego logowania.

Konfiguracja Caddy z CORS#

Tworzę Caddyfile:

:your_port {
    reverse_proxy cusdis:3000

    @widget path /js/iframe.umd.js
    header @widget Access-Control-Allow-Origin https://yourdomain.com
    header @widget Vary Origin
}

Dwa słowa wyjaśnienia. Matcher @widget dopasowuje wyłącznie ścieżkę do skryptu widgetu. Nagłówek Access-Control-Allow-Origin mówi przeglądarce, że tylko Twoja domena może odczytać ten zasób. Nagłówek Vary: Origin informuje cache, żeby rozróżniał odpowiedzi w zależności od nagłówka Origin — bez tego możesz trafić na nieprzewidywalne zachowanie przy cachowaniu.

Zamień comments.twojadomena.pl i twojadomena.pl na swoje własne domeny.

Wdrożenie#

docker compose up -d
docker compose logs -f

Caddy przy pierwszym uruchomieniu automatycznie pobiera certyfikat SSL z Let’s Encrypt. Nie musisz robić nic więcej.

Pierwsza konfiguracja w Cusdis#

  1. Otwórz https://comments.twojadomena.pl
  2. Zaloguj się danymi z pliku .env
  3. Kliknij “Add a site”
  4. Skopiuj wygenerowane App ID

Osadzenie widgetu na stronie#

Dodaj ten fragment HTML na każdej stronie, gdzie chcesz wyświetlać komentarze:

<div id="cusdis_thread"
  data-host="https://comments.twojadomena.pl"
  data-app-id="twoje-app-id"
  data-page-id="unikalne-id-strony"
  data-page-url="https://twojadomena.pl/strona"
  data-page-title="Tytuł strony"
  data-theme="dark"
></div>
<script async defer src="https://comments.twojadomena.pl/js/cusdis.es.js"></script>

Weryfikacja CORS#

Sprawdzam, czy nagłówki są poprawnie ustawione. Wysyłam preflight request:

curl -v -X OPTIONS \
  -H "Origin: null" \
  -H "Access-Control-Request-Method: GET" \
  -H "Access-Control-Request-Headers: x-timezone-offset" \
  "https://comments.twojadomena.pl/api/open/comments?page=1"

Poprawna odpowiedź powinna zawierać:

access-control-allow-origin: null
access-control-allow-credentials: true
access-control-allow-headers: *

Rozwiązywanie problemów#

“Provisional headers are shown”#

Przeglądarka zablokowała żądanie. Sprawdź dwie rzeczy: czy nagłówek Origin jest obsługiwany w Caddyfile i czy wszystkie wymagane nagłówki są wymienione w Access-Control-Allow-Headers.

Widget się nie ładuje#

Zaglądaj od razu do konsoli przeglądarki — błędy CORS są tam wyraźnie opisane. Najczęstsze przyczyny to brak obsługi null origin, brak x-timezone-offset w dozwolonych nagłówkach lub Cloudflare cachujący stare odpowiedzi. W tym ostatnim przypadku możesz przetestować z parametrem ?nocache=1.

Kontener crashuje#

Cusdis to aplikacja Next.js i potrzebuje około 200 MB RAM. Sprawdzam to w ten sposób:

docker stats --no-stream
free -h

Uwagi bezpieczeństwa#

Ta konfiguracja jest odpowiednia dla publicznego systemu komentarzy. Kilka rzeczy warto mieć na uwadze:

  • Access-Control-Allow-Origin: null jest konieczne dla widgetu osadzonego w srcdoc iframe
  • Komentarze domyślnie wymagają moderacji przed publikacją
  • Warto rozważyć dodanie rate limitingu jako ochrony przed spamem

Utrzymanie#

Aktualizacja Cusdisa sprowadza się do dwóch komend:

docker compose pull
docker compose up -d

Backup bazy danych:

docker compose exec cusdis cat /data/db.sqlite > backup.sqlite

Podsumowanie#

Self-hosting Cusdis daje lekki, prywatny system komentarzy przy minimalnych zasobach — około 200 MB RAM, śladowe użycie CPU i kilka megabajtów dysku dla bazy SQLite.