diff --git a/CLAUDE.md b/CLAUDE.md index 4ef8cc1..37c40b2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -19,6 +19,7 @@ Root `compose.yaml` uses Docker Compose's `include` directive to orchestrate mul - **scrapy**: Scrapyd web scraping cluster (scrapyd, scrapy, scrapyrt) - **n8n**: Workflow automation platform (PostgreSQL) - **stash**: Filestash web-based file manager +- **links**: Linkwarden bookmark manager (PostgreSQL + Meilisearch) - **vpn**: WireGuard VPN (wg-easy) 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 `umami` database for Track analytics - Creates `n8n` database for workflow automation +- Creates `linkwarden` database for Links bookmark manager - Grants privileges to `$DB_USER` ## Common Commands @@ -175,6 +177,24 @@ Web-based file manager: - File sharing capabilities - 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 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 - `n8n_n8n_data`: n8n workflow data - `stash_filestash_data`: Filestash configuration and state +- `links_data`, `links_meili_data`: Linkwarden bookmarks and Meilisearch index - `proxy_letsencrypt_data`: SSL certificates Volumes can be inspected with: diff --git a/README.md b/README.md index a321758..fe7668f 100644 --- a/README.md +++ b/README.md @@ -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) | | **N8N** | *Automated workflow command center* | [n8n.pivoine.art](https://n8n.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) | ### ⚙️ INFRASTRUCTURE @@ -65,7 +66,8 @@ The **Falcon** is a state-of-the-art containerized starship, powered by Docker's │ 💾 POSTGRESQL 16 DATA CORE │ │ ├─ Directus Sector Database │ │ ├─ Umami Analytics Vault │ -│ └─ n8n Workflow Engine Database │ +│ ├─ n8n Workflow Engine Database │ +│ └─ Linkwarden Bookmark Archive │ ├─────────────────────────────────────────────────┤ │ ⚡ REDIS CACHE HYPERDRIVE │ │ └─ Warp-speed data acceleration │ @@ -164,6 +166,7 @@ THE FALCON (falcon_network) │ ├─ Scrapyd Cluster [scrapy.pivoine.art] │ ├─ n8n Workflows [n8n.pivoine.art] │ ├─ Filestash Files [stash.pivoine.art] +│ ├─ Linkwarden Marks [links.pivoine.art] │ └─ WireGuard VPN [vpn.pivoine.art] │ └─ 💾 STORAGE VOLUMES @@ -175,6 +178,8 @@ THE FALCON (falcon_network) ├─ scrapy_code → Spider project code ├─ n8n_data → Workflow configurations ├─ filestash_data → File manager state + ├─ linkwarden_data → Bookmark archives + ├─ meili_data → Search index database └─ letsencrypt_data → Shield certificates ``` diff --git a/arty.yml b/arty.yml index e4c5783..8646694 100644 --- a/arty.yml +++ b/arty.yml @@ -79,6 +79,14 @@ envs: STASH_TRAEFIK_HOST: stash.pivoine.art STASH_PORT: 8334 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_COMPOSE_PROJECT_NAME: proxy PROXY_DOCKER_IMAGE: traefik:latest diff --git a/compose.yaml b/compose.yaml index fa829b3..dc8d5ff 100644 --- a/compose.yaml +++ b/compose.yaml @@ -7,6 +7,7 @@ include: - scrapy/compose.yaml - n8n/compose.yaml - stash/compose.yaml + - links/compose.yaml - umami/compose.yaml - proxy/compose.yaml - watch/compose.yaml diff --git a/core/postgres/init/01-init-databases.sh b/core/postgres/init/01-init-databases.sh index 6b7e78a..800643c 100644 --- a/core/postgres/init/01-init-databases.sh +++ b/core/postgres/init/01-init-databases.sh @@ -21,15 +21,20 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E SELECT 'CREATE DATABASE n8n' 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 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; -- Log success SELECT 'Compose databases initialized:' AS status; SELECT datname FROM pg_database - WHERE datname IN ('directus', 'umami', 'n8n') + WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden') ORDER BY datname; EOSQL @@ -41,4 +46,5 @@ echo "Databases available:" echo " • directus - Sexy application database" echo " • umami - Tracking database" echo " • n8n - Workflow automation database" +echo " • linkwarden - Bookmark manager database" echo "" diff --git a/links/compose.yaml b/links/compose.yaml new file mode 100644 index 0000000..4df93f8 --- /dev/null +++ b/links/compose.yaml @@ -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