Introduction#

When I was looking for a comment system, I quickly realized that most available solutions either track users, require heavy JavaScript, or simply cost money. Cusdis stands out from the crowd. It is lightweight, open-source, and built from the ground up with self-hosting in mind.

In this post, I show how to deploy Cusdis on any VPS using Docker and Caddy as a reverse proxy, with correctly configured CORS headers.

Why Cusdis?#

Before I get to the setup, a few words about why I chose this tool:

  • Privacy — no third-party tracking
  • Lightweight — the widget is about 5 KB after gzip compression
  • Control — your data stays on your own server
  • Zero cost — no monthly subscriptions
  • Simplicity — SQLite database and minimal hardware requirements

Why Use a Reverse Proxy at All?#

Cusdis is a Node.js application with a built-in HTTP server. Technically, it could respond to internet requests on its own. But you should not do that, and here is why.

The CORS Problem#

If you embed Cusdis comments on your domain, but the Cusdis app runs under a different address, the browser will block the connection without the correct CORS headers. To fix this directly in Cusdis, you would have to edit the source code and rebuild the application. In Caddy, one line in the config file is enough.

The SSL Problem#

The built-in Node.js server cannot get SSL certificates on its own. You would have to manually configure certbot, inject certificate paths, and write renewal scripts that run every 90 days. Caddy does all of this automatically — you just provide the domain name.

The Attack Resistance Problem#

Servers like Caddy and Nginx are designed to handle hostile network traffic. Caddy is written in Go, which handles DDoS attacks, slow clients like Slowloris, and thousands of simultaneous connections very well. Node.js is single-threaded — its job is business logic, not protection against malicious traffic.

The Static Files Problem#

A reverse proxy can compress responses using Gzip or Brotli and cache static files. Serving static files through Node.js unnecessarily loads the main CPU thread, which should be handling comments.

To sum up: Caddy acts like an armored shield in front of Cusdis. It takes care of HTTPS, headers, protection, and compression. Only clean business requests reach Cusdis.

Requirements#

  • A VPS with Docker and Docker Compose
  • A domain or subdomain pointing to that VPS
  • Basic knowledge of the terminal

Project Structure#

I start by creating a dedicated directory:

mkdir -p /cusdis && cd /cusdis

This directory will contain three files:

  • compose.yaml — Docker service definitions
  • .env — configuration and secrets
  • Caddyfile — reverse proxy configuration with CORS

Docker Compose Configuration#

I create the compose.yaml file:

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 listens internally on port 3000. Caddy takes over external traffic and handles HTTPS automatically.

Environment Configuration#

I create the .env file with randomly generated secrets:

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.yourdomain.com
EOF

echo "Admin password: ${PASSWORD}"

Important: Save the generated password before closing the terminal — you will need it for the first login.

Caddy Configuration with CORS#

I create the 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
}

A quick explanation. The @widget matcher targets only the path to the widget script. The Access-Control-Allow-Origin header tells the browser that only your domain can read that resource. The Vary: Origin header tells the cache to treat responses differently depending on the Origin header — without this, you may run into unpredictable caching behavior.

Replace comments.yourdomain.com and yourdomain.com with your own domains.

Deployment#

docker compose up -d
docker compose logs -f

On the first start, Caddy automatically gets an SSL certificate from Let’s Encrypt. You do not need to do anything else.

First Setup in Cusdis#

  1. Open https://comments.yourdomain.com
  2. Log in with the credentials from the .env file
  3. Click “Add a site”
  4. Copy the generated App ID

Embedding the Widget on Your Page#

Add this HTML snippet to every page where you want to show comments:

<div id="cusdis_thread"
  data-host="https://comments.yourdomain.com"
  data-app-id="your-app-id"
  data-page-id="unique-page-id"
  data-page-url="https://yourdomain.com/page"
  data-page-title="Page Title"
  data-theme="dark"
></div>
<script async defer src="https://comments.yourdomain.com/js/cusdis.es.js"></script>

Verifying CORS#

I check whether the headers are correctly set by sending a 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.yourdomain.com/api/open/comments?page=1"

A correct response should include:

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

Troubleshooting#

“Provisional headers are shown”#

The browser blocked the request. Check two things: whether the Origin header is handled in the Caddyfile, and whether all required headers are listed in Access-Control-Allow-Headers.

Widget Does Not Load#

Go straight to the browser console — CORS errors are clearly described there. The most common causes are: missing support for null origin, missing x-timezone-offset in the allowed headers, or Cloudflare caching old responses. In the last case, you can test with the ?nocache=1 parameter.

Container Crashes#

Cusdis is a Next.js application and needs about 200 MB of RAM. I check this with:

docker stats --no-stream
free -h

Security Notes#

This configuration is suitable for a public comment system. A few things worth keeping in mind:

  • Access-Control-Allow-Origin: null is required for a widget embedded in an srcdoc iframe
  • Comments require moderation before they are published by default
  • Consider adding rate limiting as protection against spam

Maintenance#

Updating Cusdis comes down to two commands:

docker compose pull
docker compose up -d

Database backup:

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

Summary#

Self-hosting Cusdis gives you a lightweight, private comment system with minimal resources — about 200 MB of RAM, negligible CPU usage, and a few megabytes of disk space for the SQLite database.