Skip to content

HTTPS Setup

Ilyes BEN BRIK edited this page Jul 23, 2026 · 1 revision

HTTPS Setup

Bookoholik runs over plain HTTP by default. HTTPS is an opt-in feature provided by a separate compose overlay (docker-compose.ssl.yml). The base docker-compose.yml is never modified β€” you simply layer the SSL file on top when you want HTTPS.


Why HTTPS matters

Without HTTPS With HTTPS
Passwords sent in plain text over the network All traffic encrypted end-to-end
JWT tokens visible to anyone on the LAN Tokens protected in transit
Browsers warn "Not Secure" Green padlock, no warnings
Required for some browser features (PWA, clipboard) Full browser feature support

Architecture

Browser (HTTPS)
      β”‚ 443
      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚   ssl-proxy (nginx)   or   caddy        β”‚  ◄─ docker-compose.ssl.yml
β”‚   TLS termination + reverse proxy       β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
       β”‚ http://frontend:80   β”‚ http://backend:80
       β–Ό                      β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”         β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  frontend  β”‚         β”‚    backend     β”‚  ◄─ docker-compose.yml (base, unchanged)
β”‚ (nginx SPA)β”‚         β”‚ (PHP + Apache) β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜         β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

The SSL proxy sits in front of both containers. It:

  • Redirects HTTP β†’ HTTPS
  • Terminates TLS
  • Proxies /api/* β†’ backend:80
  • Proxies everything else β†’ frontend:80

Choose your SSL mode

SSL_MODE When to use
nginx-selfsigned Home LAN, no domain. Uses ./docker/ssl-gen.sh to create a self-signed cert.
nginx-letsencrypt You have a domain and already ran certbot. Mount your cert files.
caddy You have a public domain. Caddy obtains & renews Let's Encrypt certs automatically.

Option A β€” Self-signed certificate (home LAN, quick start)

Best for: home use, local network, no public domain.

1. Generate the certificate

# Usage: ./docker/ssl-gen.sh [IP] [optional-hostname]
./docker/ssl-gen.sh 192.168.1.12
# β†’ creates ./certs/server.crt and ./certs/server.key

For multiple SANs (IP + hostname):

./docker/ssl-gen.sh 192.168.1.12 bookoholik.home

Or use mkcert for a trusted local CA (no browser warning on your devices):

brew install mkcert && mkcert -install
mkdir -p certs
mkcert -key-file ./certs/server.key -cert-file ./certs/server.crt \
    192.168.1.12 localhost 127.0.0.1

2. Create your SSL env file

cp .env.ssl.example .env.ssl
# Edit .env.ssl β€” set SSL_DOMAIN to your LAN IP (e.g. 192.168.1.12)

3. Update the base .env

CORS_ORIGIN=https://192.168.1.12
API_URL=https://192.168.1.12/api
APP_URL=https://192.168.1.12
NGINX_API_URL=https://192.168.1.12

Note: No port is needed in the URL β€” the SSL proxy handles routing on port 443.

4. Start with the SSL overlay

docker compose \
  -f docker-compose.yml \
  -f docker-compose.ssl.yml \
  --env-file .env \
  --env-file .env.ssl \
  --profile nginx-ssl \
  up -d --build

Access the app at https://192.168.1.12.

Your browser will warn about the self-signed certificate β€” click Advanced β†’ Proceed.
To eliminate the warning permanently, trust the certificate (see below).

Trust the certificate

macOS:

sudo security add-trusted-cert -d -r trustRoot \
  -k /Library/Keychains/System.keychain ./certs/server.crt

iOS / iPadOS:

  1. Email ./certs/server.crt to yourself, or temporarily host it:
    python3 -m http.server 9000 --directory certs
    # β†’ http://192.168.1.12:9000/server.crt
  2. Open the URL on the device β†’ Settings β†’ Profile Downloaded β†’ Install
  3. Settings β†’ General β†’ About β†’ Certificate Trust Settings β†’ toggle the cert on

Windows:

  1. Double-click server.crt β†’ Install Certificate
  2. Place in Trusted Root Certification Authorities

Option B β€” mkcert (LAN with trusted local CA)

Follow Option A but use mkcert to generate the certificate. The key advantage: no browser warning on any device where mkcert -install has been run.

brew install mkcert nss && mkcert -install
mkdir -p certs
mkcert -key-file ./certs/server.key -cert-file ./certs/server.crt \
    192.168.1.12 bookoholik.home localhost 127.0.0.1

Then follow steps 2–4 from Option A.

For iOS / Android with mkcert: copy $(mkcert -CAROOT)/rootCA.pem to the device and install it as a trusted profile.


Option C β€” Let's Encrypt, bring-your-own cert (certbot)

Best for: a domain you control, certbot already set up on the host.

1. Obtain a certificate with certbot

# Stop any service using port 80 first, then:
certbot certonly --standalone -d bookoholik.yourdomain.com
# Cert path: /etc/letsencrypt/live/bookoholik.yourdomain.com/

2. Configure .env.ssl

SSL_MODE=nginx-letsencrypt
SSL_DOMAIN=bookoholik.yourdomain.com
SSL_CERT_FILE=/etc/letsencrypt/live/bookoholik.yourdomain.com/fullchain.pem
SSL_KEY_FILE=/etc/letsencrypt/live/bookoholik.yourdomain.com/privkey.pem

3. Update base .env

CORS_ORIGIN=https://bookoholik.yourdomain.com
API_URL=https://bookoholik.yourdomain.com/api
APP_URL=https://bookoholik.yourdomain.com
NGINX_API_URL=https://bookoholik.yourdomain.com

4. Start

docker compose \
  -f docker-compose.yml \
  -f docker-compose.ssl.yml \
  --env-file .env \
  --env-file .env.ssl \
  --profile nginx-ssl \
  up -d --build

Option D β€” Caddy (automatic Let's Encrypt)

Best for: a public domain, zero certificate management.

Prerequisites

  • A real domain pointing to your server's public IP
  • Ports 80 and 443 open on your router/firewall

1. Configure .env.ssl

SSL_MODE=caddy
SSL_DOMAIN=bookoholik.yourdomain.com

2. Update base .env

CORS_ORIGIN=https://bookoholik.yourdomain.com
API_URL=https://bookoholik.yourdomain.com/api
APP_URL=https://bookoholik.yourdomain.com
NGINX_API_URL=https://bookoholik.yourdomain.com

3. Start

docker compose \
  -f docker-compose.yml \
  -f docker-compose.ssl.yml \
  --env-file .env \
  --env-file .env.ssl \
  --profile caddy \
  up -d --build

Caddy automatically obtains and renews the certificate. No manual cert management needed.


Stopping / switching back to plain HTTP

# Stop the SSL stack
docker compose -f docker-compose.yml -f docker-compose.ssl.yml down

# Start plain HTTP again
docker compose up -d

Files added by this feature

File Purpose
docker-compose.ssl.yml SSL overlay β€” extends the base compose file
docker/nginx-ssl.conf nginx reverse proxy config (self-signed / certbot modes)
docker/Caddyfile Caddy config (automatic Let's Encrypt mode)
docker/ssl-gen.sh Helper script to generate a self-signed certificate
.env.ssl.example Template for SSL environment variables
certs/ Where generated certificates are stored (git-ignored)

After enabling HTTPS β€” checklist

  • CORS_ORIGIN updated to https://... in base .env
  • API_URL updated to https://... in base .env
  • APP_URL updated to https://... in base .env
  • NGINX_API_URL updated to https://... in base .env
  • Frontend rebuilt (--build flag in the compose command)
  • Certificate trusted on all devices you use

Verify HTTPS is working

# Check certificate details
curl -kvI https://192.168.1.12 2>&1 | grep -E "subject|issuer|expire|SSL"

# Check HSTS header
curl -sI https://your-address | grep -i "strict-transport"

# Check HTTP β†’ HTTPS redirect
curl -sI http://your-address | grep -i "location"

Troubleshooting

Problem Cause Fix
NET::ERR_CERT_INVALID Self-signed cert not trusted Install cert as trusted root (see Option A)
API calls fail (CORS) CORS_ORIGIN still has http:// Update .env and rebuild frontend
Ebook upload fails (413) Body size limit Already set to 110 MB in nginx-ssl.conf β€” check your proxy chain
iOS: "certificate not trusted" Root CA not installed on device Follow iOS trust instructions in Option A
Let's Encrypt fails Port 80 not reachable Check router port forwarding and firewall
SSL_DOMAIN is required error Forgot to set SSL_DOMAIN in .env.ssl Set SSL_DOMAIN=your-ip-or-domain
Caddy: certificate not issued Domain doesn't resolve to server Ensure DNS A record points to your public IP

Next: Troubleshooting

Clone this wiki locally