# Proxmox CT Self-Hosted Services — Docker Compose Recipes

Reference: session 2026-07-15 and 2026-07-29. User requested Plausible Analytics and BookStack for Pirate Campaign wiki. These are Docker Compose recipes for common self-hosted services on Proxmox LXCs.

---

## 1. Wiki.js (Alternative to BookStack)

Wiki.js is a knowledge base with Markdown-native editing, graph view, and comments. Good for technical documentation and structured wiki projects.

### Prerequisites
- Debian/Ubuntu CT with `nesting=1` (Docker support)
- TUN device if using Tailscale (see `references/proxmox-ct-tailscale-pihole.md`)

### Docker Compose

```bash
mkdir -p ~/wikijs && cd ~/wikijs

cat > docker-compose.yml << 'EOF'
version: "3.8"
services:
  wikijs_db:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: wiki
      POSTGRES_USER: wikijs
      POSTGRES_PASSWORD: changeme
    volumes:
      - db-data:/var/lib/postgresql/data

  wikijs:
    image: ghcr.io/requarks/wiki:2
    ports:
      - "3000:3000"
    depends_on:
      - wikijs_db
    environment:
      DB_TYPE: postgres
      DB_HOST: wikijs_db
      DB_PORT: 5432
      DB_USER: wikijs
      DB_PASS: changeme
      DB_NAME: wiki
EOF

docker compose up -d
```

**First-time setup:** Visit `http://ct-ip:3000` and complete the setup wizard. The admin password is set during setup.

**Tailscale access:** After `tailscale up`, the wiki is reachable at `http://100.x.x.x:3000` from any tailnet device.

---

## 2. Plausible Analytics (Full Compose with HTTPS)

Same as the concise version in SKILL.md Section 3.3, but with reverse proxy ready.

```bash
# Option A: Tailscale Funnel (no reverse proxy needed)
tailscale funnel --bg 8000

# Option B: Caddy reverse proxy on the host or CT
# Install Caddy: https://caddyserver.com/docs/install
cat > Caddyfile << 'EOF'
analytics.yourdomain.com {
    reverse_proxy localhost:8000
}
EOF
caddy run
```

---

## 3. BookStack — Working Deployment

**Pitfall:** The LinuxServer BookStack image generates a default `.env` file inside `/config/www/.env` on first run. If the DB env vars are wrong in that generated file, the container crashes with SQLSTATE access-denied errors **even if** the docker-compose environment section looks correct.

### Prerequisites
- Debian 12 CT with `nesting=1` (Docker support)
- 2GB RAM minimum, 4GB recommended
- TUN device if using Tailscale

### Generate APP_KEY first

BookStack **requires** a valid `APP_KEY`. Generate it from the image itself before deploying:

```bash
APP_KEY=$(docker run --rm --entrypoint /bin/bash lscr.io/linuxserver/bookstack:latest appkey)
echo "APP_KEY=$APP_KEY"
```

### Docker Compose (production-ready)

```bash
mkdir -p /opt/bookstack && cd /opt/bookstack

cat > docker-compose.yml << EOF
version: "3.8"
services:
  bookstack:
    image: lscr.io/linuxserver/bookstack:latest
    container_name: bookstack
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - APP_URL=http://localhost:6875
      - APP_KEY=${APP_KEY}
      - DB_HOST=bookstack-db
      - DB_PORT=3306
      - DB_USER=bookstack
      - DB_PASS=bookstack_db_pass
      - DB_DATABASE=bookstack
    volumes:
      - bookstack-data:/config
    ports:
      - "127.0.0.1:6875:80"
    depends_on:
      - bookstack-db
    networks:
      - bookstack

  bookstack-db:
    image: lscr.io/linuxserver/mariadb:latest
    container_name: bookstack-db
    restart: unless-stopped
    environment:
      - PUID=1000
      - PGID=1000
      - MYSQL_ROOT_PASSWORD=bookstack_root_pass
      - MYSQL_DATABASE=bookstack
      - MYSQL_USER=bookstack
      - MYSQL_PASSWORD=bookstack_db_pass
    volumes:
      - bookstack-db-data:/config
    networks:
      - bookstack

volumes:
  bookstack-data:
  bookstack-db-data:

networks:
  bookstack:
    driver: bridge
EOF

docker compose up -d
```

### Post-deploy fixes (common first-run issues)

