diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..ea4a6d4 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,52 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this repo is + +Coolify stack definitions for LAIL — a French cooperative cloud infrastructure. Stacks are Docker Compose files deployed via Coolify (no local build/run tooling exists; deployment is done through the Coolify UI against the remote host). + +## Repository structure + +``` +stacks/ +├── lail-apps/ # Shared LAIL applications (one instance per service) +├── lail-infra/ # Infrastructure services (Authentik SSO, SFTPGo) +├── lail-sites/ # Website deployments +└── structures/ # Multi-tenant per-organization Nextcloud instances + └── _templates/ # Copy and replace XXXXXX placeholders for new orgs +``` + +## Docker Compose patterns used across stacks + +**Environment variables** — Coolify injects these automatically; reference them in compose files as-is: +- `SERVICE_URL_` — public URL for the service +- `SERVICE_USER_` / `SERVICE_PASSWORD_` — generated credentials +- `SERVICE_PASSWORD_64_` — 64-char password (used for Redis, long secrets) +- `SERVICE_HEX_32_` — 32-char hex value (used for encryption keys/secrets) + +**Data paths** — all persistent data lives on the host at: +- `/data/lail-apps//` — shared apps +- `/data/lail-structures//` — per-organization data + +**Database readiness** — always use `depends_on` with `condition: service_healthy` and define `healthcheck` on the DB service (`pg_isready` for Postgres, `redis-cli ping` for Redis). + +**Database version** — PostgreSQL 16-alpine is the standard. + +## SSO/OIDC + +Central identity provider is Authentik at `sso.lail.cloud`. When adding SSO to a new service, use its OIDC endpoints. Each app's README.md contains notes on SSO configuration status and any app-specific quirks. + +## Adding a new structure (multi-tenant org) + +1. Copy `stacks/structures/_templates/` into `stacks/structures//` +2. Replace every occurrence of `XXXXXX` with the org name +3. Update data paths: `/data/lail-structures/XXXXXX/` → `/data/lail-structures//` + +## Special requirements + +**Collabora** requires Docker host-level capabilities: `cap_add: [SYS_ADMIN]`, device `/dev/fuse`, and AppArmor profile `unconfined`. This cannot run in a constrained Docker environment. + +**Paheko** embeds its PHP config (`config.local.php`) as an inline file written via `docker-compose.yaml` — edits to PHP config go in the compose file itself, not a separate file. + +**N8N** uses a queue-mode architecture: separate `worker` and `task-runner` services alongside the main service, all sharing a Redis queue and Postgres DB. diff --git a/stacks/lail-apps/collabora/README.md b/stacks/lail-apps/lail-collabora/README.md similarity index 100% rename from stacks/lail-apps/collabora/README.md rename to stacks/lail-apps/lail-collabora/README.md diff --git a/stacks/lail-apps/lail-forgejo/README.md b/stacks/lail-apps/lail-forgejo/README.md new file mode 100644 index 0000000..0f05e7c --- /dev/null +++ b/stacks/lail-apps/lail-forgejo/README.md @@ -0,0 +1 @@ +- docker-compose sans SSO, défini à la main après installation \ No newline at end of file diff --git a/stacks/lail-apps/forgejo/docker-compose.yaml b/stacks/lail-apps/lail-forgejo/docker-compose.yaml similarity index 100% rename from stacks/lail-apps/forgejo/docker-compose.yaml rename to stacks/lail-apps/lail-forgejo/docker-compose.yaml diff --git a/stacks/lail-apps/lail-n8n/README.md b/stacks/lail-apps/lail-n8n/README.md new file mode 100644 index 0000000..82e83f6 --- /dev/null +++ b/stacks/lail-apps/lail-n8n/README.md @@ -0,0 +1 @@ +- Premier docker-compose sans SSO \ No newline at end of file diff --git a/stacks/lail-apps/lail-n8n/docker-compose.yml b/stacks/lail-apps/lail-n8n/docker-compose.yml new file mode 100644 index 0000000..958ce4f --- /dev/null +++ b/stacks/lail-apps/lail-n8n/docker-compose.yml @@ -0,0 +1,138 @@ +services: + n8n: + image: 'n8nio/n8n:2.10.4' + environment: + - SERVICE_URL_N8N_5678 + - 'N8N_EDITOR_BASE_URL=${SERVICE_URL_N8N}' + - 'WEBHOOK_URL=${SERVICE_URL_N8N}' + - 'N8N_HOST=${SERVICE_URL_N8N}' + - 'N8N_PROTOCOL=${N8N_PROTOCOL:-https}' + - 'GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-UTC}' + - 'TZ=${TZ:-UTC}' + - DB_TYPE=postgresdb + - 'DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}' + - DB_POSTGRESDB_HOST=postgresql + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_USER=$SERVICE_USER_POSTGRES + - DB_POSTGRESDB_SCHEMA=public + - DB_POSTGRESDB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - QUEUE_HEALTH_CHECK_ACTIVE=true + - 'N8N_ENCRYPTION_KEY=${SERVICE_PASSWORD_ENCRYPTION}' + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=external + - 'N8N_RUNNERS_BROKER_LISTEN_ADDRESS=${N8N_RUNNERS_BROKER_LISTEN_ADDRESS:-0.0.0.0}' + - 'N8N_RUNNERS_BROKER_PORT=${N8N_RUNNERS_BROKER_PORT:-5679}' + - N8N_RUNNERS_AUTH_TOKEN=$SERVICE_PASSWORD_N8N + - 'N8N_NATIVE_PYTHON_RUNNER=${N8N_NATIVE_PYTHON_RUNNER:-true}' + - 'N8N_RUNNERS_MAX_CONCURRENCY=${N8N_RUNNERS_MAX_CONCURRENCY:-5}' + - OFFLOAD_MANUAL_EXECUTIONS_TO_WORKERS=true + - 'N8N_BLOCK_ENV_ACCESS_IN_NODE=${N8N_BLOCK_ENV_ACCESS_IN_NODE:-true}' + - 'N8N_GIT_NODE_DISABLE_BARE_REPOS=${N8N_GIT_NODE_DISABLE_BARE_REPOS:-true}' + - 'N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=${N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS:-true}' + - 'N8N_PROXY_HOPS=${N8N_PROXY_HOPS:-1}' + - 'N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=${N8N_SKIP_AUTH_ON_OAUTH_CALLBACK:-false}' + volumes: + - '/data/lail-apps/lail-n8n/n8n-data:/home/node/.n8n' + depends_on: + postgresql: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: + - CMD-SHELL + - 'wget -qO- http://127.0.0.1:5678/' + interval: 5s + timeout: 20s + retries: 10 + n8n-worker: + image: 'n8nio/n8n:2.10.4' + command: worker + environment: + - 'GENERIC_TIMEZONE=${GENERIC_TIMEZONE:-UTC}' + - 'TZ=${TZ:-UTC}' + - DB_TYPE=postgresdb + - 'DB_POSTGRESDB_DATABASE=${POSTGRES_DB:-n8n}' + - DB_POSTGRESDB_HOST=postgresql + - DB_POSTGRESDB_PORT=5432 + - DB_POSTGRESDB_USER=$SERVICE_USER_POSTGRES + - DB_POSTGRESDB_SCHEMA=public + - DB_POSTGRESDB_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - EXECUTIONS_MODE=queue + - QUEUE_BULL_REDIS_HOST=redis + - QUEUE_HEALTH_CHECK_ACTIVE=true + - 'N8N_ENCRYPTION_KEY=${SERVICE_PASSWORD_ENCRYPTION}' + - N8N_RUNNERS_ENABLED=true + - N8N_RUNNERS_MODE=external + - 'N8N_RUNNERS_BROKER_LISTEN_ADDRESS=${N8N_RUNNERS_BROKER_LISTEN_ADDRESS:-0.0.0.0}' + - 'N8N_RUNNERS_BROKER_PORT=${N8N_RUNNERS_BROKER_PORT:-5679}' + - N8N_RUNNERS_AUTH_TOKEN=$SERVICE_PASSWORD_N8N + - 'N8N_NATIVE_PYTHON_RUNNER=${N8N_NATIVE_PYTHON_RUNNER:-true}' + - 'N8N_RUNNERS_MAX_CONCURRENCY=${N8N_RUNNERS_MAX_CONCURRENCY:-5}' + - 'N8N_BLOCK_ENV_ACCESS_IN_NODE=${N8N_BLOCK_ENV_ACCESS_IN_NODE:-true}' + - 'N8N_GIT_NODE_DISABLE_BARE_REPOS=${N8N_GIT_NODE_DISABLE_BARE_REPOS:-true}' + - 'N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS=${N8N_ENFORCE_SETTINGS_FILE_PERMISSIONS:-true}' + - 'N8N_PROXY_HOPS=${N8N_PROXY_HOPS:-1}' + - 'N8N_SKIP_AUTH_ON_OAUTH_CALLBACK=${N8N_SKIP_AUTH_ON_OAUTH_CALLBACK:-false}' + volumes: + - '/data/lail-apps/lail-n8n/n8n-data:/home/node/.n8n' + healthcheck: + test: + - CMD-SHELL + - 'wget -qO- http://127.0.0.1:5678/healthz' + interval: 5s + timeout: 20s + retries: 10 + depends_on: + n8n: + condition: service_healthy + postgresql: + condition: service_healthy + redis: + condition: service_healthy + postgresql: + image: 'postgres:16-alpine' + volumes: + - 'postgresql-data:/var/lib/postgresql/data' + environment: + - POSTGRES_USER=$SERVICE_USER_POSTGRES + - POSTGRES_PASSWORD=$SERVICE_PASSWORD_POSTGRES + - 'POSTGRES_DB=${POSTGRES_DB:-n8n}' + healthcheck: + test: + - CMD-SHELL + - 'pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}' + interval: 5s + timeout: 20s + retries: 10 + redis: + image: 'redis:6-alpine' + volumes: + - 'redis-data:/data' + healthcheck: + test: + - CMD + - redis-cli + - ping + interval: 5s + timeout: 5s + retries: 10 + task-runners: + image: 'n8nio/runners:2.10.4' + environment: + - 'N8N_RUNNERS_TASK_BROKER_URI=${N8N_RUNNERS_TASK_BROKER_URI:-http://n8n-worker:5679}' + - N8N_RUNNERS_AUTH_TOKEN=$SERVICE_PASSWORD_N8N + - 'N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT=${N8N_RUNNERS_AUTO_SHUTDOWN_TIMEOUT:-15}' + - 'N8N_RUNNERS_MAX_CONCURRENCY=${N8N_RUNNERS_MAX_CONCURRENCY:-5}' + depends_on: + - n8n + healthcheck: + test: + - CMD-SHELL + - 'wget -qO- http://127.0.0.1:5680/' + interval: 5s + timeout: 20s + retries: 10 + diff --git a/stacks/lail-apps/outline/README.md b/stacks/lail-apps/lail-outline/README.md similarity index 100% rename from stacks/lail-apps/outline/README.md rename to stacks/lail-apps/lail-outline/README.md diff --git a/stacks/lail-apps/outline/docker-compose.yaml b/stacks/lail-apps/lail-outline/docker-compose.yaml similarity index 100% rename from stacks/lail-apps/outline/docker-compose.yaml rename to stacks/lail-apps/lail-outline/docker-compose.yaml diff --git a/stacks/lail-apps/lail-paheko/README.md b/stacks/lail-apps/lail-paheko/README.md new file mode 100644 index 0000000..dc11a21 --- /dev/null +++ b/stacks/lail-apps/lail-paheko/README.md @@ -0,0 +1,3 @@ +- Impossible de le faire marcher correctement via les variables d'environnement +- Image Docker non maintenue par libretic (ancienne version) +- Image Docker "bololo/paheko" devrait fonctionner, mais pas de containers séparés... \ No newline at end of file diff --git a/stacks/lail-apps/paheko/config.local.php b/stacks/lail-apps/lail-paheko/config.local.php similarity index 100% rename from stacks/lail-apps/paheko/config.local.php rename to stacks/lail-apps/lail-paheko/config.local.php diff --git a/stacks/lail-apps/paheko/docker-compose.yaml b/stacks/lail-apps/lail-paheko/docker-compose.yaml similarity index 100% rename from stacks/lail-apps/paheko/docker-compose.yaml rename to stacks/lail-apps/lail-paheko/docker-compose.yaml diff --git a/stacks/structures/_template_nextcloud/docker-compose.yaml b/stacks/structures/_templates/nextcloud/docker-compose.yaml similarity index 100% rename from stacks/structures/_template_nextcloud/docker-compose.yaml rename to stacks/structures/_templates/nextcloud/docker-compose.yaml diff --git a/stacks/structures/structest/nextcloud/docker-compose.yaml b/stacks/structures/structest/nextcloud/docker-compose.yaml index ba20de9..a6e9d9d 100644 --- a/stacks/structures/structest/nextcloud/docker-compose.yaml +++ b/stacks/structures/structest/nextcloud/docker-compose.yaml @@ -1,24 +1,26 @@ services: nextcloud: - image: 'lscr.io/linuxserver/nextcloud:latest' + image: 'nextcloud:latest' environment: - SERVICE_URL_NEXTCLOUD_80 - - PUID=1000 - - PGID=1000 - 'TZ=${TZ:-Europe/Paris}' - 'POSTGRES_DB=${POSTGRES_DB:-nextcloud}' - 'POSTGRES_USER=${SERVICE_USER_POSTGRES}' - 'POSTGRES_PASSWORD=${SERVICE_PASSWORD_POSTGRES}' - POSTGRES_HOST=nextcloud-db - REDIS_HOST=redis - - REDIS_PORT=6379 + - REDIS_HOST_PORT=6379 - NEXTCLOUD_ADMIN_USER=${NEXTCLOUD_ADMIN_USER:-lailadmin} - NEXTCLOUD_ADMIN_PASSWORD=${NEXTCLOUD_ADMIN_PASSWORD} - NEXTCLOUD_TRUSTED_DOMAINS=${NEXTCLOUD_DOMAIN} + - OIDC_CLIENT_ID=${OIDC_CLIENT_ID} + - OIDC_CLIENT_SECRET=${OIDC_CLIENT_SECRET} + - OIDC_SLUG=${OIDC_SLUG} volumes: - - '/data/lail-structures/structest/nextcloud/nextcloud-config:/config' - - '/data/lail-structures/structest/nextcloud/nextcloud-data:/data' + - '/data/lail-structures/structest/nextcloud/nextcloud-app:/var/www/html' + - '/data/lail-structures/structest/nextcloud/nextcloud-data:/var/www/html/data' + - './post-install-oidc.sh:/docker-entrypoint-hooks.d/post-installation/010-oidc.sh:ro' depends_on: nextcloud-db: condition: service_healthy diff --git a/stacks/structures/structest/nextcloud/post-install-oidc.sh b/stacks/structures/structest/nextcloud/post-install-oidc.sh new file mode 100644 index 0000000..439c819 --- /dev/null +++ b/stacks/structures/structest/nextcloud/post-install-oidc.sh @@ -0,0 +1,18 @@ +#!/bin/bash +set -e + +OCC="php /var/www/html/occ" + +# Install or enable user_oidc app +$OCC app:install user_oidc 2>/dev/null || $OCC app:enable user_oidc + +# Register Authentik as OIDC provider +$OCC user_oidc:provider "Authentik" \ + --clientid="${OIDC_CLIENT_ID}" \ + --clientsecret="${OIDC_CLIENT_SECRET}" \ + --discoveryuri="https://sso.lail.cloud/application/o/${OIDC_SLUG}/.well-known/openid-configuration" \ + --mapping-uid="preferred_username" \ + --unique-uid=0 \ + --check-bearer=1 + +echo "OIDC provider Authentik configured."