Self-hosting Cusdis Comments with Caddy on a VPS
Table of Contents
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 secretsCaddyfile— 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#
- Open
https://comments.yourdomain.com - Log in with the credentials from the
.envfile - Click “Add a site”
- 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: nullis required for a widget embedded in ansrcdociframe- 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.