skip to content
Andrew Marder

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

1. Create a Cloudflare Tunnel

Before setting up Keila, create a tunnel in Cloudflare’s dashboard:

  1. Go to Cloudflare One
  2. Navigate to NetworksConnectorsCreate a tunnel
  3. Choose Cloudflared as the connector type
  4. Name your tunnel (e.g. keila)
  5. Copy the tunnel token — you’ll need it for the .env file below
  6. Add a Public Hostname:
    • Subdomain: newsletter (or whatever you prefer)
    • Domain: your domain
    • Service Type: HTTP
    • URL: keila:4000

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.

  1. In the Cloudflare One Dashboard, go to Access controlsApplicationsAdd an applicationSelf-hosted
  2. Set the Application domain to your Keila subdomain
  3. Create a policy that allows only your email address (e.g. Email is you@example.com)
  4. 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:

Terminal window
mkdir keila && cd keila

Create a .env file with your secrets:

Terminal window
# Generate a secret key
echo "SECRET_KEY_BASE=$(head -c 48 /dev/urandom | base64)" >> .env
# Add your other secrets
echo "POSTGRES_PASSWORD=$(head -c 24 /dev/urandom | base64)" >> .env
echo "TUNNEL_TOKEN=your-cloudflare-tunnel-token" >> .env
echo "URL_HOST=newsletter.yourdomain.com" >> .env
# User credentials to login to Keila
echo "KEILA_USER=you@example.com" >> .env
echo "KEILA_PASSWORD=$(head -c 24 /dev/urandom | base64)" >> .env

Then 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_REGISTRATION is set to true. The root user is created on first launch, and you don’t want strangers registering accounts on your instance.
  • URL_SCHEMA is set to https because Cloudflare handles TLS termination.
  • Secrets live in .env, not in the compose file. Add .env to your .gitignore.

4. Start Keila

Terminal window
docker compose up -d

On first launch, Keila creates a root user using the KEILA_USER and KEILA_PASSWORD values from your .env file. To see your password:

Terminal window
grep KEILA_PASSWORD .env

5. Log In and Configure a Sender

  1. Open https://newsletter.yourdomain.com in your browser
  2. Log in with the KEILA_USER and KEILA_PASSWORD credentials from above
  3. Create a new project
  4. Inside your new project, go to SendersCreate
  5. Configure your SMTP sender (Mailgun, SES, Postmark, etc.)
  6. 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