---
title: Wikijs Backup
description: Backup Wikijs
published: true
date: 2026-02-23T04:35:32.870Z
tags: 
editor: markdown
dateCreated: 2026-02-23T04:35:24.121Z
---

# Wiki.js Backup & Recovery

**Service:** Wiki.js (Netgrimoire)
**Stack:** Docker Compose — Wiki.js + PostgreSQL
**Backup Targets:** PostgreSQL database dump, Git content repository, Docker Compose config
**Backup Destinations:** Local vault path → Kopia → offsite vaults

---

## Overview

Wiki.js data lives in two separate places that must be backed up independently:

**PostgreSQL database** — stores page metadata, navigation, user accounts, permissions, page history, assets, and all configuration. This is the critical component for a portable restore. Without it, a new instance has no knowledge of your wiki structure.

**Git content repository** — stores the actual page content in markdown files, synced from Forgejo. This is already mirrored on the VAULT SSD at `/vault/repos/wiki/`. It is inherently redundant as long as Forgejo is healthy, but is included in backups for completeness and offline portability.

**Docker Compose config** — the `docker-compose.yml` and `.env` files needed to recreate the stack.

---

## What Gets Backed Up

| Component | Location | Method | Critical? |
|---|---|---|---|
| PostgreSQL database | Docker volume | `pg_dump` → SQL file | Yes — primary restore target |
| Git content repo | `/vault/repos/wiki/` | Already on VAULT SSD | Yes — page content |
| Docker Compose files | `/opt/stacks/wikijs/` | rsync copy | Yes — stack config |
| Wiki.js data volume | Docker volume | Optional rsync | No — DB + Git covers this |

---

## Backup Strategy

### Tier 1 — Daily Dump to Vault Path

A script runs daily via systemd timer. It produces a portable `pg_dump` SQL file written to `/vault/backups/wiki/`. These local dumps are retained for 14 days.

**Key choices:**

- `--format=plain` — plain SQL, portable to any PostgreSQL version and any host
- `--no-owner` — strips role ownership, so the dump restores cleanly on a new instance with a different postgres user (critical for Pocket Grimoire restores)
- `--no-acl` — strips GRANT/REVOKE statements for the same reason
- No application downtime required — PostgreSQL handles consistent dumps natively

### Tier 2 — Kopia Snapshot to Offsite Vaults

After the daily dump completes, Kopia snapshots the entire `/vault/backups/wiki/` directory and replicates to your offsite vaults. Kopia deduplication means only changed blocks are transferred after the first run.

---

## Setup

### Step 0 — Confirm Kopia Repository Exists

If Kopia is not yet initialized on this host, initialize it first. If you already initialized Kopia for Mailcow or another service, skip this step — all services share the same Kopia repository.

```bash
# Check if repository already exists
kopia repository status

# If not initialized, create it against your vault path
kopia repository create filesystem --path=/vault/kopia

# Connect on subsequent logins if disconnected
kopia repository connect filesystem --path=/vault/kopia
```

### Step 1 — Create Backup Directories

```bash
sudo mkdir -p /vault/backups/wiki
sudo chown $(whoami):$(whoami) /vault/backups/wiki
```

### Step 2 — Create the Backup Script

```bash
sudo nano /usr/local/sbin/wikijs-backup.sh
```

