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:
21
CLAUDE.md
21
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)
|
- **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:
|
||||||
|
|||||||
@@ -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
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
8
arty.yml
8
arty.yml
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
50
links/compose.yaml
Normal 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
|
||||||
Reference in New Issue
Block a user