Self-Hosting an Email Newsletter with Keila
/ 4 min read
Table of Contents
I want to self-host an email newsletter. This post walks through setting up Keila on a home server and exposing it to the internet with a Cloudflare Tunnel. These instructions aim to be (1) easy to follow and (2) reasonably secure.
Prerequisites
- A Linux machine with Docker and Docker Compose installed
- A domain name managed by Cloudflare
- An SMTP provider for sending emails (e.g. Mailgun, Amazon SES, Postmark)
1. Create a Cloudflare Tunnel
Before setting up Keila, create a tunnel in Cloudflare’s dashboard:
- Go to Cloudflare One
- Navigate to Networks → Connectors → Create a tunnel
- Choose Cloudflared as the connector type
- Name your tunnel (e.g.
keila) - Copy the tunnel token — you’ll need it for the
.envfile below - Add a Public Hostname:
- Subdomain:
newsletter(or whatever you prefer) - Domain: your domain
- Service Type:
HTTP - URL:
keila:4000
- Subdomain:
2. Secure with Cloudflare Access
Keila’s admin interface should not be open to the public internet without an additional layer of authentication. Cloudflare Access adds authentication before traffic reaches your server.
- In the Cloudflare One Dashboard, go to Access controls → Applications → Add an application → Self-hosted
- Set the Application domain to your Keila subdomain
- Create a policy that allows only your email address (e.g. Email is
you@example.com) - Under Path, enter
/auth*(this protects the login page so only you can authenticate with Keila)
Add a second public hostname, setting Path to /api* to protect the API as well.
Public-facing paths like /unsubscribe/*, /campaigns/*, and /forms/* should remain unprotected so newsletter links and signup forms continue to work.
3. Create docker-compose.yml
Create a project directory and move into it:
mkdir keila && cd keilaCreate a .env file with your secrets:
# Generate a secret keyecho "SECRET_KEY_BASE=$(head -c 48 /dev/urandom | base64)" >> .env
# Add your other secretsecho "POSTGRES_PASSWORD=$(head -c 24 /dev/urandom | base64)" >> .envecho "TUNNEL_TOKEN=your-cloudflare-tunnel-token" >> .envecho "URL_HOST=newsletter.yourdomain.com" >> .env
# User credentials to login to Keilaecho "KEILA_USER=you@example.com" >> .envecho "KEILA_PASSWORD=$(head -c 24 /dev/urandom | base64)" >> .envThen create docker-compose.yml:
services: keila: image: pentacent/keila:latest depends_on: postgres: condition: service_healthy environment: DB_URL: "postgres://keila:${POSTGRES_PASSWORD}@postgres/keila" SECRET_KEY_BASE: "${SECRET_KEY_BASE}" URL_HOST: "${URL_HOST}" URL_SCHEMA: "https" PORT: "4000" DISABLE_REGISTRATION: "true" KEILA_USER: "${KEILA_USER}" KEILA_PASSWORD: "${KEILA_PASSWORD}" volumes: - keila_uploads:/opt/app/uploads restart: unless-stopped
postgres: image: postgres:16-alpine environment: POSTGRES_USER: keila POSTGRES_PASSWORD: "${POSTGRES_PASSWORD}" POSTGRES_DB: keila volumes: - postgres_data:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U keila"] interval: 5s timeout: 5s retries: 5 restart: unless-stopped
cloudflared: image: cloudflare/cloudflared:latest command: tunnel --no-autoupdate run --token ${TUNNEL_TOKEN} depends_on: - keila restart: unless-stopped
volumes: postgres_data: keila_uploads:A few things to note:
- No ports are exposed. Traffic flows through the Cloudflare Tunnel, so there’s no need to publish ports on the host machine. This is more secure than binding to
0.0.0.0. DISABLE_REGISTRATIONis set totrue. The root user is created on first launch, and you don’t want strangers registering accounts on your instance.URL_SCHEMAis set tohttpsbecause Cloudflare handles TLS termination.- Secrets live in
.env, not in the compose file. Add.envto your.gitignore.
4. Start Keila
docker compose up -dOn first launch, Keila creates a root user using the KEILA_USER and KEILA_PASSWORD values from your .env file. To see your password:
grep KEILA_PASSWORD .env5. Log In and Configure a Sender
- Open
https://newsletter.yourdomain.comin your browser - Log in with the
KEILA_USERandKEILA_PASSWORDcredentials from above - Create a new project
- Inside your new project, go to Senders → Create
- Configure your SMTP sender (Mailgun, SES, Postmark, etc.)
- Send a test email to verify it works (I had to create a new campaign and send a preview email)
6. Keep It Updated
I like using dockcheck to periodically pull the latest images. I should probably set up a calendar reminder to do this monthly, but I haven’t yet.
References
- Keila Homepage
- Keila Source Code
- Keila Configuration Docs
- Cloudflare Tunnel Docs
- Cloudflare Access Docs