```bash
#!/usr/bin/env bash
# wikijs-backup.sh — Daily Wiki.js backup: pg_dump + git repo + config
# Writes to /vault/backups/wiki/, then snapshots with Kopia

set -euo pipefail

# ── Configuration ─────────────────────────────────────────────────────────────
BACKUP_DIR="/vault/backups/wiki"
DATE=$(date +%Y%m%d_%H%M%S)
CONTAINER_DB="wikijs_db"            # Adjust to your actual container name
PG_USER="wikijs"
PG_DB="wikijs"
WIKI_STACK_DIR="/opt/stacks/wikijs" # Location of docker-compose.yml and .env
GIT_REPO_DIR="/vault/repos/wiki"    # Git content mirror (already on vault SSD)
RETAIN_DAYS=14                      # Local dump retention

LOG="/var/log/wikijs-backup.log"
touch "$LOG"

log() { echo "$(date -Is) $*" | tee -a "$LOG"; }

# ── Step 1: PostgreSQL dump ────────────────────────────────────────────────────
log "Starting Wiki.js PostgreSQL dump..."

docker exec "$CONTAINER_DB" pg_dump \
  -U "$PG_USER" \
  "$PG_DB" \
  --format=plain \
  --no-owner \
  --no-acl \
  > "${BACKUP_DIR}/wikijs-db-${DATE}.sql"

gzip "${BACKUP_DIR}/wikijs-db-${DATE}.sql"

log "PostgreSQL dump complete: wikijs-db-${DATE}.sql.gz"

# ── Step 2: Docker Compose config backup ──────────────────────────────────────
log "Backing up Docker Compose config..."

CONFIG_BACKUP="${BACKUP_DIR}/wikijs-config-${DATE}.tar.gz"

tar -czf "$CONFIG_BACKUP" \
  -C "$(dirname "$WIKI_STACK_DIR")" \
  "$(basename "$WIKI_STACK_DIR")"

log "Config backup complete: wikijs-config-${DATE}.tar.gz"

# ── Step 3: Git repo snapshot (content mirror) ────────────────────────────────
# The git repo lives on the VAULT SSD and is already versioned.
# We record the current HEAD commit for reference.

if [ -d "${GIT_REPO_DIR}/.git" ]; then
  GIT_HEAD=$(git -C "$GIT_REPO_DIR" rev-parse HEAD 2>/dev/null || echo "unknown")
  echo "Git HEAD at backup time: ${GIT_HEAD}" \
    > "${BACKUP_DIR}/wikijs-git-ref-${DATE}.txt"
  log "Git content repo HEAD: ${GIT_HEAD}"
else
  log "WARNING: Git repo not found at ${GIT_REPO_DIR} — skipping git ref"
fi

# ── Step 4: Cleanup old local dumps ───────────────────────────────────────────
log "Cleaning up dumps older than ${RETAIN_DAYS} days..."

find "$BACKUP_DIR" -name "wikijs-db-*.sql.gz" -mtime +"$RETAIN_DAYS" -delete
find "$BACKUP_DIR" -name "wikijs-config-*.tar.gz" -mtime +"$RETAIN_DAYS" -delete
find "$BACKUP_DIR" -name "wikijs-git-ref-*.txt" -mtime +"$RETAIN_DAYS" -delete

# ── Step 5: Kopia snapshot ────────────────────────────────────────────────────
log "Running Kopia snapshot of /vault/backups/wiki/..."

kopia snapshot create "$BACKUP_DIR" \
  --tags "service:wikijs,host:$(hostname -s)"

log "Kopia snapshot complete."

# ── Done ──────────────────────────────────────────────────────────────────────
log "Wiki.js backup finished successfully."
```

```bash
sudo chmod +x /usr/local/sbin/wikijs-backup.sh
```

### Step 3 — Create systemd Service and Timer

```bash
sudo nano /etc/systemd/system/wikijs-backup.service
```

```ini
[Unit]
Description=Wiki.js daily backup (pg_dump + config + Kopia snapshot)
After=docker.service

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/wikijs-backup.sh
```

```bash
sudo nano /etc/systemd/system/wikijs-backup.timer
```

```ini
[Unit]
Description=Run Wiki.js backup daily at 02:00

[Timer]
OnCalendar=*-*-* 02:00:00
Persistent=true

[Install]
WantedBy=timers.target
```

```bash
sudo systemctl daemon-reload
sudo systemctl enable wikijs-backup.timer
sudo systemctl start wikijs-backup.timer

# Verify
systemctl list-timers | grep wikijs
```

### Step 4 — Configure Kopia Retention Policy

```bash
# Set retention policy for wiki backups
kopia policy set /vault/backups/wiki \
  --keep-daily 14 \
  --keep-weekly 8 \
  --keep-monthly 12 \
  --compression zstd

# Verify policy
kopia policy show /vault/backups/wiki
```

### Step 5 — Test the Backup

```bash
# Run manually first time
sudo /usr/local/sbin/wikijs-backup.sh

# Verify output
ls -lh /vault/backups/wiki/
# Should show: wikijs-db-YYYYMMDD_HHMMSS.sql.gz
#              wikijs-config-YYYYMMDD_HHMMSS.tar.gz
#              wikijs-git-ref-YYYYMMDD_HHMMSS.txt

# Verify Kopia snapshot was created
kopia snapshot list /vault/backups/wiki

# Check backup log
tail -n 30 /var/log/wikijs-backup.log
```

---

## Verifying Backups

### Check dump is readable

