Self-hosting
If your requirement is that no comment data touches a vendor, self-hosting is usually a
better answer than the github or external backends. You keep the full feature set and
give up nothing.
You need the Go server, a Postgres database, and somewhere to serve the widget bundle.
Run the server
docker run -p 8080:8080 \ -e DATABASE_URL="postgres://…" \ -e GUTTERNOTE_ENV=production \ ghcr.io/gutternote-dev/server:latestThe image is distroless and runs as non-root. Configuration is entirely environment-driven:
| Variable | Default | Description |
|---|---|---|
DATABASE_URL | — | Required. Postgres connection string. |
GUTTERNOTE_ADDR | :8080 | Listen address. |
GUTTERNOTE_ENV | development | production switches logs to JSON. |
GUTTERNOTE_LOG_LEVEL | info | debug, info, warn, error. |
GUTTERNOTE_TRUSTED_PROXIES | false | Read the client IP from X-Forwarded-For. Enable only behind a proxy you control. |
GUTTERNOTE_GITHUB_TOKEN | — | Only if you use the github backend. |
Migrate
make -C apps/server migrate DATABASE_URL="postgres://…"Migrations are goose SQL files in apps/server/internal/migrate/sql/, embedded
in the binary and applied in filename order — the image that ships the code
ships its schema.
Serve the widget
The widget is a single static file. Serve it from any origin your previews can reach, with a long immutable cache on versioned paths:
Cache-Control: public, max-age=31536000, immutableThen point the script tag at your own API:
<script src="https://cdn.acme.com/gutternote/v1/widget.js" data-gutternote-key="pk_live_..." data-gutternote-api="https://gutternote-api.acme.com" defer></script>What you’re responsible for
Backups, upgrades, and TLS. The server is stateless — all state is in Postgres — so scale it horizontally and treat the database as the only thing to back up.