feat: add Linkwarden bookmark manager stack

Added new Links stack to Falcon infrastructure:

**Links Stack (links.pivoine.art):**
- Linkwarden bookmark manager with PostgreSQL backend
- Meilisearch v1.12.8 for full-text search
- Browser extension support
- Screenshot and PDF archiving
- Collaborative bookmark sharing

**Infrastructure Updates:**
- Created links/compose.yaml with linkwarden and meilisearch services
- Added linkwarden database to PostgreSQL init script
- Added LINKS_* environment variables to arty.yml
- Updated compose.yaml to include links stack
- Cleaned up .env to contain only secrets
- Added all EMAIL_* variables to .env

**Documentation:**
- Updated CLAUDE.md with Links service details
- Updated README.md with Links in CORE SYSTEMS table
- Added linkwarden_data and meili_data volumes to docs

**Required Secrets (in .env):**
- LINKS_NEXTAUTH_SECRET: NextAuth.js session encryption
- LINKS_MEILI_MASTER_KEY: Meilisearch API authentication

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-11-06 07:49:49 +01:00
parent 0ffab37e99
commit f5728b5f9c
6 changed files with 93 additions and 2 deletions

View File

@@ -19,6 +19,7 @@ Root `compose.yaml` uses Docker Compose's `include` directive to orchestrate mul
- **scrapy**: Scrapyd web scraping cluster (scrapyd, scrapy, scrapyrt) - **scrapy**: Scrapyd web scraping cluster (scrapyd, scrapy, scrapyrt)
- **n8n**: Workflow automation platform (PostgreSQL) - **n8n**: Workflow automation platform (PostgreSQL)
- **stash**: Filestash web-based file manager - **stash**: Filestash web-based file manager
- **links**: Linkwarden bookmark manager (PostgreSQL + Meilisearch)
- **vpn**: WireGuard VPN (wg-easy) - **vpn**: WireGuard VPN (wg-easy)
All services connect to a single external Docker network (`falcon_network` by default, defined by `$NETWORK_NAME`). All services connect to a single external Docker network (`falcon_network` by default, defined by `$NETWORK_NAME`).
@@ -52,6 +53,7 @@ Services expose themselves via Docker labels:
- Creates `directus` database for Sexy CMS - Creates `directus` database for Sexy CMS
- Creates `umami` database for Track analytics - Creates `umami` database for Track analytics
- Creates `n8n` database for workflow automation - Creates `n8n` database for workflow automation
- Creates `linkwarden` database for Links bookmark manager
- Grants privileges to `$DB_USER` - Grants privileges to `$DB_USER`
## Common Commands ## Common Commands
@@ -175,6 +177,24 @@ Web-based file manager:
- File sharing capabilities - File sharing capabilities
- Data persisted in `filestash_data` volume - Data persisted in `filestash_data` volume
### Links (links/compose.yaml)
Linkwarden bookmark manager with full-text search:
- **linkwarden**: Linkwarden app exposed at `links.pivoine.art:3000`
- Bookmark and link management with collections
- Full-text search via Meilisearch
- Collaborative bookmark sharing
- Screenshot and PDF archiving
- Browser extension support
- PostgreSQL backend for bookmark persistence
- Data persisted in `linkwarden_data` volume
- **linkwarden_meilisearch**: Meilisearch v1.12.8 search engine
- Powers full-text search for bookmarks
- Data persisted in `linkwarden_meili_data` volume
**Required Environment Variables** (add to `.env`):
- `LINKS_NEXTAUTH_SECRET`: NextAuth.js secret for session encryption
- `LINKS_MEILI_MASTER_KEY`: Meilisearch master key for API authentication
## Important Environment Variables ## Important Environment Variables
Key variables defined in `arty.yml` and overridden in `.env`: Key variables defined in `arty.yml` and overridden in `.env`:
@@ -197,6 +217,7 @@ Each service uses named volumes prefixed with project name:
- `scrapy_scrapyd_data`, `scrapy_scrapy_code`: Scrapy spider data and code - `scrapy_scrapyd_data`, `scrapy_scrapy_code`: Scrapy spider data and code
- `n8n_n8n_data`: n8n workflow data - `n8n_n8n_data`: n8n workflow data
- `stash_filestash_data`: Filestash configuration and state - `stash_filestash_data`: Filestash configuration and state
- `links_data`, `links_meili_data`: Linkwarden bookmarks and Meilisearch index
- `proxy_letsencrypt_data`: SSL certificates - `proxy_letsencrypt_data`: SSL certificates
Volumes can be inspected with: Volumes can be inspected with:

View File