```bash
# Inspect the SQL dump without extracting
zcat /vault/backups/wiki/wikijs-db-YYYYMMDD_HHMMSS.sql.gz | head -50

# Should show PostgreSQL header, version info, and CREATE TABLE statements
```

### Verify Kopia snapshots

```bash
# List recent snapshots
kopia snapshot list /vault/backups/wiki

# Show snapshot details
kopia snapshot list /vault/backups/wiki --all

# Verify snapshot integrity
kopia snapshot verify
```

### Test restore to a temporary database (non-destructive)

```bash
# Start a temporary Postgres container
docker run --rm -d \
  --name wikijs-restore-test \
  -e POSTGRES_USER=wikijs \
  -e POSTGRES_PASSWORD=testpassword \
  -e POSTGRES_DB=wikijs_test \
  postgres:16-alpine

# Wait for Postgres to be ready
sleep 5

# Restore dump into test container
zcat /vault/backups/wiki/wikijs-db-YYYYMMDD_HHMMSS.sql.gz | \
  docker exec -i wikijs-restore-test psql -U wikijs -d wikijs_test

# Verify tables exist
docker exec wikijs-restore-test psql -U wikijs -d wikijs_test -c "\dt"

# Expected output: List of tables (pages, users, pageHistory, assets, etc.)

# Cleanup test container
docker stop wikijs-restore-test
```

---

## Recovery Procedures

### Scenario A — Restore to a New Wiki.js Instance (Any Host)

This covers full disaster recovery to a fresh server, including Pocket Grimoire.

**Requirements on the destination host:**
- Docker and Docker Compose installed
- A `docker-compose.yml` and `.env` ready (from backup or Pocket Grimoire stack)
- Sufficient disk space

**Step 1: Locate the backup**

```bash
# On Netgrimoire, find the dump to restore
ls -lh /vault/backups/wiki/

# Or restore from Kopia
kopia snapshot list /vault/backups/wiki
kopia restore SNAPSHOT_ID /tmp/wiki-restore/
ls /tmp/wiki-restore/
```

**Step 2: Copy dump to the destination host**

```bash
# From Netgrimoire, copy to the destination server
scp /vault/backups/wiki/wikijs-db-YYYYMMDD_HHMMSS.sql.gz \
  user@destination-host:/tmp/

# Or to Pocket Grimoire
scp /vault/backups/wiki/wikijs-db-YYYYMMDD_HHMMSS.sql.gz \
  user@pocket-grimoire.local:/tmp/
```

**Step 3: Start the database container only**

On the destination host, start just the database — do not start Wiki.js yet:

```bash
cd /srv/pocket-grimoire/stacks/wikijs   # Adjust path as needed

# Start only the database container
docker compose up -d db

# Wait for healthy status
docker compose ps
# db should show: healthy
```

**Step 4: Restore the dump**

```bash
# Restore the dump into the running database container
zcat /tmp/wikijs-db-YYYYMMDD_HHMMSS.sql.gz | \
  docker exec -i pocketgrimoire_db psql \
  -U wikijs \
  -d wikijs

# Verify tables restored
docker exec pocketgrimoire_db psql -U wikijs -d wikijs -c "\dt"
```

**Step 5: Start Wiki.js**

```bash
docker compose up -d

# Watch startup logs
docker logs -f pocketgrimoire_wikijs
# Wait for: "HTTP Server started successfully"
```

**Step 6: Verify**

Open `http://pocket-grimoire.local:3000` and confirm:
- Pages load correctly
- Navigation structure is intact
- User accounts are present (if you had multiple users)

**Step 7: Re-sync Git content (if needed)**

The database knows the page structure, but if the Git content repo isn't present on the new host, import it:

```bash
# In Wiki.js admin panel:
# Administration → Storage → Git
# Click "Force Sync" or "Import Content"

# Or copy the repo from VAULT SSD
rsync -avP /vault/repos/wiki/ /srv/pocket-grimoire/repos/wiki/
```

---

### Scenario B — Restore on Existing Netgrimoire Instance

Use this when the Wiki.js database is corrupted but the host is otherwise healthy.

**Step 1: Stop Wiki.js (leave database running)**

```bash
cd /opt/stacks/wikijs
docker compose stop wikijs
```

**Step 2: Drop and recreate the database**

```bash
docker exec -it wikijs_db psql -U postgres -c "DROP DATABASE wikijs;"
docker exec -it wikijs_db psql -U postgres -c "CREATE DATABASE wikijs OWNER wikijs;"
```

**Step 3: Restore**

