Skip to content

Custom Domain Setup for Lenny

Roni bhakta edited this page Mar 4, 2026 · 1 revision

This guide explains how to make a Lenny instance accessible over a custom subdomain with SSL.

Background: How Lenny Constructs URLs

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.


Architecture Overview

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.


Step 1: DNS

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.


Step 2: SSL Termination

Choose one of the two approaches below. The Lenny config changes in Step 3 are the same for both.


Option A — Cloudflare (recommended if you have Cloudflare managing your domain)

  1. In the Cloudflare dashboard for your domain, ensure the A record you added in Step 1 has the proxy enabled (orange cloud icon).

  2. 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).
  3. That's it for infrastructure. Cloudflare handles certificate issuance and renewal automatically.

Traffic flow:

User → HTTPS → Cloudflare edge (SSL terminated) → HTTP → EC2:8080 → Lenny

Option B — Host Nginx + Let's Encrypt (no Cloudflare)

SSH into the server and run:

sudo apt update
sudo apt install -y nginx certbot python3-certbot-nginx

Create 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 nginx

Get the SSL certificate (Certbot will auto-update the nginx config):

sudo certbot --nginx -d your-subdomain.yourdomain.com

Verify auto-renewal works:

sudo certbot renew --dry-run

Traffic flow:

User → HTTPS → Host Nginx (SSL terminated, Certbot cert) → HTTP → 127.0.0.1:8080 → Lenny

Step 3: Update Lenny Config

This step is required regardless of which SSL option you chose.

3a. Set LENNY_PROXY in .env

Open the .env file in the Lenny root directory and set:

LENNY_PROXY=https://your-subdomain.yourdomain.com

This tells Lenny to use your subdomain as the base for all generated URLs — manifest links, reader links, upload callbacks, OPDS entries, etc.

3b. Update reader.env

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

3c. Restart the containers

Run 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 reader

Why two separate commands?

  • make restart force-recreates only the API container. It picks up LENNY_PROXY from .env without touching any other container, volume, or data.
  • The reader must be rebuilt (not just restarted) because NEXT_PUBLIC_MANIFEST_ALLOWED_DOMAINS is a Next.js build-time variable — it gets baked into the app at build time, not read at runtime. The targeted docker compose up --build reader rebuilds only the reader container and leaves the database, S3, and all other services untouched.

Avoid make build and make rebuild for this operation. Both commands run docker compose down --volumes which wipes the database and S3 storage before rebuilding — destructive on a live installation.


Verification

  1. Visit https://your-subdomain.yourdomain.com/v1/api/ — you should see the Lenny interface over HTTPS.
  2. 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.
  3. Visit https://your-subdomain.yourdomain.com/v1/api/opds Check OPDS feed entries contain your subdomain in acquisition links.

Troubleshooting

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.