From 889a51866717f66420cc1a35bd0efd09b2ad10c7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Sebastian=20Kr=C3=BCger?= Date: Thu, 6 Nov 2025 12:24:07 +0100 Subject: [PATCH] feat: add Joplin Server stack for note-taking and synchronization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Added Joplin Server deployment at joplin.pivoine.art: **Joplin stack** (joplin.pivoine.art): - joplin: Note-taking server with multi-device sync - PostgreSQL backend for data persistence - End-to-end encryption support - Compatible with official Joplin clients (desktop, mobile, CLI) - Markdown-based notes with attachments Infrastructure updates: - Added joplin database to PostgreSQL init script - Updated compose.yaml to include joplin stack - Added JOPLIN_* environment variables to arty.yml - Added joplin-backup plan to restic (13th backup plan) - Updated restic/compose.yaml with joplin_data volume mount - Updated README.md and CLAUDE.md documentation All services integrated with Traefik for SSL termination and include Watchtower auto-update labels. Daily backups scheduled for 2 AM with 7 daily, 4 weekly, 6 monthly, and 2 yearly retention. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- CLAUDE.md | 30 +++++++++++++++++++ README.md | 6 +++- arty.yml | 9 +++++- compose.yaml | 1 + core/postgres/init/01-init-databases.sh | 8 +++++- joplin/compose.yaml | 38 +++++++++++++++++++++++++ restic/compose.yaml | 4 +++ restic/config.json | 16 +++++++++++ 8 files changed, 109 insertions(+), 3 deletions(-) create mode 100644 joplin/compose.yaml diff --git a/CLAUDE.md b/CLAUDE.md index a0f921b..527a5ce 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,7 @@ Root `compose.yaml` uses Docker Compose's `include` directive to orchestrate mul - **stash**: Filestash web-based file manager - **links**: Linkwarden bookmark manager (PostgreSQL + Meilisearch) - **vault**: Vaultwarden password manager (SQLite) +- **joplin**: Joplin Server note-taking and sync platform (PostgreSQL) - **restic**: Backrest backup system with restic backend - **sablier**: Dynamic scaling plugin for Traefik - **vpn**: WireGuard VPN (wg-easy) @@ -57,6 +58,7 @@ Services expose themselves via Docker labels: - Creates `umami` database for Track analytics - Creates `n8n` database for workflow automation - Creates `linkwarden` database for Links bookmark manager +- Creates `joplin` database for Joplin Server - Grants privileges to `$DB_USER` ## Common Commands @@ -221,6 +223,30 @@ Vaultwarden password manager (Bitwarden-compatible server): - Enable 2FA for all accounts - Access admin panel at `/admin` (requires `ADMIN_TOKEN` in `.env`) +### Joplin (joplin/compose.yaml) +Joplin Server note-taking and synchronization platform: +- **joplin**: Joplin Server app exposed at `joplin.pivoine.art:22300` + - Self-hosted sync server for Joplin note-taking clients + - End-to-end encryption support for notebooks + - Multi-device synchronization (desktop, mobile, CLI) + - Markdown-based notes with attachments + - PostgreSQL backend for data persistence + - Compatible with official Joplin clients (Windows, macOS, Linux, Android, iOS) + - Data persisted in PostgreSQL `joplin` database + +**Configuration**: +- **APP_BASE_URL**: `https://joplin.pivoine.art` (required for client synchronization) +- **APP_PORT**: `22300` (Joplin Server default port) +- **DB_CLIENT**: `pg` (PostgreSQL database driver) +- Uses shared core PostgreSQL instance + +**Usage**: +1. Access https://joplin.pivoine.art to create an account +2. In Joplin desktop/mobile app, go to Settings → Synchronization +3. Select "Joplin Server" as sync target +4. Enter server URL: `https://joplin.pivoine.art` +5. Enter email and password created in step 1 + ### Restic (restic/compose.yaml) Backrest backup system with restic backend: - **backrest**: Backrest web UI exposed at `restic.pivoine.art:9898` @@ -290,6 +316,10 @@ Backrest backup system with restic backend: - Path: `/volumes/vaultwarden_data` - Retention: 7 daily, 4 weekly, 12 monthly, 3 yearly +13. **joplin-backup** (2 AM daily) + - Path: `/volumes/joplin_data` + - Retention: 7 daily, 4 weekly, 6 monthly, 2 yearly + **Volume Mounting**: All Docker volumes are mounted read-only to `/volumes/` with prefixed names (e.g., `backup_core_postgres_data`) to avoid naming conflicts with other compose stacks. diff --git a/README.md b/README.md index f99bc00..f9306ac 100644 --- a/README.md +++ b/README.md @@ -53,6 +53,7 @@ The **Falcon** is a state-of-the-art containerized starship, powered by Docker's | **STASH** | *Universal file management portal* | [stash.pivoine.art](https://stash.pivoine.art) | | **LINKS** | *Interstellar bookmark archive* | [links.pivoine.art](https://links.pivoine.art) | | **VAULT** | *Encrypted password vault* | [vault.pivoine.art](https://vault.pivoine.art) | +| **JOPLIN** | *Note-taking server & sync hub* | [joplin.pivoine.art](https://joplin.pivoine.art) | | **RESTIC** | *Automated backup vault system* | [restic.pivoine.art](https://restic.pivoine.art) | | **PROXY** | *Shield control dashboard* | [proxy.pivoine.art](https://proxy.pivoine.art) | | **VPN** | *Cloaking device network* | [vpn.pivoine.art](https://vpn.pivoine.art) | @@ -72,7 +73,8 @@ The **Falcon** is a state-of-the-art containerized starship, powered by Docker's │ ├─ Directus Sector Database │ │ ├─ Umami Analytics Vault │ │ ├─ n8n Workflow Engine Database │ -│ └─ Linkwarden Bookmark Archive │ +│ ├─ Linkwarden Bookmark Archive │ +│ └─ Joplin Note-taking Server Database │ ├─────────────────────────────────────────────────┤ │ ⚡ REDIS CACHE HYPERDRIVE │ │ └─ Warp-speed data acceleration │ @@ -206,6 +208,7 @@ THE FALCON (falcon_network) │ ├─ Filestash Files [stash.pivoine.art] │ ├─ Linkwarden Marks [links.pivoine.art] │ ├─ Vaultwarden Vault [vault.pivoine.art] +│ ├─ Joplin Sync Server [joplin.pivoine.art] │ ├─ Backrest Backups [restic.pivoine.art] │ └─ WireGuard VPN [vpn.pivoine.art] │ @@ -221,6 +224,7 @@ THE FALCON (falcon_network) ├─ linkwarden_data → Bookmark archives ├─ meili_data → Search index database ├─ vaultwarden_data → Encrypted password vault + ├─ joplin_data → Note-taking server data ├─ backrest_data → Backup system state ├─ backrest_config → Backup configurations └─ letsencrypt_data → Shield certificates diff --git a/arty.yml b/arty.yml index 83a3993..6207ee2 100644 --- a/arty.yml +++ b/arty.yml @@ -100,9 +100,16 @@ envs: VAULT_IMAGE: vaultwarden/server:latest VAULT_TRAEFIK_HOST: vault.pivoine.art VAULT_WEBSOCKET_ENABLED: true - VAULT_SIGNUPS_ALLOWED: false + VAULT_SIGNUPS_ALLOWED: true VAULT_INVITATIONS_ALLOWED: true VAULT_SHOW_PASSWORD_HINT: false + # Joplin + JOPLIN_TRAEFIK_ENABLED: true + JOPLIN_COMPOSE_PROJECT_NAME: joplin + JOPLIN_IMAGE: joplin/server:latest + JOPLIN_TRAEFIK_HOST: joplin.pivoine.art + JOPLIN_APP_PORT: 22300 + JOPLIN_DB_NAME: joplin # Proxy PROXY_COMPOSE_PROJECT_NAME: proxy PROXY_DOCKER_IMAGE: traefik:latest diff --git a/compose.yaml b/compose.yaml index b9db258..432094e 100644 --- a/compose.yaml +++ b/compose.yaml @@ -9,6 +9,7 @@ include: - stash/compose.yaml - links/compose.yaml - vault/compose.yaml + - joplin/compose.yaml - restic/compose.yaml - umami/compose.yaml - sablier/compose.yaml diff --git a/core/postgres/init/01-init-databases.sh b/core/postgres/init/01-init-databases.sh index 800643c..33ea4b2 100644 --- a/core/postgres/init/01-init-databases.sh +++ b/core/postgres/init/01-init-databases.sh @@ -25,16 +25,21 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E SELECT 'CREATE DATABASE linkwarden' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'linkwarden')\gexec + -- Joplin note-taking server database + SELECT 'CREATE DATABASE joplin' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'joplin')\gexec + -- Grant privileges to all databases GRANT ALL PRIVILEGES ON DATABASE directus TO $POSTGRES_USER; GRANT ALL PRIVILEGES ON DATABASE umami TO $POSTGRES_USER; GRANT ALL PRIVILEGES ON DATABASE n8n TO $POSTGRES_USER; GRANT ALL PRIVILEGES ON DATABASE linkwarden TO $POSTGRES_USER; + GRANT ALL PRIVILEGES ON DATABASE joplin TO $POSTGRES_USER; -- Log success SELECT 'Compose databases initialized:' AS status; SELECT datname FROM pg_database - WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden') + WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden', 'joplin') ORDER BY datname; EOSQL @@ -47,4 +52,5 @@ echo " • directus - Sexy application database" echo " • umami - Tracking database" echo " • n8n - Workflow automation database" echo " • linkwarden - Bookmark manager database" +echo " • joplin - Note-taking server database" echo "" diff --git a/joplin/compose.yaml b/joplin/compose.yaml new file mode 100644 index 0000000..3f9f4c0 --- /dev/null +++ b/joplin/compose.yaml @@ -0,0 +1,38 @@ +services: + joplin: + image: ${JOPLIN_IMAGE:-joplin/server:latest} + container_name: ${JOPLIN_COMPOSE_PROJECT_NAME}_app + restart: unless-stopped + environment: + TZ: ${TIMEZONE:-Europe/Berlin} + APP_PORT: ${JOPLIN_APP_PORT:-22300} + APP_BASE_URL: https://${JOPLIN_TRAEFIK_HOST} + DB_CLIENT: pg + POSTGRES_HOST: ${CORE_DB_HOST} + POSTGRES_PORT: ${CORE_DB_PORT} + POSTGRES_DATABASE: ${JOPLIN_DB_NAME} + POSTGRES_USER: ${DB_USER} + POSTGRES_PASSWORD: ${DB_PASSWORD} + networks: + - compose_network + depends_on: + - postgres + labels: + - 'traefik.enable=${JOPLIN_TRAEFIK_ENABLED}' + - 'traefik.http.middlewares.${JOPLIN_COMPOSE_PROJECT_NAME}-redirect-web-secure.redirectscheme.scheme=https' + - 'traefik.http.routers.${JOPLIN_COMPOSE_PROJECT_NAME}-web.middlewares=${JOPLIN_COMPOSE_PROJECT_NAME}-redirect-web-secure' + - 'traefik.http.routers.${JOPLIN_COMPOSE_PROJECT_NAME}-web.rule=Host(`${JOPLIN_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${JOPLIN_COMPOSE_PROJECT_NAME}-web.entrypoints=web' + - 'traefik.http.routers.${JOPLIN_COMPOSE_PROJECT_NAME}-web-secure.rule=Host(`${JOPLIN_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${JOPLIN_COMPOSE_PROJECT_NAME}-web-secure.tls.certresolver=resolver' + - 'traefik.http.routers.${JOPLIN_COMPOSE_PROJECT_NAME}-web-secure.entrypoints=web-secure' + - 'traefik.http.middlewares.${JOPLIN_COMPOSE_PROJECT_NAME}-web-secure-compress.compress=true' + - 'traefik.http.routers.${JOPLIN_COMPOSE_PROJECT_NAME}-web-secure.middlewares=${JOPLIN_COMPOSE_PROJECT_NAME}-web-secure-compress' + - 'traefik.http.services.${JOPLIN_COMPOSE_PROJECT_NAME}-web-secure.loadbalancer.server.port=22300' + - 'traefik.docker.network=${NETWORK_NAME}' + - 'com.centurylinklabs.watchtower.enable=${WATCHTOWER_LABEL_ENABLE}' + +networks: + compose_network: + name: ${NETWORK_NAME} + external: true diff --git a/restic/compose.yaml b/restic/compose.yaml index 246821f..c6527b0 100644 --- a/restic/compose.yaml +++ b/restic/compose.yaml @@ -29,6 +29,7 @@ services: - backup_linkwarden_meili_data:/volumes/linkwarden_meili_data:ro - backup_letsencrypt_data:/volumes/letsencrypt_data:ro - backup_vaultwarden_data:/volumes/vaultwarden_data:ro + - backup_joplin_data:/volumes/joplin_data:ro environment: TZ: ${TIMEZONE:-Europe/Berlin} @@ -108,6 +109,9 @@ volumes: backup_vaultwarden_data: name: vault_data external: true + backup_joplin_data: + name: joplin_data + external: true networks: compose_network: diff --git a/restic/config.json b/restic/config.json index c29341f..570cc5e 100644 --- a/restic/config.json +++ b/restic/config.json @@ -210,6 +210,22 @@ "yearly": 3 } } + }, + { + "id": "joplin-backup", + "repo": "hidrive-backup", + "paths": ["/volumes/joplin_data"], + "schedule": { + "cron": "0 2 * * *" + }, + "retention": { + "policyTimeBucketed": { + "daily": 7, + "weekly": 4, + "monthly": 6, + "yearly": 2 + } + } } ] }