Fresh Deployment & Legacy Article Import
---
canonical_id: 01KQ0P44TNFQMPBHGY5R4G4A4F
cluster: databases
summary: End-to-end guide for bringing up a fresh Wikantik instance, including how to import pages from a legacy JSPWiki deployment.
tags:
- deployment
- postgresql
- operations
type: article
---
This guide walks you through standing up a new Wikantik instance against PostgreSQL and, optionally, importing an existing corpus from a legacy JSPWiki deployment. It reflects the current code on `main`: database migrations live in `bin/db/migrations/`, the WAR deploys as the ROOT context, and the React SPA is served from the same origin.
Overview
A complete deployment touches five things:
| Layer | What it holds | How it is provisioned |
|-------|---------------|-----------------------|
| PostgreSQL | Users, roles, groups, policy grants, chunk embeddings, API keys | `bin/db/install-fresh.sh` + `bin/db/migrate.sh` (idempotent) |
| Tomcat 11 | Servlet container + JNDI DataSource | Downloaded automatically by `bin/deploy-local.sh` |
| Page corpus | Markdown files under `docs/wikantik-pages/` | Version-controlled; no per-instance copy step |
| Search indexes | Lucene (in `workDir`) + `content_chunk_embeddings` (Postgres) | Built on startup + async bootstrap |
| Embedding backend | Ollama HTTP endpoint (optional) | External service; URL configured in properties |
---
Prerequisites
| Tool | Version | Check |
|------|---------|-------|
| Java (JDK) | 21+ | `java -version` |
| Maven | 3.9+ | `mvn -version` |
| Node.js + npm | 18+ | Required — the WAR build runs `npm install` + `vite build` automatically |
| PostgreSQL | 15+ (with `pgvector`) | `psql --version`; pgvector is installed by `V004` |
| `curl`, `tar` | — | Used by `deploy-local.sh` to fetch Tomcat |
PostgreSQL must be reachable on `localhost:5432` (or override `PGHOST`/`PGPORT`).
---
Fresh Deployment (Clean Instance)
1. Bootstrap the database
`install-fresh.sh` creates the database, the application role, and runs every migration in order. It is idempotent — re-running against an already-bootstrapped database is a no-op.
```bash
sudo -u postgres \
DB_NAME=wikantik \
DB_APP_USER=jspwiki \
DB_APP_PASSWORD='ChangeMe123!' \
bin/db/install-fresh.sh
```
This creates:
- Database `wikantik` with the `pgvector` extension installed
- Application role `jspwiki` with `CONNECT` + `USAGE` on `public`
- All tables from `bin/db/migrations/V001`–`V010` (users/roles/groups, policy grants, knowledge graph, content chunks + embeddings, API keys)
- A `schema_migrations` table so `migrate.sh` knows what has been applied
To bootstrap a dedicated migration role (recommended for production so `ALTER` migrations don't run as `postgres`), also set `DB_MIGRATE_PASSWORD` and the script will call `create-migrate-user.sh` and transfer ownership.
2. Build the WAR
```bash
mvn clean install -Dmaven.test.skip -T 1C
```
This compiles all modules, builds the React frontend, and produces `wikantik-war/target/Wikantik.war`. Use a full `mvn clean install` (without `-Dmaven.test.skip`) before any production cutover to make sure the test suite is green.
3. Deploy to Tomcat
```bash
bin/deploy-local.sh
```
The script is self-assembling. On first run it will:
1. Download Tomcat 11 into `tomcat/tomcat-11/` (gitignored)
2. Download the PostgreSQL JDBC driver to `tomcat/tomcat-11/lib/`
3. Copy the context template to `tomcat/tomcat-11/conf/Catalina/localhost/ROOT.xml` with a `YOUR_SECURE_PASSWORD_HERE` placeholder
4. Copy the properties template to `tomcat/tomcat-11/lib/wikantik-custom.properties`, substituting `@@REPO_ROOT@@` for the repo path
5. Copy the Log4j config to `tomcat/tomcat-11/lib/log4j2.xml`
6. Deploy `Wikantik.war` as `webapps/ROOT.war`
7. Run `migrate.sh` against the target database (no-op on fresh install, applies any new migrations on subsequent runs)
8. Run `bin/db/seed-users.sql` — upserts the `admin` / `admin123` and `[email protected]` / `passw0rd` dev accounts
9. Start Tomcat
4. Set the database password
Edit the context file to replace the placeholder:
```bash
$EDITOR tomcat/tomcat-11/conf/Catalina/localhost/ROOT.xml
username="wikantik" → username="jspwiki"
password="YOUR_SECURE_PASSWORD_HERE" → password="ChangeMe123!" (or whatever you set)
```
Restart Tomcat so the DataSource picks up the new password:
```bash
tomcat/tomcat-11/bin/shutdown.sh
tomcat/tomcat-11/bin/startup.sh
```
5. Verify
- Browse http://localhost:8080/ — the React SPA should load.
- Log in as `admin` / `admin123` (change this immediately if the instance is anything more than local scratch).
- Visit `/admin/content/stats` from the admin UI — confirm page count and index status.
- Tail `tomcat/tomcat-11/logs/catalina.out` for any DataSource errors.
For automated testing, `test.properties` (gitignored) holds a dedicated `testbot` admin account — see `CLAUDE.md` for how to recreate it after a database reset.
---
Importing Articles from a Legacy JSPWiki Deployment
Wikantik stores pages as `*.md` files under `docs/wikantik-pages/` (version-controlled), with optional `*.properties` sidecars for per-page metadata. A legacy JSPWiki corpus stores pages as `*.txt` files using classic wiki syntax. The import is a one-time conversion.
1. Export the legacy corpus
Copy the page directory and attachments from the old deployment:
```bash
On the legacy host
tar -czf legacy-pages.tgz \
/var/lib/jspwiki/pages \
/var/lib/jspwiki/attachments
scp legacy-pages.tgz new-host:/tmp/
```
A JSPWiki page directory typically contains:
- `PageName.txt` — the current revision, wiki syntax
- `PageName.properties` — frontmatter-like metadata (author, timestamp, keywords)
- `OLD/PageName/1.txt`, `2.txt`, ... — prior revisions (optional; Wikantik treats git history as the version store)
- `attachments/PageName-att/...` — file attachments per page
2. Stage the files
Unpack into a staging directory — **not** directly into `docs/wikantik-pages/` until after conversion:
```bash
mkdir -p /tmp/wikantik-import
tar -xzf /tmp/legacy-pages.tgz -C /tmp/wikantik-import --strip-components=3
```
Drop the `OLD/` hierarchy if you do not want to preserve old revisions as separate pages — Wikantik treats git as the version of record.
3. Convert wiki syntax → Markdown
The `scripts/wiki2markdown.py` converter is a faithful port of `WikiToMarkdownConverter.java`. It scans a directory for `.txt` files, scores each against a wiki-syntax heuristic, converts if the score is above threshold, renames straight to `.md` if the file is already Markdown, and deletes the source `.txt`.
```bash
Dry-run first — prints every planned action with per-file warnings
python3 scripts/wiki2markdown.py /tmp/wikantik-import --dry-run
Commit the conversion
python3 scripts/wiki2markdown.py /tmp/wikantik-import
```
Conversions performed:
| Wiki | Markdown |
|------|----------|
| `! Heading`, `!! Heading`, `!!! Heading` | `# Heading`, `## Heading`, `### Heading` |
| `__bold__` | `**bold**` |
| `''italic''` | `*italic*` |
| `{{inline code}}` | `` `inline code` `` |
| `[PageName]` | `[PageName]()` (Flexmark plugin syntax — Wikantik resolves the empty URL on render) |
| `[text|url]` | `[text](http://example.com)` |
| `[{Plugin args}]` | `[{Plugin args}]()` |
| `{{{ code block }}}` | triple-backtick fenced block |
| `|| header || cells ||` / `|| row |` | GFM tables |
Any unconverted constructs (`%%style`, old-style footnotes, etc.) are flagged as warnings next to the filename. Resolve these by hand or leave them — Flexmark will render them as literal text.
4. Move into the page corpus
```bash
Pages
cp /tmp/wikantik-import/*.md docs/wikantik-pages/
Keep the property sidecars — they carry categories, keywords, authors
cp /tmp/wikantik-import/*.properties docs/wikantik-pages/ 2>/dev/null || true
```
Attachments are not yet version-controlled in `docs/wikantik-pages/`. If the legacy corpus uses them:
```bash
Attachments live under the wiki workDir, referenced by wikantik.basicAttachmentProvider.storageDir
mkdir -p tomcat/tomcat-11/data/attachments
cp -R /tmp/wikantik-import/attachments/* tomcat/tomcat-11/data/attachments/
```
5. Backfill frontmatter (optional)
Wikantik expects a YAML frontmatter block on each Markdown page. If a page lacks one, `wikantik.frontmatter.autoDefaults = true` (set by the deploy template) will generate a minimal block — `title`, `type: article`, `tags: [uncategorized]`, empty `summary` — the first time the page is saved through the UI. For a bulk import you typically want real frontmatter. A minimal block looks like:
```yaml
---
type: article
tags:
- imported
summary: One-line summary for search and link previews.
---
```
Run a small script over the staging directory to inject this block on every `.md` file that does not already have one — the import script at `scripts/wiki2markdown.py` does **not** do this for you.
6. Commit and redeploy
```bash
git add docs/wikantik-pages/
git commit -m "import: legacy JSPWiki page corpus"
bin/deploy-local.sh
```
`deploy-local.sh` does not rebuild indexes automatically — Wikantik will pick up the new Markdown on first request, but Lucene and the chunk embeddings need to be rebuilt explicitly.
7. Rebuild indexes
**Lucene + chunks** — via admin UI at `/admin/index-status`, or via REST:
```bash
source <(grep -v '^#' test.properties | sed 's/test.user.//' | sed 's/= /="/' | sed 's/$/"/')
curl -u "${login}:${password}" -X POST \
http://localhost:8080/admin/content/rebuild-indexes
```
**Embedding reindex** (hybrid search — only if `wikantik.search.hybrid.enabled=true` and the Ollama endpoint is reachable):
```bash
curl -u "${login}:${password}" -X POST \
http://localhost:8080/admin/content/reindex-embeddings
```
The embedding rebuild is async and streams progress into `GET /admin/content/index-status`. For a corpus of a few thousand pages against a CPU-only Ollama host, budget 30–90 minutes.
On a completely fresh instance the startup `BootstrapEmbeddingIndexer` will kick off the embedding run automatically when it detects an empty `content_chunk_embeddings` table — no admin action needed unless you want to force a rerun.
---
Subsequent Deployments
After the one-time setup, redeploying is a single command:
```bash
bin/deploy-local.sh
```
The script is safe to re-run: it preserves the context file (and therefore the DB password), reapplies any new migrations, re-seeds users, and restarts Tomcat. Rebuild the project first with `mvn clean install -Dmaven.test.skip -T 1C` if you have source changes.
For a faster iteration when only the WAR has changed:
```bash
mvn clean install -Dmaven.test.skip -T 1C -pl wikantik-war -am
tomcat/tomcat-11/bin/shutdown.sh
rm -rf tomcat/tomcat-11/webapps/ROOT tomcat/tomcat-11/webapps/ROOT.war \
tomcat/tomcat-11/data/workdir tomcat/tomcat-11/logs/wikantik
cp wikantik-war/target/Wikantik.war tomcat/tomcat-11/webapps/ROOT.war
tomcat/tomcat-11/bin/startup.sh
```
---
Configuration Reference
Git-tracked templates
Located in `wikantik-war/src/main/config/tomcat/`:
| File | Installed to | Purpose |
|------|--------------|---------|
| `Wikantik-context.xml.template` | `conf/Catalina/localhost/ROOT.xml` | JNDI DataSource for PostgreSQL |
| `wikantik-custom-postgresql.properties.template` | `lib/wikantik-custom.properties` | Wiki settings (page dir, workDir, admin bootstrap, hybrid search, CORS) |
| `log4j2-local.xml.template` | `lib/log4j2.xml` | Logging config pointed at `${catalina.base}/logs/wikantik` |
Key properties
| Property | Default | Notes |
|----------|---------|-------|
| `wikantik.pageProvider` | `VersioningFileProvider` | Reads from the on-disk page directory |
| `wikantik.fileSystemProvider.pageDir` | `<repo>/docs/wikantik-pages` | Tracked in git |
| `wikantik.workDir` | `<repo>/tomcat/tomcat-11/data/workdir` | Lucene index lives here |
| `wikantik.datasource` | `jdbc/WikiDatabase` | Must match the Resource name in `ROOT.xml` |
| `wikantik.userdatabase` | `JDBCUserDatabase` | Users/roles stored in Postgres |
| `wikantik.admin.bootstrap` | _unset_ | Set to a login name on first deploy to guarantee admin access; remove after |
| `wikantik.frontmatter.autoDefaults` | `true` | Generates minimal frontmatter on save for pages without it |
| `wikantik.search.hybrid.enabled` | `true` | Toggle dense retrieval; falls back to BM25 if off |
| `wikantik.search.embedding.base-url` | `http://inference.jakefear.com:11434` | Ollama endpoint |
| `wikantik.search.embedding.model` | `qwen3-embedding-0.6b` | Also supports `nomic-embed-v1.5`, `bge-m3` |
| `wikantik.cors.allowedOrigins` | _unset_ | Comma list; supports `https://*.example.com` wildcards |
---
Troubleshooting
Database
| Symptom | Cause | Resolution |
|---------|-------|------------|
| `Connection refused` in `catalina.out` | PostgreSQL not running | `sudo systemctl start postgresql` |
| `Password authentication failed for user "jspwiki"` | Password mismatch between `ROOT.xml` and role | Edit `ROOT.xml`; restart Tomcat |
| `relation "users" does not exist` | Migrations never ran | `DB_NAME=wikantik bin/db/migrate.sh --status` to check state; then run without `--status` to apply |
| `must be owner of table X` during migrate | Tables owned by `postgres`, running as `migrate` | Re-run `bin/db/create-migrate-user.sh` to transfer ownership |
| `extension "vector" is not available` | pgvector not installed on server | `sudo apt install postgresql-15-pgvector` (or the distro equivalent) |
Deployment
| Symptom | Cause | Resolution |
|---------|-------|------------|
| `WAR file not found` | Build not run | `mvn clean install -Dmaven.test.skip -T 1C` |
| `npm not found` | Node.js missing | Install Node 18+; the WAR build depends on `vite` |
| `JNDI name not found` | Context file not loaded | Confirm `ROOT.xml` exists in `conf/Catalina/localhost/` |
| Login fails with seeded password | Schema mismatch after a reset | Re-run `psql -d wikantik -f bin/db/seed-users.sql` |
| 500 on admin API after DB restart | Expected: fail-soft for reads, hard-fail for writes | Connection pool reheats on next request; admin writes need the DB back |
Indexes
| Symptom | Cause | Resolution |
|---------|-------|------------|
| Empty search results on a freshly-imported corpus | Lucene empty | `POST /admin/content/rebuild-indexes` |
| Hybrid search returns BM25-only | Embedding backend unreachable or circuit open | Check `wikantik.search.embedding.base-url`; inspect `catalina.out` for `embedder` warnings |
| `reindex-embeddings` returns 409 | A rebuild is already running | Wait; poll `GET /admin/content/index-status` |
| `reindex-embeddings` returns 503 | `wikantik.search.hybrid.enabled=false` or no embedder configured | Enable hybrid search and confirm the Ollama endpoint responds |
Logs
```bash
Tomcat container log
tail -f tomcat/tomcat-11/logs/catalina.out
Application logs (Log4j)
tail -f tomcat/tomcat-11/logs/wikantik/wikantik.log
Filter for DataSource / JNDI errors
grep -i "jdbc\|datasource\|jndi" tomcat/tomcat-11/logs/catalina.out
Migration status
DB_NAME=wikantik bin/db/migrate.sh --status
```
Full reset (local only — destroys data)
```bash
Tomcat
tomcat/tomcat-11/bin/shutdown.sh
rm -rf tomcat/tomcat-11/webapps/ROOT tomcat/tomcat-11/webapps/ROOT.war \
tomcat/tomcat-11/data/workdir tomcat/tomcat-11/logs/wikantik
Database
sudo -u postgres psql -c 'DROP DATABASE wikantik;'
sudo -u postgres DB_NAME=wikantik DB_APP_USER=jspwiki \
DB_APP_PASSWORD='ChangeMe123!' bin/db/install-fresh.sh
Redeploy
bin/deploy-local.sh
```
---
Related Documentation
- [Developing with PostgreSQL](DevelopingWithPostgresql) — JDBC and JNDI configuration reference
- [Docker Deployment](DockerDeployment) — containerized production deployment
- [Observability Design](ObservabilityDesign) — health checks, Prometheus metrics, request correlation
- [Index Rebuild](IndexRebuild) — deeper dive on Lucene + embedding rebuild internals
- `bin/db/migrations/README.md` — migration conventions for schema changes
- `CLAUDE.md` — sole-developer workflow notes, test credentials, and dev shortcuts