diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2bad43d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,130 @@ +# Repository Guidelines + +## Project Structure & Module Organization + +This is a multi-service Docker Compose infrastructure project organized as follows: + +- **Root**: `compose.yaml` (orchestrates all services), `arty.yml` (centralized configuration) +- **Service Directories**: Each service has its own folder (`core/`, `sexy/`, `proxy/`, `ai/`, etc.) containing: + - `compose.yaml`: Service-specific Docker Compose configuration + - Service-specific configuration files and initialization scripts +- **Shared Infrastructure**: `core/` provides PostgreSQL 16 and Redis 7 for all services +- **Proxy**: `proxy/` contains Traefik reverse proxy with dynamic security configurations in `proxy/dynamic/` + +All services connect to a single external Docker network (`falcon_network`) defined by `$NETWORK_NAME`. + +## Build, Test, and Development Commands + +All commands use `pnpm arty` (leveraging scripts defined in `arty.yml`): + +- `pnpm arty up` - Start all services in detached mode +- `pnpm arty down` - Stop and remove all containers +- `pnpm arty ps` - List running containers +- `pnpm arty logs` - Follow logs for all services +- `pnpm arty restart` - Restart all services +- `pnpm arty pull` - Pull latest images for all services +- `pnpm arty config` - View rendered configuration with variables substituted +- `pnpm arty net/create` - Create the external Docker network (required before first run) + +## Coding Style & Naming Conventions + +**Environment Variables:** +- Pattern: `{SERVICE}_COMPOSE_PROJECT_NAME`, `{SERVICE}_TRAEFIK_HOST`, `{SERVICE}_DOCKER_IMAGE` +- Defined in `arty.yml` under `envs.default` with sensible defaults +- Sensitive values stored in `.env` (passwords, secrets, API keys) + +**Service Configuration:** +- Use `${VARIABLE:-default_value}` syntax for environment variable references +- Container names: `${SERVICE_COMPOSE_PROJECT_NAME}_app` or `${SERVICE_COMPOSE_PROJECT_NAME}_component` +- Volume names: `${SERVICE_COMPOSE_PROJECT_NAME}_volume_name` + +**Traefik Labels:** +- HTTP → HTTPS redirect on `web` entrypoint (port 80) +- SSL termination on `web-secure` entrypoint (port 443) +- All routers scoped to `${NETWORK_NAME}` network +- Use middleware pattern: `${SERVICE_COMPOSE_PROJECT_NAME}-middleware-name` + +**File Organization:** +- Database initialization scripts: `core/postgres/init/*.sh` +- Dynamic Traefik configuration: `proxy/dynamic/*.yaml` +- Service data exports: `{service}/` (e.g., `sexy/directus.yaml`) + +## Testing Guidelines + +This is an infrastructure project with no automated test suite. Validation is done through: + +**Service Health Checks:** +- Most services include healthcheck definitions in their `compose.yaml` +- Check service health: `docker ps` (look for "healthy" status) +- Inspect specific service: `docker inspect ` + +**Manual Verification:** +- Access services via configured hostnames (e.g., `https://sexy.pivoine.art`) +- Check Traefik dashboard: `https://proxy.pivoine.art` (requires HTTP Basic Auth) +- Test SSL certificates: `curl -I https://.pivoine.art` +- Monitor logs: `pnpm arty logs` or `docker logs ` + +**Troubleshooting:** +- Database connectivity: `docker exec core_postgres psql -U $DB_USER -l` +- Network exists: `docker network ls | grep falcon` +- Check configuration rendering: `pnpm arty config` + +## Commit & Pull Request Guidelines + +**Commit Message Format:** +- Follow conventional commits: `type: description` +- Types: `feat`, `fix`, `docs`, `refactor`, `chore` +- Keep description concise and imperative mood +- Add detailed explanation in commit body when needed +- Include co-author attribution: `Co-Authored-By: Claude ` +- Add generation marker: `🤖 Generated with [Claude Code](https://claude.com/claude-code)` + +**Examples:** +``` +feat: add sexy/bundle/update script to refresh Directus extensions + +Added arty script to update Directus extension bundle from the latest +sexy_frontend image. This ensures the API container always has the +latest extensions when the frontend image is rebuilt. +``` + +``` +fix: enable email functionality in Linkwarden with correct SMTP configuration + +- Add NEXT_PUBLIC_EMAIL_PROVIDER=true to enable email features +- Change EMAIL_SERVER protocol from smtp:// to smtps:// for port 465 +``` + +**Changes to Make:** +- Update `arty.yml` for new environment variables or scripts +- Update service `compose.yaml` files for container configuration +- Add initialization scripts to `core/postgres/init/` for new databases +- Document new services in README.md +- Never commit secrets - they belong in `.env` only + +## Security & Configuration Tips + +**Secrets Management:** +- Store all secrets in `.env` (never committed to git) +- Use Apache htpasswd format for HTTP Basic Auth: `openssl passwd -apr1 'password'` +- Escape `$` signs with `$$` in `.env` files for Docker Compose compatibility + +**Security Headers:** +- Global security settings: `proxy/dynamic/security.yaml` +- Automatically reloaded by Traefik (no restart needed) +- Includes HSTS, X-Frame-Options, CSP, and rate limiting + +**Database Access:** +- PostgreSQL exposed on port 5432 for local development +- All databases use shared credentials: `$DB_USER` and `$DB_PASSWORD` +- Database initialization: `core/postgres/init/01-init-databases.sh` + +**SSL Certificates:** +- Automatic Let's Encrypt via Traefik ACME +- Requires `ADMIN_EMAIL` in `.env` +- Certificates stored in `proxy_letsencrypt_data` volume + +**Network Architecture:** +- All services must connect to external network: `${NETWORK_NAME}` +- Create network before first run: `pnpm arty net/create` +- Network must be external and created separately from compose stacks diff --git a/arty.yml b/arty.yml index 70dc5c3..04650c0 100644 --- a/arty.yml +++ b/arty.yml @@ -161,6 +161,9 @@ envs: MEDIA_KOEL_DEBUG: false MEDIA_KOEL_MEMORY_LIMIT: 512 MEDIA_KOEL_STREAMING_METHOD: x-sendfile + MEDIA_AMPACHE_IMAGE: ampache/ampache:latest + MEDIA_AMPACHE_TRAEFIK_HOST: ampache.media.pivoine.art + MEDIA_AMPACHE_DB_NAME: ampache # PairDrop DROP_TRAEFIK_ENABLED: true DROP_COMPOSE_PROJECT_NAME: drop diff --git a/core/postgres/init/01-init-databases.sh b/core/postgres/init/01-init-databases.sh index 364835a..3311e12 100644 --- a/core/postgres/init/01-init-databases.sh +++ b/core/postgres/init/01-init-databases.sh @@ -45,6 +45,10 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E SELECT 'CREATE DATABASE koel' WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'koel')\\gexec + -- Ampache music streaming database + SELECT 'CREATE DATABASE ampache' + WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'ampache')\\gexec + -- Grant privileges to all databases GRANT ALL PRIVILEGES ON DATABASE directus TO $POSTGRES_USER; GRANT ALL PRIVILEGES ON DATABASE umami TO $POSTGRES_USER; @@ -55,11 +59,12 @@ psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-E GRANT ALL PRIVILEGES ON DATABASE tandoor TO $POSTGRES_USER; GRANT ALL PRIVILEGES ON DATABASE asciinema TO $POSTGRES_USER; GRANT ALL PRIVILEGES ON DATABASE koel TO $POSTGRES_USER; + GRANT ALL PRIVILEGES ON DATABASE ampache TO $POSTGRES_USER; -- Log success SELECT 'Compose databases initialized:' AS status; SELECT datname FROM pg_database - WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden', 'joplin', 'mattermost', 'tandoor', 'asciinema', 'koel') + WHERE datname IN ('directus', 'umami', 'n8n', 'linkwarden', 'joplin', 'mattermost', 'tandoor', 'asciinema', 'koel', 'ampache') ORDER BY datname; EOSQL @@ -77,4 +82,5 @@ echo " • mattermost - Chat platform database" echo " • tandoor - Recipe manager database" echo " • asciinema - Terminal recording server database" echo " • koel - Music streaming server database" +echo " • ampache - Music streaming server database" echo "" diff --git a/media/compose.yaml b/media/compose.yaml index c5ee315..0427ec3 100644 --- a/media/compose.yaml +++ b/media/compose.yaml @@ -149,6 +149,44 @@ services: networks: - compose_network + # Ampache - Music streaming server with public sharing + ampache: + image: ${MEDIA_AMPACHE_IMAGE:-ampache/ampache:latest} + container_name: ${MEDIA_COMPOSE_PROJECT_NAME}_ampache + restart: unless-stopped + volumes: + - ampache_config:/var/www/config + - ampache_log:/var/log/ampache + - /mnt/hidrive/users/valknar/Music:/media:ro + environment: + TZ: ${TIMEZONE:-Europe/Berlin} + DB_HOST: ${CORE_DB_HOST} + DB_PORT: ${CORE_DB_PORT} + DB_NAME: ${MEDIA_AMPACHE_DB_NAME} + DB_USERNAME: ${DB_USER} + DB_PASSWORD: ${DB_PASSWORD} + AMPACHE_WEB_PATH: https://${MEDIA_AMPACHE_TRAEFIK_HOST} + networks: + - compose_network + labels: + - 'traefik.enable=${MEDIA_TRAEFIK_ENABLED}' + # HTTP to HTTPS redirect + - 'traefik.http.middlewares.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-redirect-web-secure.redirectscheme.scheme=https' + - 'traefik.http.routers.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web.middlewares=${MEDIA_COMPOSE_PROJECT_NAME}-ampache-redirect-web-secure' + - 'traefik.http.routers.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web.rule=Host(`${MEDIA_AMPACHE_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web.entrypoints=web' + # HTTPS router + - 'traefik.http.routers.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web-secure.rule=Host(`${MEDIA_AMPACHE_TRAEFIK_HOST}`)' + - 'traefik.http.routers.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web-secure.tls.certresolver=resolver' + - 'traefik.http.routers.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web-secure.entrypoints=web-secure' + - 'traefik.http.middlewares.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web-secure-compress.compress=true' + - 'traefik.http.routers.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web-secure.middlewares=${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web-secure-compress,security-headers@file' + # Service + - 'traefik.http.services.${MEDIA_COMPOSE_PROJECT_NAME}-ampache-web-secure.loadbalancer.server.port=80' + - 'traefik.docker.network=${NETWORK_NAME}' + # Watchtower + - 'com.centurylinklabs.watchtower.enable=${WATCHTOWER_LABEL_ENABLE}' + volumes: jellyfin_config: name: ${MEDIA_COMPOSE_PROJECT_NAME}_jellyfin_config @@ -160,6 +198,10 @@ volumes: name: ${MEDIA_COMPOSE_PROJECT_NAME}_koel_covers koel_search_index: name: ${MEDIA_COMPOSE_PROJECT_NAME}_koel_search_index + ampache_config: + name: ${MEDIA_COMPOSE_PROJECT_NAME}_ampache_config + ampache_log: + name: ${MEDIA_COMPOSE_PROJECT_NAME}_ampache_log networks: compose_network: