-
Notifications
You must be signed in to change notification settings - Fork 17
Custom Domain Setup for Lenny
This guide explains how to make a Lenny instance accessible over a custom subdomain with SSL.
Lenny does not simply proxy traffic — it generates absolute URLs internally that are embedded in responses. These URLs appear in:
- OPDS feed entries and acquisition links
- Readium manifest files (
manifest.json) - Reader app links passed to Thorium
- Upload callback headers
By default, Lenny uses its raw host and port (e.g. http://<server-ip>:8080) as the base for all these URLs. If you add a custom domain without telling Lenny, traffic will reach the server correctly but all internal links will still point to the raw IP and port, breaking the reader and other services.
The fix is the LENNY_PROXY environment variable. When set, every URL Lenny generates uses it as the base instead of the raw host/port.
User Browser
│
│ https://your-subdomain.yourdomain.com
▼
[ SSL Termination Layer ] ← Cloudflare proxy OR host-level Nginx + Certbot
│
│ http://127.0.0.1:8080
▼
[ Lenny Docker: Nginx on port 8080 ]
│
├──▶ FastAPI (port 1337) ← all URLs built using LENNY_PROXY
└──▶ Reader / Readium app
The SSL termination layer and the Lenny config change are both required. One without the other will not work correctly.
At your DNS provider, add an A record pointing your subdomain to the server's public IP address.
| Type | Name | Value |
|---|---|---|
| A | your-subdomain |
<server-public-ip> |
Allow a few minutes for DNS propagation before proceeding.
Choose one of the two approaches below. The Lenny config changes in Step 3 are the same for both.
-
In the Cloudflare dashboard for your domain, ensure the A record you added in Step 1 has the proxy enabled (orange cloud icon).
-
Go to SSL/TLS > Overview and set the mode to Full.
- Flexible: Cloudflare talks HTTP to your origin. Simple but not end-to-end encrypted.
- Full: Cloudflare talks HTTPS to your origin using any certificate (including self-signed). Recommended.
- Full (Strict): Requires a valid certificate on the origin (use a Cloudflare Origin Certificate if choosing this).
-
That's it for infrastructure. Cloudflare handles certificate issuance and renewal automatically.
Traffic flow:
User → HTTPS → Cloudflare edge (SSL terminated) → HTTP → EC2:8080 → Lenny
SSH into the server and run:
sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginxCreate a virtual host config:
sudo tee /etc/nginx/sites-available/lenny << 'EOF'
server {
listen 80;
server_name your-subdomain.yourdomain.com;
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;
}
}
EOF
sudo ln -s /etc/nginx/sites-available/lenny /etc/nginx/sites-enabled/
sudo nginx -t
sudo systemctl reload nginxGet the SSL certificate (Certbot will auto-update the nginx config):
sudo certbot --nginx -d your-subdomain.yourdomain.comVerify auto-renewal works:
sudo certbot renew --dry-runTraffic flow:
User → HTTPS → Host Nginx (SSL terminated, Certbot cert) → HTTP → 127.0.0.1:8080 → Lenny
This step is required regardless of which SSL option you chose.
Open the .env file in the Lenny root directory and set:
LENNY_PROXY=https://your-subdomain.yourdomain.comThis tells Lenny to use your subdomain as the base for all generated URLs — manifest links, reader links, upload callbacks, OPDS entries, etc.
Open reader.env and add your subdomain to the allowed domains list:
NEXT_PUBLIC_MANIFEST_ALLOWED_DOMAINS=127.0.0.1,localhost,your-subdomain.yourdomain.comRun these two commands from the Lenny root directory:
# 1. Restart the API container to pick up the new LENNY_PROXY value
make restart
# 2. Rebuild only the reader container to apply the updated allowed domains
docker compose -p lenny up -d --build readerWhy two separate commands?
-
make restartforce-recreates only the API container. It picks upLENNY_PROXYfrom.envwithout touching any other container, volume, or data. - The reader must be rebuilt (not just restarted) because
NEXT_PUBLIC_MANIFEST_ALLOWED_DOMAINSis a Next.js build-time variable — it gets baked into the app at build time, not read at runtime. The targeteddocker compose up --build readerrebuilds only the reader container and leaves the database, S3, and all other services untouched.
Avoid
make buildandmake rebuildfor this operation. Both commands rundocker compose down --volumeswhich wipes the database and S3 storage before rebuilding — destructive on a live installation.
- Visit
https://your-subdomain.yourdomain.com/v1/api/— you should see the Lenny interface over HTTPS. - Open a book in the reader and check that the manifest URL in the browser address bar uses your subdomain, not the raw IP and port.
- Visit
https://your-subdomain.yourdomain.com/v1/api/opdsCheck OPDS feed entries contain your subdomain in acquisition links.
Browser shows the site but reader/manifest links are broken
LENNY_PROXY is not set or the API container was not restarted. Double-check .env and run docker compose restart api.
Certificate error in browser
- Option A: Ensure the Cloudflare SSL/TLS mode is set to Full, not Off or Flexible.
- Option B: Certbot did not complete successfully. Re-run
sudo certbot --nginx -d your-subdomain.yourdomain.com.
DNS not resolving
Run dig your-subdomain.yourdomain.com or nslookup your-subdomain.yourdomain.com to check propagation. Wait a few minutes and retry.
Cloudflare: site loads but shows Cloudflare error 522 (connection timed out) The server's firewall is blocking Cloudflare's IPs. Ensure ports 80 and 443 are open in the server's security group / firewall rules.
Port 8080 not reachable
Confirm Lenny's Docker containers are running: docker compose ps. Confirm port mapping: docker ps should show 0.0.0.0:8080->80/tcp for the Nginx container.