Deploying Your Own IndieWeb Site with Indiekit + Eleventy (Docker Compose based)

15 min read Original article ↗

A complete guide to deploying [Indiekit](https://getindiekit.com) on your own server using Docker Compose.

A complete guide to deploying Indiekit on your own server using Docker Compose. By the end of this guide, you’ll have a fully functional IndieWeb blog with automatic HTTPS, Micropub support, syndication to Mastodon and Bluesky, and a static Eleventy-powered frontend — all running on a domain you own.

What You’ll Get

  • A static blog generated by Eleventy with automatic rebuilds when you publish
  • A Micropub server so you can post from any Micropub client
  • Automatic HTTPS via Caddy and Let’s Encrypt
  • POSSE syndication to Mastodon, Bluesky, and LinkedIn
  • Webmention support for receiving likes, replies, and reposts
  • Background jobs that handle syndication and webmention sending
  • MongoDB for data storage, all running in Docker containers

Table of Contents

  1. Prerequisites
  2. Server Setup
  3. Clone and Configure
  4. Write Your .env File
  5. Launch the Stack
  6. Create Your Admin Password
  7. Log In and Explore
  8. Write Your First Post
  9. Set Up Syndication
  10. Set Up Webmentions
  11. Enable the Full Plugin Set
  12. Backup and Restore
  13. Updating
  14. Troubleshooting

1. Prerequisites

You need:

  • A server — any VPS with at least 1 GB RAM (2 GB recommended). DigitalOcean, Hetzner, Linode, or any provider works.
  • A domain name — e.g., janedoe.me. You’ll need access to its DNS settings.
  • Docker and Docker Compose v2 — installed on the server.
  • Ports 80 and 443 open — Caddy needs these for automatic HTTPS.

Install Docker (if needed)

On Ubuntu/Debian:

curl -fsSL https://get.docker.com | sh
sudo usermod -aG docker $USER
# Log out and back in for group change to take effect

Verify:

docker --version
# Docker version 27.x.x
docker compose version
# Docker Compose version v2.x.x

Point Your Domain

Create a DNS A record pointing your domain to your server’s IP address:

Type: A
Name: @  (or janedoe.me)
Value: 203.0.113.42  (your server IP)
TTL: 300

If you also want www:

Type: CNAME
Name: www
Value: janedoe.me
TTL: 300

Wait for DNS to propagate (usually a few minutes, sometimes up to an hour). You can check with:

dig janedoe.me +short
# Should return: 203.0.113.42

2. Server Setup

SSH into your server:

ssh user@203.0.113.42

Make sure ports 80 and 443 are open:

# If using ufw
sudo ufw allow 80/tcp
sudo ufw allow 443/tcp
sudo ufw allow 443/udp   # HTTP/3
sudo ufw status

3. Clone and Configure

git clone https://github.com/rmdes/indiekit-deploy.git
cd indiekit-deploy

Initialize the Eleventy theme submodule:

make init

This pulls the Eleventy theme that powers your site’s frontend.


4. Write Your .env File

Copy the example and open it in your editor:

cp .env.example .env
nano .env       # or vim, or whatever you prefer

Here’s what a realistic .env looks like for a new deployment. We’ll start with the essentials and add syndication later:

# =============================================================================
# Domain & Site
# =============================================================================

DOMAIN=janedoe.me
SITE_URL=https://janedoe.me
SITE_NAME=Jane Doe
SITE_DESCRIPTION=Thinking out loud on the open web.
SITE_LOCALE=en
SITE_TIMEZONE=Europe/Paris
SITE_CATEGORIES=blog,tech,indieweb,personal,links

# =============================================================================
# Author
# =============================================================================

AUTHOR_NAME=Jane Doe
AUTHOR_BIO=Web developer and IndieWeb enthusiast. I write about building things for the open web.
AUTHOR_AVATAR=/images/default-avatar.svg
AUTHOR_LOCATION=Paris, France
AUTHOR_EMAIL=jane@janedoe.me

# =============================================================================
# Social Links (two options — pick one)
# =============================================================================

# Option A: Set individual handles (auto-generates social links)
GITHUB_USERNAME=janedoe
BLUESKY_HANDLE=janedoe.bsky.social
MASTODON_INSTANCE=https://mastodon.social
MASTODON_USER=@janedoe
LINKEDIN_USERNAME=janedoe

# Option B: Set SITE_SOCIAL directly for full control (overrides Option A)
# Format: "Name|URL|icon,Name|URL|icon"
# SITE_SOCIAL=GitHub|https://github.com/janedoe|github,Bluesky|https://bsky.app/profile/janedoe.bsky.social|bluesky,Mastodon|https://mastodon.social/@janedoe|mastodon,LinkedIn|https://www.linkedin.com/in/janedoe/|linkedin

# =============================================================================
# Syndication — leave blank for now, we'll set these up later
# =============================================================================

MASTODON_ACCESS_TOKEN=
BLUESKY_PASSWORD=
LINKEDIN_ACCESS_TOKEN=

# =============================================================================
# Webmentions — leave blank for now
# =============================================================================

WEBMENTION_IO_TOKEN=

# =============================================================================
# Authentication — leave PASSWORD_SECRET blank for first run
# =============================================================================

PASSWORD_SECRET=

Save and close.

Key things to get right:

  • DOMAIN must exactly match your DNS A record (no https://, no trailing slash)
  • SITE_URL must be https:// + your domain
  • SITE_TIMEZONE uses IANA timezone names like America/New_York, Europe/London, Asia/Tokyo
  • Leave PASSWORD_SECRET empty — you’ll fill it in after the first launch

5. Launch the Stack

For the core plugin set (recommended to start):

make up

Or equivalently:

docker compose up -d

This starts 5 containers:

Container What it does
mongodb Stores your posts, settings, and syndication state
indiekit The Indiekit server — handles Micropub, auth, admin UI
eleventy Watches for new posts and rebuilds your static site
caddy HTTPS reverse proxy — serves your site to the world
cron Runs syndication every 2 minutes and webmentions every 5 minutes

Watch the logs to make sure everything starts:

make logs

You should see:

caddy-1    | ... certificate obtained successfully
indiekit-1 | ==> Indiekit entrypoint (profile: core)
indiekit-1 | ==> Generating JWT secret
indiekit-1 | ==> Starting Indiekit on port 8080
eleventy-1 | ==> Waiting for Indiekit at http://indiekit:8080...
eleventy-1 | ==> Indiekit is ready
eleventy-1 | ==> Building Eleventy site
eleventy-1 | [11ty] Wrote 3 files in 0.42 seconds
eleventy-1 | [eleventy-watcher] Starting watcher

After about 30–60 seconds, your site should be live at https://janedoe.me. You’ll see a minimal blog with no posts yet.

If you see TLS errors: your DNS might not have propagated yet. Wait a few minutes and check dig janedoe.me. Caddy retries automatically.


6. Create Your Admin Password

This is the critical first-run step. You must do this before you can log in or publish anything.

Step 1: Visit the Login Page

Open https://janedoe.me/session/login in your browser.

Since no password is set yet, Indiekit shows a “New password” page instead of a login form.

Step 2: Create Your Password

Enter a strong password and submit. Indiekit will:

  1. Hash your password using bcrypt
  2. Display the hash on screen — it looks like: $2b$10$Eujjehrmx.K.n92T3SFLJe/mN5ZQ4gHIvP.Y8rdBmqko9SLHG7K2u
  3. Tell you to save this as PASSWORD_SECRET

Copy the full hash. You need every character.

Step 3: Save the Hash in Your .env

Open your .env file and find the PASSWORD_SECRET= line. Paste the hash, but escape every $ as $$:

# What Indiekit displayed:
# $2b$10$Eujjehrmx.K.n92T3SFLJe/mN5ZQ4gHIvP.Y8rdBmqko9SLHG7K2u

# What you put in .env (every $ doubled to $$):
PASSWORD_SECRET=$$2b$$10$$Eujjehrmx.K.n92T3SFLJe/mN5ZQ4gHIvP.Y8rdBmqko9SLHG7K2u

Why the double dollars? Docker Compose uses $ for variable substitution. A bare $10 in .env would be interpreted as an environment variable reference (and resolve to empty). Doubling them ($$) tells Docker Compose to treat them as literal $ characters.

Step 4: Restart Indiekit

docker compose restart indiekit

Important: use restart, not down && up. A full down + up would also restart MongoDB and the other containers unnecessarily.

Step 5: Verify

Go to https://janedoe.me/session/login again. You should now see a normal login form. Enter your password. If it works, you’re in.

If login fails: double-check that every $ in the hash is doubled in .env. A common mistake is missing the $$ before 10 (the bcrypt cost factor). The hash has exactly 5 $ characters, so your .env value should have exactly 5 $$ pairs.


7. Log In and Explore

After logging in, you’ll see the Indiekit dashboard. From here you can:

  • Create posts at /create — write notes, articles, bookmarks, etc.
  • Manage posts at /posts — see all your published content
  • View files at /files — browse the raw Markdown files on disk
  • Check plugins at /plugins — see what’s loaded

The admin UI is served by Indiekit at https://janedoe.me/session/login. Your public blog is the Eleventy-generated static site at https://janedoe.me/.


8. Write Your First Post

From the Admin UI

  1. Go to https://janedoe.me/create
  2. Select Note as the post type
  3. Write something: Hello from my new IndieWeb site! This is my first post, published via Micropub. #indieweb
  4. Click Publish

Within a few seconds, Eleventy detects the new Markdown file, rebuilds the site, and your post appears at https://janedoe.me/.

From a Micropub Client

You can also post from any Micropub client:

  • Quill — web-based, great for quick notes
  • Indigenous — iOS/Android app
  • iA Writer — macOS/iOS, supports Micropub publishing

To connect a Micropub client:

  1. Point it at https://janedoe.me
  2. The client discovers your Micropub endpoint automatically (via <link rel="micropub">)
  3. Authenticate with your domain (IndieAuth)
  4. Start posting

9. Set Up Syndication

Syndication (POSSE — Publish on your Own Site, Syndicate Elsewhere) lets you cross-post to social networks automatically. Posts are created on your site first, then syndicated to Mastodon, Bluesky, or LinkedIn.

Mastodon

  1. Go to your Mastodon instance (e.g., https://mastodon.social)
  2. Navigate to Settings > Development > New Application
  3. Give it a name like “Indiekit”
  4. Grant scopes: read, write
  5. Save and copy the Access Token

Add to your .env:

MASTODON_INSTANCE=https://mastodon.social
MASTODON_USER=@janedoe
MASTODON_ACCESS_TOKEN=Ba91_xYzAbCdEfGhIjKlMnOpQrStUv0123456789abc

Bluesky

  1. Go to bsky.app
  2. Navigate to Settings > App Passwords > Add App Password
  3. Name it “Indiekit” and copy the generated password

Add to your .env:

BLUESKY_HANDLE=janedoe.bsky.social
BLUESKY_PASSWORD=abcd-1234-efgh-5678

Apply Changes

After updating .env, restart Indiekit to pick up the new environment variables:

docker compose restart indiekit

The cron sidecar automatically runs syndication every 2 minutes. When you create a new post with syndication targets checked, it will be queued and syndicated on the next cron run.

How Syndication Works

  1. You create a post at /create with syndication targets checked (Mastodon, Bluesky, etc.)
  2. The post is saved to disk as Markdown
  3. The cron sidecar POSTs to /syndicate every 2 minutes
  4. Indiekit picks up unsyndicated posts, posts them to each target, and saves the syndication URLs back to the post

You can check syndication status at /posts — each post shows which targets it was syndicated to.


10. Set Up Webmentions

Webmentions are how IndieWeb sites notify each other about links, likes, replies, and reposts.

Sending Webmentions

The cron sidecar automatically sends webmentions for your posts every 5 minutes. When you publish a post that links to another site, Indiekit sends a webmention to notify them. No configuration needed — this works out of the box.

Receiving Webmentions

To receive webmentions (likes, replies, reposts from other sites), use webmention.io:

  1. Sign up at webmention.io using your domain
  2. It discovers your site’s rel="me" links for authentication
  3. After signing in, go to Settings and copy your API Token

Add to your .env:

WEBMENTION_IO_TOKEN=your-webmention-io-api-token-here

Restart:

docker compose restart indiekit

The Eleventy theme automatically displays webmentions (likes, replies, reposts) on each post page.


11. Enable the Full Plugin Set

The core profile gives you a functional IndieWeb blog. The full profile adds extra integrations:

Plugin What it does
GitHub Shows your GitHub activity, stars, and contributions
Funkwhale Displays listening history from a Funkwhale instance
Last.fm Shows scrobbles, loved tracks, and listening stats
YouTube Displays your YouTube channel activity
RSS Aggregates and caches external RSS feeds
Microsub Social reader with feed subscriptions
Podroll Aggregates podcast episodes from FreshRSS
Blogroll Manages a blogroll with OPML/Microsub import
Homepage Configurable homepage sections
CV Manage and display a resume/CV

To switch to the full profile, stop the stack and start with the full override:

docker compose down
docker compose -f docker-compose.yml -f docker-compose.full.yml up -d

Or use the Makefile shortcut:

make down
make up-full

Important: the first time you switch to full profile, the Indiekit container is rebuilt with all plugins installed. This takes a few minutes.

Configure Full Profile Plugins

Add the relevant environment variables to your .env. Each plugin only activates when its required env vars are set — you only need to fill in the ones you want:

# ─── GitHub ───
# Shows commits, stars, contributions on /github
# Get a token at https://github.com/settings/tokens (scopes: read:user, repo)
GITHUB_TOKEN=ghp_abc123def456ghi789jkl012mno345pqr678
GITHUB_FEATURED_REPOS=janedoe/my-cool-project,janedoe/another-repo

# ─── Last.fm ───
# Shows scrobbles, loved tracks, top artists on /listening
# Get an API key at https://www.last.fm/api/account/create
LASTFM_API_KEY=a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4
LASTFM_USERNAME=janedoe

# ─── YouTube ───
# Shows latest videos, live streaming status on /youtube
# Get an API key from Google Cloud Console > APIs & Services > Credentials
YOUTUBE_API_KEY=AIzaSyB1c2D3e4F5g6H7i8J9k0L1m2N3o4P5q6R
YOUTUBE_CHANNELS=@janedoe-tech

# ─── Funkwhale ───
# Shows listening history from a Funkwhale instance on /funkwhale
FUNKWHALE_INSTANCE=https://funkwhale.example.com
FUNKWHALE_USERNAME=janedoe
FUNKWHALE_TOKEN=your-funkwhale-api-token

# ─── Podroll ───
# Aggregates podcast episodes from a FreshRSS instance
# These URLs come from your FreshRSS API (greader format for episodes, OPML for sidebar)
PODROLL_EPISODES_URL=https://rss.example.com/api/query.php?user=jane&t=yourtoken&f=greader
PODROLL_OPML_URL=https://rss.example.com/api/query.php?user=jane&t=yourtoken&f=opml

# ─── LinkedIn Syndication ───
# Option 1 (recommended): Use the OAuth flow — visit /linkedin after launching
# Set these to enable the OAuth endpoint:
LINKEDIN_CLIENT_ID=77abc123def456
LINKEDIN_CLIENT_SECRET=WPLsecret123abc

# Option 2: Set access token manually (expires after ~60 days)
# LINKEDIN_ACCESS_TOKEN=AQV...long-token...
# LINKEDIN_AUTHOR_NAME=Jane Doe
# LINKEDIN_PROFILE_URL=https://www.linkedin.com/in/janedoe/

After updating .env, rebuild the full profile:

make build-full
make up-full

LinkedIn OAuth Flow

If you set LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET, you can use the built-in OAuth flow instead of managing tokens manually:

  1. Create an app at the LinkedIn Developer Portal
  2. Set the redirect URI to https://janedoe.me/linkedin/callback
  3. Add LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET to .env
  4. Restart Indiekit
  5. Visit https://janedoe.me/linkedin and authorize
  6. The token is saved automatically and refreshed when needed

12. Backup and Restore

Backup

make backup

This creates a compressed archive in backups/ containing:

  • content/ — all your Markdown posts and media
  • uploads/ — uploaded files
  • mongodb/ — the full database
  • config/ — your Indiekit config and JWT secret
backups/indiekit-20260214-120000.tar.gz

Automate it with a cron job on the host:

crontab -e
# Add:
0 3 * * * cd /home/user/indiekit-deploy && make backup

Restore

make restore FILE=backups/indiekit-20260214-120000.tar.gz

This stops all services, restores the volumes from the archive, and restarts everything.


13. Updating

When a new version of Indiekit or the theme is released:

# Pull latest code
cd ~/indiekit-deploy
git pull

# Update the Eleventy theme
make update-theme

# Rebuild images with new code
make build        # core profile
# or
make build-full   # full profile

# Restart with new images
make up           # core
# or
make up-full      # full

Using Pre-built Images

If you prefer not to build locally, pre-built images are published to Docker Hub:

docker compose pull    # Pulls latest images from Docker Hub
docker compose up -d   # Starts with the pulled images

Available images:

Image Description
rmdes/indiekit-deploy-server Indiekit server (full plugin set)
rmdes/indiekit-deploy-site Eleventy static site builder
rmdes/indiekit-deploy-cron Cron sidecar

To pin a specific version (e.g., 1.0.0-beta.25), create a docker-compose.override.yml:

services:
  indiekit:
    image: rmdes/indiekit-deploy-server:1.0.0-beta.25
  eleventy:
    image: rmdes/indiekit-deploy-site:1.0.0-beta.25
  cron:
    image: rmdes/indiekit-deploy-cron:1.0.0-beta.25

14. Troubleshooting

“Blog coming soon” or “Building site…”

Eleventy is still building (or the build failed). Check the logs:

docker compose logs eleventy

A successful build looks like:

[11ty] Wrote 15 files in 1.23 seconds
[eleventy-watcher] Starting watcher

If it failed, look for Nunjucks template errors or missing files.

Can’t log in after setting PASSWORD_SECRET

The most common cause is incorrect $ escaping. Check your .env:

# WRONG — Docker Compose interprets $10 as a variable
PASSWORD_SECRET=$2b$10$Eujjehrmx...

# CORRECT — every $ is doubled
PASSWORD_SECRET=$$2b$$10$$Eujjehrmx...

After fixing, you must fully recreate the container (not just restart):

docker compose down indiekit
docker compose up -d indiekit

A docker compose restart does not re-read .env changes. You need a full down/up cycle.

Caddy TLS errors

  • Verify DNS resolves: dig janedoe.me +short should return your server IP
  • Verify ports are open: sudo ufw status should show 80 and 443 allowed
  • Check Caddy logs: docker compose logs caddy
  • Caddy retries certificate provisioning automatically. If DNS just propagated, wait a minute.

Posts don’t appear on the site

Eleventy watches the content directory and rebuilds automatically. If a post doesn’t appear:

  1. Check Eleventy logs: docker compose logs eleventy
  2. Look for [11ty] File changed messages
  3. The watcher auto-restarts with exponential backoff if it crashes

Syndication not working

  1. Check cron logs: docker compose logs cron
  2. Verify your syndicator env vars are set correctly in .env
  3. Syndication runs every 2 minutes — check the timestamp in logs
  4. Make sure the JWT secret exists: docker compose exec cron cat /data/config/.secret

MongoDB won’t start

docker compose logs mongodb
docker compose ps mongodb

Common cause: disk full. Check with df -h.

Environment variable changes don’t take effect

docker compose restart does not re-read .env. You must:

docker compose down indiekit    # Stop and remove the container
docker compose up -d indiekit   # Recreate with new env

View all running services

make status
# or
docker compose ps

Shell into a container

make shell-indiekit    # Indiekit container
make shell-eleventy    # Eleventy container
make shell-cron        # Cron container
make shell-caddy       # Caddy container

Quick Reference

Useful Commands

make up              # Start (core profile)
make up-full         # Start (full profile)
make down            # Stop everything
make logs            # Follow all logs
make restart         # Restart all services
make status          # Show running services
make build           # Rebuild images (core)
make build-full      # Rebuild images (full)
make backup          # Backup all data
make update-theme    # Pull latest theme

Important URLs

URL Purpose
https://janedoe.me/ Your public blog
https://janedoe.me/session/login Log in to admin
https://janedoe.me/create Create a new post
https://janedoe.me/posts Manage posts
https://janedoe.me/files Browse content files
https://janedoe.me/plugins View loaded plugins
https://janedoe.me/feed.json JSON Feed
https://janedoe.me/micropub Micropub endpoint

Architecture

Internet → Caddy :443 (auto HTTPS)
             ├── Static site (Eleventy) → /data/site
             └── API endpoints → Indiekit :8080
                                   └── MongoDB :27017
           Cron sidecar → syndicate every 2m, webmentions every 5m

Data Volumes

All your data lives in Docker volumes that persist across restarts and upgrades:

Volume Contents
content Your posts (Markdown files)
uploads Uploaded media
site Built static HTML (regenerated automatically)
mongodb_data Database
indiekit_config Config file + JWT secret
caddy_data TLS certificates

What’s Next?

  • Customize your theme — the Eleventy theme supports dark mode, RSS feeds, sitemaps, and social embeds out of the box. Fork the theme repo to customize.
  • Add rel=“me” links — verify your social profiles by adding rel="me" links. The theme generates these from your GITHUB_USERNAME, MASTODON_INSTANCE, and BLUESKY_HANDLE env vars.
  • Connect Bridgy — use brid.gy to backfeed responses from Mastodon and Bluesky as webmentions.
  • Join the IndieWeb — add yourself to the IndieWeb wiki, join the chat, and attend an Homebrew Website Club meetup.