@@ -51,6 +51,7 @@ The **Falcon** is a state-of-the-art containerized starship, powered by Docker's
| **SCRAPY** | *Web scraping reconnaissance cluster* | [scrapy.pivoine.art](https://scrapy.pivoine.art) | | **SCRAPY** | *Web scraping reconnaissance cluster* | [scrapy.pivoine.art](https://scrapy.pivoine.art) |
| **N8N** | *Automated workflow command center* | [n8n.pivoine.art](https://n8n.pivoine.art) | | **N8N** | *Automated workflow command center* | [n8n.pivoine.art](https://n8n.pivoine.art) |
| **STASH** | *Universal file management portal* | [stash.pivoine.art](https://stash.pivoine.art) | | **STASH** | *Universal file management portal* | [stash.pivoine.art](https://stash.pivoine.art) |
| **LINKS** | *Interstellar bookmark archive* | [links.pivoine.art](https://links.pivoine.art) |
| **VPN** | *Cloaking device network* | [vpn.pivoine.art](https://vpn.pivoine.art) | | **VPN** | *Cloaking device network* | [vpn.pivoine.art](https://vpn.pivoine.art) |
### ⚙️ INFRASTRUCTURE ### ⚙️ INFRASTRUCTURE
@@ -65,7 +66,8 @@ The **Falcon** is a state-of-the-art containerized starship, powered by Docker's
│ 💾 POSTGRESQL 16 DATA CORE │ │ 💾 POSTGRESQL 16 DATA CORE │
│ ├─ Directus Sector Database │ │ ├─ Directus Sector Database │
│ ├─ Umami Analytics Vault │ │ ├─ Umami Analytics Vault │
─ n8n Workflow Engine Database │ ─ n8n Workflow Engine Database │
│ └─ Linkwarden Bookmark Archive │
├─────────────────────────────────────────────────┤ ├─────────────────────────────────────────────────┤
│ ⚡ REDIS CACHE HYPERDRIVE │ │ ⚡ REDIS CACHE HYPERDRIVE │
│ └─ Warp-speed data acceleration │ │ └─ Warp-speed data acceleration │
@@ -164,6 +166,7 @@ THE FALCON (falcon_network)
│ ├─ Scrapyd Cluster [scrapy.pivoine.art] │ ├─ Scrapyd Cluster [scrapy.pivoine.art]
│ ├─ n8n Workflows [n8n.pivoine.art] │ ├─ n8n Workflows [n8n.pivoine.art]
│ ├─ Filestash Files [stash.pivoine.art] │ ├─ Filestash Files [stash.pivoine.art]
│ ├─ Linkwarden Marks [links.pivoine.art]
│ └─ WireGuard VPN [vpn.pivoine.art] │ └─ WireGuard VPN [vpn.pivoine.art]
└─ 💾 STORAGE VOLUMES └─ 💾 STORAGE VOLUMES
@@ -175,6 +178,8 @@ THE FALCON (falcon_network)
├─ scrapy_code → Spider project code ├─ scrapy_code → Spider project code
├─ n8n_data → Workflow configurations ├─ n8n_data → Workflow configurations
├─ filestash_data → File manager state ├─ filestash_data → File manager state
├─ linkwarden_data → Bookmark archives
├─ meili_data → Search index database
└─ letsencrypt_data → Shield certificates └─ letsencrypt_data → Shield certificates
``` ```

View File

@@ -79,6 +79,14 @@ envs:
STASH_TRAEFIK_HOST: stash.pivoine.art STASH_TRAEFIK_HOST: stash.pivoine.art
STASH_PORT: 8334 STASH_PORT: 8334
STASH_CANARY: true STASH_CANARY: true
# Linkwarden
LINKS_TRAEFIK_ENABLED: true
LINKS_COMPOSE_PROJECT_NAME: links
LINKS_DOCKER_IMAGE: ghcr.io/linkwarden/linkwarden:latest
LINKS_TRAEFIK_HOST: links.pivoine.art
LINKS_DB_NAME: linkwarden
LINKS_MEILI_IMAGE: getmeili/meilisearch:v1.12.8
LINKS_MEILI_NO_ANALYTICS: true
# Proxy # Proxy
PROXY_COMPOSE_PROJECT_NAME: proxy PROXY_COMPOSE_PROJECT_NAME: proxy
PROXY_DOCKER_IMAGE: traefik:latest PROXY_DOCKER_IMAGE: traefik:latest

View File

@@ -7,6 +7,7 @@ include:
- scrapy/compose.yaml - scrapy/compose.yaml
- n8n/compose.yaml - n8n/compose.yaml
- stash/compose.yaml - stash/compose.yaml
- links/compose.yaml
- umami/compose.yaml - umami/compose.yaml
- proxy/compose.yaml - proxy/compose.yaml
- watch/compose.yaml - watch/compose.yaml

View File

@@ -21,15 +21,20 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E
SELECT 'CREATE DATABASE n8n' SELECT 'CREATE DATABASE n8n'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'n8n')\gexec WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'n8n')\gexec
-- Linkwarden bookmark manager database
SELECT 'CREATE DATABASE linkwarden'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'linkwarden')\gexec
-- Grant privileges to all databases -- Grant privileges to all databases
GRANT ALL PRIVILEGES ON DATABASE directus TO $POSTGRES_USER; GRANT ALL PRIVILEGES ON DATABASE directus TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE umami 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 n8n TO $POSTGRES_USER;
GRANT ALL PRIVILEGES ON DATABASE linkwarden TO $POSTGRES_USER;
-- Log success -- Log success
SELECT 'Compose databases initialized:' AS status; SELECT 'Compose databases initialized:' AS status;
SELECT datname FROM pg_database SELECT datname FROM pg_database
WHERE datname IN ('directus', 'umami', 'n8n') WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden')
ORDER BY datname; ORDER BY datname;
EOSQL EOSQL
@@ -41,4 +46,5 @@ echo "Databases available:"
echo " • directus - Sexy application database" echo " • directus - Sexy application database"
echo " • umami - Tracking database" echo " • umami - Tracking database"
echo " • n8n - Workflow automation database" echo " • n8n - Workflow automation database"
echo " • linkwarden - Bookmark manager database"
echo "" echo ""

50
links/compose.yaml Normal file
View File

@@ -0,0 +1,50 @@
services:
linkwarden:
image: ${LINKS_DOCKER_IMAGE}
container_name: ${LINKS_COMPOSE_PROJECT_NAME}_app
restart: unless-stopped
networks:
- compose_network
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
DATABASE_URL: postgresql://${DB_USER}:${DB_PASSWORD}@${CORE_DB_HOST}:${CORE_DB_PORT}/${LINKS_DB_NAME}
NEXTAUTH_SECRET: ${LINKS_NEXTAUTH_SECRET}
NEXTAUTH_URL: https://${LINKS_TRAEFIK_HOST}
MEILI_ADDR: http://linkwarden_meilisearch:7700
MEILI_MASTER_KEY: ${LINKS_MEILI_MASTER_KEY}
volumes:
- linkwarden_data:/data/data
depends_on:
- linkwarden_meilisearch
labels:
- 'traefik.enable=${LINKS_TRAEFIK_ENABLED:-true}'
- 'traefik.http.middlewares.${LINKS_COMPOSE_PROJECT_NAME}-redirect-web-secure.redirectscheme.scheme=https'
- 'traefik.http.routers.${LINKS_COMPOSE_PROJECT_NAME}-web.middlewares=${LINKS_COMPOSE_PROJECT_NAME}-redirect-web-secure'
- 'traefik.http.routers.${LINKS_COMPOSE_PROJECT_NAME}-web.rule=Host(`${LINKS_TRAEFIK_HOST}`)'
- 'traefik.http.routers.${LINKS_COMPOSE_PROJECT_NAME}-web.entrypoints=web'
- 'traefik.http.routers.${LINKS_COMPOSE_PROJECT_NAME}-web-secure.rule=Host(`${LINKS_TRAEFIK_HOST}`)'
- 'traefik.http.routers.${LINKS_COMPOSE_PROJECT_NAME}-web-secure.tls.certresolver=resolver'
- 'traefik.http.routers.${LINKS_COMPOSE_PROJECT_NAME}-web-secure.entrypoints=web-secure'
- 'traefik.http.middlewares.${LINKS_COMPOSE_PROJECT_NAME}-web-secure-compress.compress=true'
- 'traefik.http.routers.${LINKS_COMPOSE_PROJECT_NAME}-web-secure.middlewares=${LINKS_COMPOSE_PROJECT_NAME}-web-secure-compress'
- 'traefik.http.services.${LINKS_COMPOSE_PROJECT_NAME}-web-secure.loadbalancer.server.port=3000'
- 'traefik.docker.network=${NETWORK_NAME}'
- 'com.centurylinklabs.watchtower.enable=true'
linkwarden_meilisearch:
image: ${LINKS_MEILI_IMAGE}
container_name: ${LINKS_COMPOSE_PROJECT_NAME}_meilisearch
restart: unless-stopped
networks:
- compose_network
environment:
MEILI_MASTER_KEY: ${LINKS_MEILI_MASTER_KEY}
MEILI_NO_ANALYTICS: ${LINKS_MEILI_NO_ANALYTICS:-true}
volumes:
- linkwarden_meili_data:/meili_data
volumes:
linkwarden_data:
name: ${LINKS_COMPOSE_PROJECT_NAME}_data
linkwarden_meili_data:
name: ${LINKS_COMPOSE_PROJECT_NAME}_meili_data