Wikantik on Docker

This guide covers everything you need to deploy Wikantik using Docker Compose, from initial setup through production operations and disaster recovery.

Architecture Overview

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 21) | 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.

What lives where

| 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.

File Layout

```

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

```

Environment Variables

All configuration is driven by environment variables in a `.env` file. Copy the template to get started:

```bash

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.

How the Entrypoint Works

The `docker/entrypoint.sh` script runs every time the wikantik container starts. It generates three configuration files from environment variables:

1. **`wikantik-custom.properties`** — Wiki settings: base URL, page directory, PostgreSQL JDBC database names, SMTP config, column mappings

2. **`ROOT.xml`** — Tomcat context with a single JNDI DataSource (`jdbc/WikiDatabase`) pointing to the PostgreSQL container

3. **`wikantik-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.

Production Deployment

First-time setup

```bash

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:

- Pull the `postgres:17-alpine` image

- Build the Wikantik image (multi-stage Maven build, takes a few minutes the first time)

- Start PostgreSQL and run `docker/db/001-init.sql` to create tables and a default admin user

- Start Wikantik, which connects to PostgreSQL and begins serving pages

- Start the backup container with cron scheduling

Verifying it works

```bash

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

```

Default admin login

The init script creates an admin user. Log in at `http://your-host:8080/Login.jsp`:

- **Username:** `admin`

- **Password:** `admin`

**Change this password immediately** after first login via the user preferences page.

Deploying updates

When you pull new code and want to deploy:

```bash

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.

Stopping everything

```bash

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:

```bash

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.

Backup System

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.

What gets backed up

Every backup captures **two things**:

1. **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.

2. **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)

- Attachment files (anything uploaded to wiki pages)

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.

Backup schedule and retention

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/

...

```

Running a manual backup

You can trigger a backup at any time without waiting for cron:

```bash

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.**

Verifying backups

Check that backups are being created:

```bash

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"

```

Changing the backup directory

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`:

```bash

BACKUP_DIR=/mnt/external-backup/wikantik

```

Changing retention

To keep daily backups for 60 days instead of 30:

```bash

BACKUP_RETENTION_DAYS=60

```

Then restart the backup container:

```bash

docker compose -f docker-compose.yml -f docker-compose.prod.yml up -d --no-deps backup

```

Restore Procedure

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.

Step-by-step restore

```bash

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:

1. Verify SHA-256 checksums to confirm the backup is not corrupted

2. Drop and recreate the PostgreSQL tables from the backup dump

3. Clear the current pages directory and extract the backup archive

4. Report how many users and pages were restored

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.

Restoring only the database (keeping current pages)

If you only need to restore user accounts without touching page content:

```bash

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

```

Restoring only pages (keeping current users)

If you only need to restore page content without touching the database:

```bash

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

```

Disaster recovery: restoring to a fresh machine

If the original server is gone and you are starting from a new machine with only the backup files:

```bash

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

```

Migrating from Bare-Metal Tomcat

If you are currently running Wikantik on a bare-metal Tomcat with a local PostgreSQL database, follow these steps to migrate to containers.

Before you start

Make a safety backup of your current system:

```bash

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.

Migration steps

```bash

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

```

Verify the migration

- Browse to your wiki URL and confirm pages load correctly

- Log in with your existing admin account

- Check that search works (the index rebuilds on startup)

- Create a test page and verify it persists across a container restart

- Run a manual backup and verify the output:

```bash

docker compose -f docker-compose.yml -f docker-compose.prod.yml \

exec backup /usr/local/bin/backup.sh daily

```

If something goes wrong

You still have the bare-metal Tomcat and the pre-migration backup files. To roll back:

```bash

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

```

Development Workflow

The dev setup skips the production build and backup service. It bind-mounts your local files so changes appear immediately.

```bash

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:

- **Pages are bind-mounted** from `docs/wikantik-pages/` — edits in the wiki appear in your Git working directory

- **WAR is bind-mounted** from `wikantik-war/target/` — rebuild the WAR and restart the container to pick up changes

- **Debug port 5005** is exposed for remote JVM debugging

- **PostgreSQL port 15432** is exposed (not 5432, to avoid conflicting with a local PostgreSQL)

- **MCP has no access keys** (open for local testing)

- **No backup service** runs

Cloudflare Integration

The Tomcat `server.xml` includes two settings for running behind Cloudflare:

1. **`RemoteIpValve`** — Extracts the real client IP from the `CF-Connecting-IP` header. Without this, all requests appear to come from Cloudflare's IPs.

2. **`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.

Troubleshooting

Container won't start

```bash

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

```

Database connection errors

```bash

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;"

```

Pages not showing up

If the wiki starts but pages are missing, the pages volume may be empty:

```bash

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).

Backup container not running

```bash

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

```

Quick Reference

```bash

=== 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

```