Get Started
← All Posts

Self-Hosting the MarkReply Core Engine

Deploy your own blazing-fast, SQLite-backed comment engine in minutes using Docker Compose.

Self-Hosting the MarkReply Core Engine

MarkReply is a blazing-fast, self-hosted comment engine built in Rust. It provides a universal JavaScript widget and a fully automated SQLite backend, completely eliminating the need for heavy external dependencies.

If you prefer managing your own infrastructure over using a managed cloud, you can spin up the core engine on any VPS in minutes.

✨ The Core Features

  • Zero-Touch Database: Fully automated SQLite setup with build-time and run-time migrations.
  • Threaded Conversations: Built-in support for nested replies to keep discussions organized.
  • Spam Protection: Invisible honeypot fields silently trap basic bot submissions.
  • Admin Auto-Approval: The site owner’s comments bypass moderation and are instantly published.

πŸ”’ Customizing Caddy for SSL

To serve your widget securely, we use Caddy as a reverse proxy. While the standard Caddy image works great, adding a DNS module (like Cloudflare) allows you to automate Let’s Encrypt ACME challenges without exposing port 80, which is highly recommended for production environments.

First, create a Dockerfile.caddy to build a custom Caddy binary with the Cloudflare DNS module:

Note: We are using Cloudflare in this example, but you should swap out the GitHub URL for the specific plugin that matches your domain's DNS provider (e.g., Route53, DigitalOcean). You can find the full list of available modules on the Caddy DNS GitHub page.

Dockerfile.caddy
FROM caddy:2-builder AS builder
RUN xcaddy build \
--with github.com/caddy-dns/cloudflare
FROM caddy:2
COPY --from=builder /usr/bin/caddy /usr/bin/caddy

Next, define your routing and SSL configuration in a Caddyfile:

A quick note on syntax highlighting: Our blog engine's syntax highlighter doesn't natively support Caddyfiles yet. We are using Nginx highlighting here as a fallback since the structure is similar enough to keep the code easily readable!

Caddyfile
{
email user@email.com
cert_issuer acme
}
comments.example.com {
encode {
zstd
gzip 6
}
# Proxy requests to the MarkReply container
reverse_proxy markreply-engine:3000
}

πŸš€ Docker Compose Deployment

MarkReply is designed to be entirely self-contained. The SQLite schema is automatically generated on startup.

Create a docker-compose.yml file in the same directory as your Caddyfile and Dockerfile.caddy:

To receive moderation emails and enable admin auto-approval, simply provide your SMTP credentials and admin email in the environment variables.

services:
caddy:
build:
context: .
dockerfile: Dockerfile.caddy
container_name: markreply-proxy
ports:
- "80:80"
- "443:443"
environment:
# Inject your Cloudflare API token for the DNS challenge
- CLOUDFLARE_API_TOKEN=your_cloudflare_token
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./caddy_data:/data
- ./caddy_config:/config
restart: unless-stopped
depends_on:
- markreply-engine
markreply-engine:
image: ghcr.io/rajasekhar-gundala/markreply:latest
container_name: markreply-engine
environment:
- DATABASE_URL=sqlite://data/comments.db?mode=rwc
- ADMIN_TOKEN=your-super-secret-admin-key
# Admin email for auto-approvals and moderation notifications
- ADMIN_EMAIL=user@example.com
# SMTP Configuration for outgoing emails
- SMTP_HOST=smtp.yourprovider.com
- SMTP_PORT=465
- SMTP_USER=your-smtp-username
- SMTP_PASS=your-smtp-password
volumes:
- ./data:/app/data
restart: unless-stopped

Start the engine by running docker compose up -d. The ./data volume ensures your SQLite database persists safely across container restarts.

πŸ’» Frontend Integration

Once your backend is running, adding comments to your site is as simple as injecting the widget. Place this snippet exactly where you want the discussion thread to appear:

Replace comments.example.com with your real domain name.

<!-- The data-slug attribute groups comments for specific pages or blog posts -->
<div id="markreply-comments"></div>
<script src="https://comments.example.com/widget.js" async></script>