```bash
zcat /vault/backups/wiki/wikijs-db-YYYYMMDD_HHMMSS.sql.gz | \
  docker exec -i wikijs_db psql -U wikijs -d wikijs
```

**Step 4: Restart Wiki.js**

```bash
docker compose start wikijs
docker logs -f wikijs
```

---

### Scenario C — Restore Config Only

If the stack config was lost but the database volume is intact:

```bash
# Extract config from backup
tar -xzf /vault/backups/wiki/wikijs-config-YYYYMMDD_HHMMSS.tar.gz \
  -C /opt/stacks/

# Verify
ls /opt/stacks/wikijs/
# Should show: docker-compose.yml  .env

# Restart stack
cd /opt/stacks/wikijs
docker compose up -d
```

---

### Restore from Kopia (Offsite)

When local vault files are unavailable, restore the backup directory from Kopia first:

```bash
# List available snapshots
kopia snapshot list /vault/backups/wiki

# Restore snapshot to temp directory
kopia restore SNAPSHOT_ID /tmp/wiki-restore/

# Then proceed with the appropriate scenario above
# using files from /tmp/wiki-restore/ instead of /vault/backups/wiki/
```

---

## Pocket Grimoire Specifics

When restoring to Pocket Grimoire, note the following differences from a full Netgrimoire instance:

**Container names** differ — use `pocketgrimoire_db` instead of `wikijs_db`.

**Stack path** is `/srv/pocket-grimoire/stacks/wikijs/` instead of `/opt/stacks/wikijs/`.

**The database is already initialized** when Pocket Grimoire is first set up. Restoring a Netgrimoire dump overwrites it entirely, which is the intended behavior — Pocket Grimoire becomes a mirror of Netgrimoire's wiki state.

**Git content repo** is located at `/srv/pocket-grimoire/repos/wiki/` and is populated via the sync script (`pocketgrimoire-sync.sh`). A database restore alone is sufficient if the Git repo is already in place.

**Recommended restore workflow for Pocket Grimoire:**

```bash
# 1. Copy dump from VAULT SSD (already available on Pocket Grimoire)
ls /srv/vaultpg/backups/wiki/

# 2. Start db container only
cd /srv/pocket-grimoire/stacks/wikijs && docker compose up -d db

# 3. Restore
zcat /srv/vaultpg/backups/wiki/wikijs-db-LATEST.sql.gz | \
  docker exec -i pocketgrimoire_db psql -U wikijs -d wikijs

# 4. Start full stack
docker compose up -d
```

Because the VAULT SSD is always connected to Pocket Grimoire, no file transfer is needed — the dumps are already there.

---

## Monitoring & Alerts

Add the following to your existing ntfy/monitoring setup to alert on backup failures. Wrap the backup script call in an error trap:

```bash
# Add to wikijs-backup.sh after set -euo pipefail:

NTFY_URL="https://ntfy.YOUR_DOMAIN/wikijs-backup"

on_error() {
  curl -fsS -X POST "$NTFY_URL" \
    -H "Title: Wiki.js backup FAILED ($(hostname -s))" \
    -H "Priority: high" \
    -H "Tags: rotating_light" \
    -d "Backup failed at $(date -Is). Check /var/log/wikijs-backup.log"
}
trap on_error ERR
```

### Check backup age manually

```bash
# Find most recent dump
ls -lt /vault/backups/wiki/wikijs-db-*.sql.gz | head -3

# Check Kopia last snapshot time
kopia snapshot list /vault/backups/wiki | tail -5
```

---

## Quick Reference

```bash
# Run backup manually
sudo /usr/local/sbin/wikijs-backup.sh

# Watch backup log
tail -f /var/log/wikijs-backup.log

# Check timer status
systemctl status wikijs-backup.timer

# List local dumps
ls -lh /vault/backups/wiki/

# List Kopia snapshots
kopia snapshot list /vault/backups/wiki

# Restore dump (generic)
zcat /vault/backups/wiki/wikijs-db-YYYYMMDD_HHMMSS.sql.gz | \
  docker exec -i CONTAINER_NAME psql -U wikijs -d wikijs

# Test dump is readable
zcat /vault/backups/wiki/wikijs-db-YYYYMMDD_HHMMSS.sql.gz | head -50
```

---

## Revision History

| Version | Date | Notes |
|---|---|---|
| 1.0 | 2026-02-22 | Initial release — pg_dump + Kopia + Pocket Grimoire restore procedures |