**Issue 1 — Database credentials mismatch:**
The LinuxServer BookStack image creates a **default `.env` file inside `/config/www/.env` on first run**. That file contains hardcoded default credentials (`database_username` / `database_user_password`). Even though docker-compose passes the correct `DB_USER` and `DB_PASS`, the **container reads the generated `.env` first** and crashes with:

```
SQLSTATE[HY000] [1045] Access denied for user 'database_username'@...
```

**Fix:** Write the correct credentials into the container's `.env`:
```bash
docker exec bookstack bash -c 'cat > /config/www/.env << ENDFILE
APP_KEY=<paste your APP_KEY here>
APP_URL=http://localhost:6875
DB_HOST=bookstack-db
DB_PORT=3306
DB_DATABASE=bookstack
DB_USERNAME=bookstack
DB_PASSWORD=bookstack_db_pass
ENDFILE'
docker restart bookstack
```

**Issue 2 — Broken page styling after enabling Tailscale Funnel:**
If you expose BookStack via Tailscale Funnel or a reverse proxy but `APP_URL` remains `http://localhost:6875`, browsers load the HTML then try to fetch CSS/JS from `http://localhost:6875` on the user's own machine. The result is an unstyled page with "refused to connect" errors.
**Fix:**
```bash
# Update both docker-compose.yml and the live .env for the change to persist across restarts
sed -i 's|APP_URL=.*|APP_URL=https://bookstack.tailXXXXX.ts.net|' /opt/bookstack/docker-compose.yml
docker exec bookstack sed -i 's|APP_URL=.*|APP_URL=https://bookstack.tailXXXXX.ts.net|' /config/www/.env
docker compose down && docker compose up -d
```

### Bulk content import via API

When migrating from Obsidian (or any Markdown source), the BookStack REST API is safer than direct database writes.

**Create an API token:**
1. In BookStack, go to Settings -> API Access -> Create Token
2. Note the `Token ID` and `Token Secret`

**API endpoint pattern:**
```bash
curl -H "Authorization: Token <ID>:<SECRET>" https://bookstack.tailXXXXX.ts.net/api/books
```

**Import workflow:**
1. Create a Shelf via `POST /api/shelves`
2. Create Books via `POST /api/books`
3. Create Pages via `POST /api/pages` with `book_id` and `html` content
4. Tag secret pages with `dm-only` so they can be restricted in the UI later

**Pitfall — BookStack uses a polymorphic `entities` table.** There are no separate `books`, `shelves`, `chapters`, or `pages` tables in the schema. Instead, everything is stored in `entities` with a `type` column (`book`, `shelf`, `chapter`, `page`). The API abstracts this; direct SQL writes require understanding the entity system and are not recommended.

**Pitfall — Markdown must be converted to HTML.** BookStack stores HTML in `entity_page_data.html`. A basic converter covering headers, bold/italic, lists, tables, links, and blockquotes is sufficient for most vault content. Obsidian-specific features (transclusions, callouts, embedded images) require manual cleanup.

A working import script is available at `scripts/import-obsidian-to-bookstack.py`.

### First login and immediate change

- Default user: `admin@admin.com`
- Default password: `password`
- **Change the password immediately** after first login.

---

## 4. Quick-Start Checklist for New CTs

1. `pct create <id> ... --features nesting=1`
2. Inside CT: `curl -fsSL https://get.docker.com | sh`
3. Optional: Install Tailscale (remember TUN device)
4. `mkdir -p ~/service && cd ~/service`
5. Create `docker-compose.yml`
6. `docker compose up -d`
7. Point reverse proxy or Tailscale Funnel to the exposed port
8. Complete first-time setup via web UI
9. Change default passwords immediately

---

## 5. CT Resource Guidelines

| Service | Memory | Disk | CPU | Notes |
|---------|--------|------|-----|-------|
| Pi-hole | 512MB | 8GB | 1 | Very light, DNS only |
| BookStack | 1GB | 16GB | 1 | MariaDB needs some RAM |
| Wiki.js | 1GB | 16GB | 1 | Postgres + Node.js |
| Plausible | 2GB | 20GB | 1 | ClickHouse + Postgres |
| Jellyfin | 2GB | varies | 2 | Transcoding needs more CPU |
| Vaultwarden | 512MB | 4GB | 1 | Very light Rust binary |

Adjust `--memory`, `--rootfs`, and `--cores` accordingly in `pct create`.

## Related

- `proxmox-ve` skill for CT lifecycle and storage discovery
- `media-streaming` skill for Jellyfin-specific operations
- `references/proxmox-ct-tailscale-pihole.md` for TUN device configuration
