Deploying Your Own IndieWeb Site with Indiekit + Eleventy (Docker Compose based)
Content changed
Changes
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
Prerequisites
Server Setup
Clone and Configure
Write Your.env File
Launch the Stack
Create Your Admin Password
Log In and Explore
Write Your First Post
Set Up Syndication
Set Up Webmentions
Enable the Full Plugin Set
Backup and Restore
Updating
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:
Hash your password using bcrypt
Display the hash on screen — it looks like: $2b$10$Eujjehrmx.K.n92T3SFLJe/mN5ZQ4gHIvP.Y8rdBmqko9SLHG7K2u
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
Go to https://janedoe.me/create
Select Note as the post type
Write something: Hello from my new IndieWeb site! This is my first post, published via Micropub. #indieweb
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:
Point it at https://janedoe.me
The client discovers your Micropub endpoint automatically (via <link rel="micropub"> )
Authenticate with your domain (IndieAuth)
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
Go to your Mastodon instance (e.g., https://mastodon.social )
Navigate to Settings > Development > New Application
Give it a name like “Indiekit”
Grant scopes: read , write
Save and copy the Access Token
Add to your .env :
MASTODON_INSTANCE=https://mastodon.social
MASTODON_USER=@janedoe
MASTODON_ACCESS_TOKEN=Ba91_xYzAbCdEfGhIjKlMnOpQrStUv0123456789abc
Bluesky
Go to bsky.app
Navigate to Settings > App Passwords > Add App Password
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
You create a post at /create with syndication targets checked (Mastodon, Bluesky, etc.)
The post is saved to disk as Markdown
The cron sidecar POSTs to /syndicate every 2 minutes
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 :
Sign up at webmention.io using your domain
It discovers your site’s rel="me" links for authentication
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=AIzaSyB1c2D3e4F5g6H7i8J9k0L1m2N3o4P5q6Ryour-youtube-api-key-here
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:
Create an app at the LinkedIn Developer Portal
Set the redirect URI to https://janedoe.me/linkedin/callback
Add LINKEDIN_CLIENT_ID and LINKEDIN_CLIENT_SECRET to .env
Restart Indiekit
Visit https://janedoe.me/linkedin and authorize
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:
Check Eleventy logs: docker compose logs eleventy
Look for [11ty] File changed messages
The watcher auto-restarts with exponential backoff if it crashes
Syndication not working
Check cron logs: docker compose logs cron
Verify your syndicator env vars are set correctly in .env
Syndication runs every 2 minutes — check the timestamp in logs
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
Migrating from another Static Site Generator
Already running Hugo, Jekyll, or micro.blog? indiekit-deploy now ships with a content migration tool that moves your existing posts, media, and old URLs into your new Indiekit deployment without losing them.
The tool runs as a profile-gated Docker service — no host-side Node or Python required:
cp -r ~/old-blog/* migration/input/
make migrate-detect # auto-detects the SSG layout
make migrate-convert FROM=hugo # transform → migration/staged/
make migrate-preview # diff against live volumes
make migrate-apply # copy into content + uploads volumes
docker compose restart caddy # activate URL redirects
What gets migrated:
Markdown content — articles, notes, photos, bookmarks, replies, page bundles
Frontmatter — titles, dates, tags, response properties (like-of , bookmark-of , etc.)
Media files — images, video, audio, copied with their original web paths preserved so unchanged markdown still resolves
301 redirects — every old URL gets a redirect to its canonical Indiekit URL, generated as a Caddy snippet that’s auto-imported
The classifier auto-maps content into Indiekit’s post types (titled posts → article, untitled short bodies → note, frontmatter with bookmark-of: → bookmark, etc.). For edge cases, drop a migration/input/_classify.yaml file with glob-pattern overrides.
For Hugo specifically, there’s a step-by-step guide covering page bundles, custom permalinks, shortcodes, and troubleshooting:
Migrating from Hugo to Indiekit — full walkthrough with edge cases and verification checklist
Migration tool overview — supported sources, classification rules, override syntax
Jekyll and micro.blog adapters ship in v1 (untested with real exports yet); Ghost and WordPress (WXR) are on the roadmap .
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.
Editorial assistance — article drafted by human, AI helped with structure and clarity
Learn more about AI usage on this site
NewsDiff