This guide covers everything you need to deploy Wikantik using Docker Compose, from initial setup through production operations and disaster recovery.
The containerized deployment runs three services orchestrated by Docker Compose:
| Service | Image | Purpose |
|---|---|---|
| db | postgres:17-alpine | Stores users, groups, and roles |
| wikantik | Custom (Tomcat 11 / JDK 25) | Runs the wiki application |
| backup | postgres:17-alpine | Automated database dumps and page file archives (production only) |
All three services share a private Docker network. The wikantik container connects to db by hostname. The backup container connects to db for pg_dump and mounts the same pages volume as wikantik to archive page files. Only port 8080 is exposed to the host.
| Data | Container path | Docker volume | Can rebuild? | Backed up? |
|---|---|---|---|---|
| Wiki pages (.md + .properties) | /var/wikantik/pages | wikantik-pages | NO | YES |
| File attachments | /var/wikantik/pages | wikantik-pages | NO | YES |
| PostgreSQL data | /var/lib/postgresql/data | pgdata | NO | YES |
| Lucene search index | /var/wikantik/work | wikantik-work | YES (auto-rebuilds on startup) | No |
| Reference manager cache | /var/wikantik/work | wikantik-work | YES (auto-rebuilds on startup) | No |
| Application logs | /var/wikantik/logs | wikantik-logs | N/A | No |
The three items that cannot be rebuilt (pages, attachments, database) are all covered by the automated backup system.
docker/
config/
server.xml Tomcat HTTP connector, Cloudflare RemoteIpValve
catalina.properties Tomcat classpath and security settings
log4j2-docker.xml Console + rolling file logging for Docker
db/
001-init.sql Creates tables and seeds admin user (first startup only)
backup/
backup.sh Runs pg_dump + tar with tiered retention
restore.sh Guided restore with checksum verification
crontab Schedules daily/weekly/monthly backups
entrypoint.sh Generates config files from environment variables
docker-compose.yml Base services: db + wikantik
docker-compose.dev.yml Dev overrides: bind mounts, debug port, no backup
docker-compose.prod.yml Prod overrides: resource limits, backup service
Dockerfile Production: multi-stage Maven build inside container
Dockerfile.dev Dev: Tomcat only, WAR bind-mounted from host
.env.example Template for all environment variables
.dockerignore Keeps build context small
All configuration is driven by environment variables in a .env file. Copy the template to get started:
cp .env.example .env
Then edit .env with your values:
| Variable | Default | Description |
|---|---|---|
POSTGRES_DB | wikantik | Database name |
POSTGRES_USER | wikantik | Database user |
POSTGRES_PASSWORD | CHANGEME | Change this! Database password |
WIKANTIK_BASE_URL | http://localhost:8080/ | Public URL of the wiki |
WIKANTIK_PAGE_DIR | /var/wikantik/pages | Page storage path inside container |
WIKANTIK_WORK_DIR | /var/wikantik/work | Work directory (Lucene index, caches) |
WIKANTIK_ATTACHMENT_DIR | /var/wikantik/pages | Attachment storage path |
MCP_ACCESS_KEYS | (empty) | Comma-separated Bearer tokens for MCP API |
MCP_RATE_LIMIT_GLOBAL | 100 | MCP requests/second (all clients) |
MCP_RATE_LIMIT_PER_CLIENT | 10 | MCP requests/second (per client) |
MAIL_SMTP_HOST | (empty) | SMTP server for email notifications |
MAIL_SMTP_PORT | 587 | SMTP port |
MAIL_SMTP_ACCOUNT | (empty) | SMTP username |
MAIL_SMTP_PASSWORD | (empty) | SMTP password |
MAIL_FROM | (empty) | From address for emails |
BACKUP_RETENTION_DAYS | 30 | Days to keep daily backups |
BACKUP_DIR | ./backups | Host path for backup files |
Important: The .env file contains secrets (database password, SMTP credentials, MCP keys). It is excluded from Git by .gitignore. Keep a copy of this file somewhere safe outside the repository.
The docker/entrypoint.sh script runs every time the wikantik container starts. It generates three configuration files from environment variables:
wikantik-custom.properties — Wiki settings: base URL, page directory, PostgreSQL JDBC database names, SMTP config, column mappingsROOT.xml — Tomcat context with a single JNDI DataSource (jdbc/WikiDatabase) pointing to the PostgreSQL containerwikantik-mcp.properties — MCP server rate limits and access keys. This single properties file configures both MCP endpoints (/wikantik-admin-mcp and /knowledge-mcp); the filename is retained from the original module name for backward compatibility.This means you never edit config files inside the container. Change an environment variable, restart the container, and the new config takes effect.
# 1. Create your .env file
cp .env.example .env
# Edit .env — at minimum, change POSTGRES_PASSWORD and WIKANTIK_BASE_URL
# 2. Create the backups directory
mkdir -p backups
# 3. Build and start everything
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
This will:
postgres:17-alpine imagedocker/db/001-init.sql to create tables and a default admin user# Check all three services are running and healthy
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
# Check the wiki responds
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/wiki/Main
# Should print: 200
# Check PostgreSQL has the admin user
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec db psql -U wikantik -d wikantik -c "SELECT login_name FROM users;"
# Check the MCP endpoint
curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/mcp
# Should print: 400 (expected — MCP uses Streamable HTTP, not plain GET)
# View wikantik startup logs
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs wikantik
The init script creates an admin user. Log in at http://your-host:8080/Login.jsp:
adminadminChange this password immediately after first login via the user preferences page.
When you pull new code and want to deploy:
# Rebuild only the wikantik image and restart it
# PostgreSQL and backup keep running — no data loss
docker compose -f docker-compose.yml -f docker-compose.prod.yml build wikantik
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps wikantik
Downtime is roughly 10-20 seconds while the container restarts. If you have Cloudflare in front, it serves cached content during the gap.
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
This stops all containers but preserves all Docker volumes (database, pages, logs). Your data is safe. To start again:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d
Warning: docker compose down -v deletes all volumes and destroys all data. Never use -v unless you intentionally want to start from scratch.
The backup system is the most important part of the production deployment. It protects against data loss from disk failure, accidental deletion, software bugs, or corrupted content.
Every backup captures two things:
PostgreSQL database — pg_dump creates a complete SQL dump of all tables (users, roles, groups, group members). This file can restore the database to any PostgreSQL 15+ server.
Wiki page files — tar archives the entire /var/wikantik/pages directory, which contains:
.md files (the actual article content in Markdown).properties files (page metadata: author, change notes, timestamps)Both are essential. Without the database, you lose all user accounts and group memberships. Without the page files, you lose all wiki content. The backup system captures both in every run.
The cron schedule runs three tiers of backups:
| Tier | Schedule | Retention | Purpose |
|---|---|---|---|
| Daily | 2:00 AM every day | 30 days (configurable) | Recover from recent mistakes |
| Weekly | 3:00 AM every Sunday | 12 weeks | Recover from issues noticed late |
| Monthly | 4:00 AM on the 1st | 12 months | Long-term recovery point |
Each backup creates a directory like backups/daily/2026-03-21/ containing:
backups/
daily/
2026-03-21/
db.sql PostgreSQL dump (all users, groups, roles)
pages.tar.gz All wiki pages, properties, and attachments
checksums.sha256 SHA-256 hashes for integrity verification
2026-03-20/
...
weekly/
2026-03-16/
...
monthly/
2026-03-01/
...
You can trigger a backup at any time without waiting for cron:
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup /usr/local/bin/backup.sh daily
The output shows exactly what was captured:
================================================================
[Fri Mar 21 02:00:01 UTC 2026] Starting daily backup
================================================================
Dumping PostgreSQL database wikantik...
db.sql: 4523 bytes
Archiving wiki pages...
pages.tar.gz: 189234 bytes (47 .md files)
checksums.sha256 written
Backup written to /backups/daily/2026-03-21
total 192K
-rw-r--r-- 1 root root 4.5K Mar 21 02:00 db.sql
-rw-r--r-- 1 root root 185K Mar 21 02:00 pages.tar.gz
-rw-r--r-- 1 root root 142 Mar 21 02:00 checksums.sha256
================================================================
Always run a manual backup before deploying updates or making major changes.
Check that backups are being created:
# List recent backups on the host
ls -la backups/daily/
# Check backup sizes (should not be zero)
du -sh backups/daily/*/
# Verify checksums of a specific backup
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup sh -c "cd /backups/daily/2026-03-21 && sha256sum -c checksums.sha256"
By default, backups go to ./backups/ relative to the docker-compose files. To use a different location (like an external drive or NFS mount), set BACKUP_DIR in your .env:
BACKUP_DIR=/mnt/external-backup/wikantik
To keep daily backups for 60 days instead of 30:
BACKUP_RETENTION_DAYS=60
Then restart the backup container:
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps backup
Restoring from backup is a three-step process. The restore script handles the actual data recovery; you just need to stop the wiki, run the script, and start the wiki again.
# 1. Stop the wiki (prevents writes during restore)
docker compose -f docker-compose.yml -f docker-compose.prod.yml stop wikantik
# 2. See what backups are available
ls backups/daily/
# 3. Run the restore script (pick the date you want)
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup /usr/local/bin/restore.sh /backups/daily/2026-03-21
# 4. Start the wiki
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d wikantik
The restore script will:
After starting the wiki, the Lucene search index rebuilds automatically from the restored page files. This takes 30-60 seconds depending on the number of pages.
If you only need to restore user accounts without touching page content:
docker compose -f docker-compose.yml -f docker-compose.prod.yml stop wikantik
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup sh -c "
psql -h db -U \${POSTGRES_USER} -d \${POSTGRES_DB} \
-c 'DROP TABLE IF EXISTS group_members, groups, roles, users CASCADE;'
psql -h db -U \${POSTGRES_USER} -d \${POSTGRES_DB} \
< /backups/daily/2026-03-21/db.sql
"
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d wikantik
If you only need to restore page content without touching the database:
docker compose -f docker-compose.yml -f docker-compose.prod.yml stop wikantik
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup sh -c "
rm -rf /var/wikantik/pages/*
tar -xzf /backups/daily/2026-03-21/pages.tar.gz -C /var/wikantik/pages
"
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d wikantik
If the original server is gone and you are starting from a new machine with only the backup files:
# 1. Clone the repository
git clone https://github.com/your-repo/wikantik.git
cd wikantik
# 2. Create .env with your production values
cp .env.example .env
# Edit .env with the same POSTGRES_PASSWORD and other settings
# 3. Start only PostgreSQL (let it initialize)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d db
# Wait for it to become healthy:
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps
# 4. Copy your backup files to the backups directory
mkdir -p backups/daily/2026-03-21
cp /path/to/your/backup/db.sql backups/daily/2026-03-21/
cp /path/to/your/backup/pages.tar.gz backups/daily/2026-03-21/
cp /path/to/your/backup/checksums.sha256 backups/daily/2026-03-21/
# 5. Build and start wikantik (this creates the pages volume)
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build wikantik
# 6. Start the backup container
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d backup
# 7. Run the restore (replaces init data with your real data)
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup /usr/local/bin/restore.sh /backups/daily/2026-03-21
# 8. Restart wikantik to pick up the restored content
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart wikantik
If you are currently running Wikantik on a bare-metal Tomcat with a local PostgreSQL database, follow these steps to migrate to containers.
Make a safety backup of your current system:
# Dump the existing database
pg_dump -U jspwiki -d jspwiki --no-owner --no-privileges > pre-migration.sql
# Archive the current pages
tar -czf pre-migration-pages.tar.gz -C /path/to/your/wikantik-pages .
Keep these files safe. They are your rollback path.
# 1. Stop the bare-metal Tomcat
/path/to/tomcat/bin/shutdown.sh
# 2. Create .env with production values
cp .env.example .env
# Set POSTGRES_PASSWORD, WIKANTIK_BASE_URL, SMTP settings, MCP keys, etc.
# 3. Start only PostgreSQL
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d db
# Wait for healthy...
# 4. Replace the auto-created tables with your real data
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec -T db psql -U wikantik -d wikantik < pre-migration.sql
# 5. Build and start wikantik
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build wikantik
# 6. Copy your existing pages into the Docker volume
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec -T wikantik sh -c "rm -rf /var/wikantik/pages/*"
docker cp pre-migration-pages.tar.gz \
$(docker compose -f docker-compose.yml -f docker-compose.prod.yml ps -q wikantik):/tmp/
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec wikantik sh -c "tar -xzf /tmp/pre-migration-pages.tar.gz -C /var/wikantik/pages && rm /tmp/pre-migration-pages.tar.gz"
# 7. Restart wikantik to rebuild the search index with the real pages
docker compose -f docker-compose.yml -f docker-compose.prod.yml restart wikantik
# 8. Start the backup service
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d backup
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup /usr/local/bin/backup.sh daily
You still have the bare-metal Tomcat and the pre-migration backup files. To roll back:
# Stop the containers
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
# Start the bare-metal Tomcat again
/path/to/tomcat/bin/startup.sh
The dev setup skips the production build and backup service. It bind-mounts your local files so changes appear immediately.
# Build the WAR on your host (uses your local Maven cache — fast)
mvn clean install -Dmaven.test.skip -T 1C
# Start the dev environment
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
# Wiki: http://localhost:8080/
# Debug port: 5005 (attach IntelliJ/VS Code remote debugger)
# PostgreSQL: localhost:15432 (connect with any SQL client)
Key differences from production:
docs/wikantik-pages/ — edits in the wiki appear in your Git working directorywikantik-war/target/ — rebuild the WAR and restart the container to pick up changesThe Tomcat server.xml includes two settings for running behind Cloudflare:
RemoteIpValve — Extracts the real client IP from the CF-Connecting-IP header. Without this, all requests appear to come from Cloudflare's IPs.
AccessLogValve — Logs the CF-IPCountry header for geographic insight.
Cloudflare terminates TLS, so the container only listens on HTTP port 8080. There is no HTTPS connector in the container.
If you are using WIKANTIK_BASE_URL=https://wiki.example.com/, the wiki generates HTTPS URLs for links, sitemaps, and canonical tags, even though the container itself receives HTTP. This is the correct configuration for Cloudflare proxying.
# Check logs for errors
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs wikantik
# Common issues:
# - "Connection refused" to db → PostgreSQL isn't healthy yet, wikantik will retry
# - "CHANGEME" in logs → You didn't set POSTGRES_PASSWORD in .env
# - Port 8080 already in use → Stop your bare-metal Tomcat first
# Verify PostgreSQL is healthy
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps db
# Connect directly to verify
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec db psql -U wikantik -d wikantik -c "SELECT 1;"
If the wiki starts but pages are missing, the pages volume may be empty:
# Check how many pages are in the volume
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec wikantik find /var/wikantik/pages -name '*.md' | wc -l
If zero, the volume was created empty. Restore from backup or copy pages in (see migration steps).
# Check if it's running
docker compose -f docker-compose.yml -f docker-compose.prod.yml ps backup
# Check cron logs
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs backup
# Verify cron schedule is loaded
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup crontab -l
# === Production ===
# Start everything
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --build
# Rebuild and deploy wiki (db + backup keep running)
docker compose -f docker-compose.yml -f docker-compose.prod.yml build wikantik
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps wikantik
# View logs
docker compose -f docker-compose.yml -f docker-compose.prod.yml logs -f wikantik
# Manual backup
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup /usr/local/bin/backup.sh daily
# Restore from backup
docker compose -f docker-compose.yml -f docker-compose.prod.yml stop wikantik
docker compose -f docker-compose.yml -f docker-compose.prod.yml \
exec backup /usr/local/bin/restore.sh /backups/daily/2026-03-21
docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d wikantik
# Stop everything (data preserved)
docker compose -f docker-compose.yml -f docker-compose.prod.yml down
# === Development ===
mvn clean install -Dmaven.test.skip -T 1C
docker compose -f docker-compose.yml -f docker-compose.dev.yml up -d --build
docker compose -f docker-compose.yml -f docker-compose.dev.yml down