Skip to content
Serelane

Self-hosting

Serelane is one binary with the frontend inside it. It speaks plain HTTP on one port, writes to one directory, and needs nothing else.

docker run -d \
	--name serelane \
	-v serelane:/data \
	-p 8080:8080 \
	-e SERELANE_BASE_URL=https://roadmap.example.com \
	serelane/serelane:latest

/data is the only path Serelane writes to. It holds the SQLite database, the uploads and the generated secret key, so backing up that volume backs up the instance.

The container runs as uid 65532 and nothing else in the filesystem is writable. If you bind-mount a host directory instead of a named volume, chown it to 65532 first, or the first boot fails trying to create the database.

The repository ships a working docker-compose.yml:

services:
  serelane:
    image: serelane/serelane:latest
    restart: unless-stopped
    ports:
      - '8080:8080'
    volumes:
      - serelane-data:/data
    environment:
      SERELANE_BASE_URL: https://roadmap.example.com

volumes:
  serelane-data:

Every other setting is listed in the configuration reference.

Serelane terminates no TLS. It expects something in front of it, and that is where your certificate lives.

Whatever you use, set SERELANE_BASE_URL to the address people actually visit. Without it Serelane guesses from the Host header, and a proxy that does not forward one makes that guess wrong in emails and link previews.

roadmap.example.com {
	reverse_proxy localhost:8080
}

Caddy obtains and renews the certificate itself, which is why this is three lines.

server {
	listen 443 ssl;
	server_name roadmap.example.com;

	ssl_certificate     /etc/letsencrypt/live/roadmap.example.com/fullchain.pem;
	ssl_certificate_key /etc/letsencrypt/live/roadmap.example.com/privkey.pem;

	location / {
		proxy_pass http://127.0.0.1:8080;
		proxy_set_header Host              $host;
		proxy_set_header X-Real-IP         $remote_addr;
		proxy_set_header X-Forwarded-For   $proxy_add_x_forwarded_for;
		proxy_set_header X-Forwarded-Proto $scheme;
	}
}

With Compose, as labels on the service:

labels:
  - 'traefik.enable=true'
  - 'traefik.http.routers.serelane.rule=Host(`roadmap.example.com`)'
  - 'traefik.http.routers.serelane.entrypoints=websecure'
  - 'traefik.http.routers.serelane.tls.certresolver=letsencrypt'
  - 'traefik.http.services.serelane.loadbalancer.server.port=8080'

This one is easy to miss and quietly breaks two features.

SERELANE_TRUSTED_PROXIES=172.16.0.0/12

Serelane ignores X-Forwarded-For unless the request came from a network listed here. That default is the safe one: a header anybody can set is a header anybody can lie with.

The cost of leaving it unset behind a proxy is that every visitor appears to arrive from the proxy’s own address. Anonymous vote deduplication and rate limiting both key on the address, so both collapse onto a single bucket: one person can vote repeatedly, and one busy visitor can rate-limit everybody.

Set it to the network your proxy sits on. A bare address is accepted and treated as a single host, which is what you want for a sidecar.

Mail is optional, and Serelane degrades rather than fails without it.

With no SERELANE_SMTP_HOST, sign-in links and notifications are written to the log instead of being sent. That is fine for a board using anonymous identity. It is not fine for magic-link identity, where the sign-in link is the sign-in: nobody outside the log can read it.

SERELANE_SMTP_HOST=smtp.example.com
SERELANE_SMTP_PORT=587
SERELANE_SMTP_USERNAME=serelane
SERELANE_SMTP_PASSWORD=...
SERELANE_SMTP_FROM=roadmap@example.com
SERELANE_SMTP_TLS=starttls

SERELANE_SMTP_FROM is required once a host is set, and the instance refuses to boot without it.

Use starttls for port 587 and implicit for port 465. They are different connections, not a preference: 587 begins in the clear and is upgraded, 465 is TLS from the first byte.

# liveness: answers whenever the process can serve HTTP at all
curl -fsS https://roadmap.example.com/healthz

# readiness: also checks the database and the schema version
curl -fsS https://roadmap.example.com/readyz

Operations explains which of those belongs in which place, and how to back the instance up.