Add self-contained Docker Compose stacks for pivoine.art infrastructure

Migrated 11 services from monolithic docker-compose project into independent stacks,
each with dedicated databases, minimal .env configuration, and bind-mount data volumes.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-15 22:41:50 +01:00
commit f21e0611b4
36 changed files with 896 additions and 0 deletions

3
.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
.claude
.env
*.sql

39
README.md Normal file
View File

@@ -0,0 +1,39 @@
# Stacks
Self-contained Docker Compose stacks for `pivoine.art` infrastructure.
Each stack is independently deployable with its own `compose.yml` and `.env`. All persistent data lives in `../.data/<stack>/`.
## Stacks
| Stack | Description | Services |
|---|---|---|
| `traefik` | Reverse proxy, TLS termination | traefik |
| `mailpit` | SMTP relay (no web UI) | mailpit |
| `watchtower` | Automatic container updates | watchtower |
| `umami` | Web analytics | umami, db |
| `immich` | Photo & video management | immich, ml, redis, db |
| `joplin` | Note sync server | joplin, db |
| `mattermost` | Team chat | mattermost, db |
| `gitea` | Git hosting + CI runner | gitea, runner, db |
| `coolify` | Deployment platform | coolify, realtime, redis, db |
| `sexy` | pivoine.art website | directus, frontend, redis, db |
| `vaultwarden` | Password manager | vaultwarden |
## Deployment
```bash
# Sync a stack to VPS
rsync -avz <stack>/ vps:~/stacks/<stack>/
# Start a stack
ssh vps 'cd ~/stacks/<stack> && docker compose up -d'
```
## Network
All stacks share the external `falcon_network` Docker network for inter-service communication (e.g. traefik routing, mailpit SMTP).
## Data
Persistent data is stored in `~/stacks/.data/<stack>/` on the VPS using bind mounts. Database stacks use dedicated Postgres instances with simple credentials.

129
coolify/compose.yml Normal file
View File

@@ -0,0 +1,129 @@
---
services:
coolify:
image: ghcr.io/coollabsio/coolify:latest
container_name: coolify
environment:
APP_ID: ${APP_ID}
APP_KEY: ${APP_KEY}
APP_NAME: Coolify
APP_ENV: production
APP_URL: https://${TRAEFIK_HOST}
APP_PORT: 8080
DB_HOST: coolify_db
DB_PORT: 5432
DB_DATABASE: coolify
DB_USERNAME: coolify
DB_PASSWORD: coolify
REDIS_HOST: coolify_redis
REDIS_PORT: 6379
PUSHER_HOST: realtime.${TRAEFIK_HOST}
PUSHER_PORT: 443
PUSHER_APP_ID: ${PUSHER_APP_ID}
PUSHER_APP_KEY: ${PUSHER_APP_KEY}
PUSHER_APP_SECRET: ${PUSHER_APP_SECRET}
PUSHER_SCHEME: https
SSL_MODE: "off"
volumes:
- ../.data/coolify/data:/data/coolify
- /var/run/docker.sock:/var/run/docker.sock
- ../.data/coolify/storage/ssh:/var/www/html/storage/app/ssh
- ../.data/coolify/storage/applications:/var/www/html/storage/app/applications
- ../.data/coolify/storage/databases:/var/www/html/storage/app/databases
- ../.data/coolify/storage/services:/var/www/html/storage/app/services
- ../.data/coolify/storage/backups:/var/www/html/storage/app/backups
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
realtime:
condition: service_healthy
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.coolify-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.coolify-web.middlewares=coolify-redirect-web-secure"
- "traefik.http.routers.coolify-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.coolify-web.entrypoints=web"
- "traefik.http.routers.coolify-web.service=coolify"
- "traefik.http.routers.coolify-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.coolify-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.coolify-web-secure.entrypoints=web-secure"
- "traefik.http.routers.coolify-web-secure.middlewares=security-headers@file"
- "traefik.http.routers.coolify-web-secure.service=coolify"
- "traefik.http.routers.coolify-web-secure.priority=1"
- "traefik.http.services.coolify.loadbalancer.server.port=8080"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
realtime:
image: ghcr.io/coollabsio/coolify-realtime:1.0.10
container_name: coolify_realtime
environment:
APP_NAME: Coolify
SOKETI_DEBUG: "false"
SOKETI_DEFAULT_APP_ID: ${PUSHER_APP_ID}
SOKETI_DEFAULT_APP_KEY: ${PUSHER_APP_KEY}
SOKETI_DEFAULT_APP_SECRET: ${PUSHER_APP_SECRET}
volumes:
- ../.data/coolify/storage/ssh:/var/www/html/storage/app/ssh
restart: always
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://127.0.0.1:6001/ready && wget -qO- http://127.0.0.1:6002/ready"]
interval: 5s
timeout: 2s
retries: 10
labels:
- "traefik.enable=true"
- "traefik.http.routers.coolify-realtime-web.rule=Host(`realtime.${TRAEFIK_HOST}`)"
- "traefik.http.routers.coolify-realtime-web.entrypoints=web"
- "traefik.http.routers.coolify-realtime-web.service=coolify-realtime"
- "traefik.http.routers.coolify-realtime-web-secure.rule=Host(`realtime.${TRAEFIK_HOST}`)"
- "traefik.http.routers.coolify-realtime-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.coolify-realtime-web-secure.entrypoints=web-secure"
- "traefik.http.routers.coolify-realtime-web-secure.service=coolify-realtime"
- "traefik.http.services.coolify-realtime.loadbalancer.server.port=6001"
- "traefik.http.routers.coolify-terminal-ws.rule=Host(`${TRAEFIK_HOST}`) && PathPrefix(`/terminal/ws`)"
- "traefik.http.routers.coolify-terminal-ws.tls.certresolver=resolver"
- "traefik.http.routers.coolify-terminal-ws.entrypoints=web-secure"
- "traefik.http.routers.coolify-terminal-ws.service=coolify-terminal"
- "traefik.http.routers.coolify-terminal-ws.priority=100"
- "traefik.http.services.coolify-terminal.loadbalancer.server.port=6002"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
redis:
image: redis:7-alpine
container_name: coolify_redis
restart: always
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
db:
image: postgres:16-alpine
container_name: coolify_db
environment:
POSTGRES_DB: coolify
POSTGRES_USER: coolify
POSTGRES_PASSWORD: coolify
volumes:
- ../.data/coolify/db:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

104
gitea/compose.yml Normal file
View File

@@ -0,0 +1,104 @@
---
services:
gitea:
image: gitea/gitea:latest
container_name: gitea
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
USER_UID: 1000
USER_GID: 1000
GITEA__APP_NAME: dev.pivoine.art
GITEA__database__DB_TYPE: postgres
GITEA__database__HOST: gitea_db:5432
GITEA__database__NAME: gitea
GITEA__database__USER: gitea
GITEA__database__PASSWD: gitea
GITEA__server__DOMAIN: ${TRAEFIK_HOST}
GITEA__server__SSH_DOMAIN: ${TRAEFIK_HOST}
GITEA__server__ROOT_URL: https://${TRAEFIK_HOST}/
GITEA__server__PROTOCOL: http
GITEA__server__HTTP_PORT: 3000
GITEA__server__START_SSH_SERVER: "true"
GITEA__server__SSH_PORT: 2222
GITEA__server__SSH_LISTEN_PORT: 2222
GITEA__mailer__ENABLED: "true"
GITEA__mailer__PROTOCOL: smtp
GITEA__mailer__SMTP_ADDR: mailpit
GITEA__mailer__SMTP_PORT: 1025
GITEA__service__DISABLE_REGISTRATION: "false"
GITEA__service__REQUIRE_SIGNIN_VIEW: "false"
GITEA__service__ENABLE_NOTIFY_MAIL: "true"
GITEA__service__DEFAULT_EMAIL_NOTIFICATIONS: enabled
GITEA__packages__ENABLED: "true"
GITEA__actions__ENABLED: "true"
GITEA__ui__THEMES: gitea-auto,gitea-light,gitea-dark,arc-green,edge-auto,edge-dark,edge-light,everforest-auto,everforest-dark,everforest-light,gruvbox-auto,gruvbox-dark,gruvbox-light,gruvbox-material-auto,gruvbox-material-dark,gruvbox-material-light,nord,palenight,soft-era,sonokai,sonokai-andromeda,sonokai-atlantis,sonokai-espresso,sonokai-maia,sonokai-shusia
GITEA__ui__DEFAULT_THEME: edge-dark
GITEA__ui__ENABLE_FEED: "true"
ports:
- "2222:2222"
volumes:
- ../.data/gitea/data:/data
- ../.data/gitea/config:/etc/gitea
- ./themes:/data/gitea/public/assets/css:ro
- /etc/localtime:/etc/localtime:ro
depends_on:
db:
condition: service_healthy
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.gitea-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.gitea-web.middlewares=gitea-redirect-web-secure"
- "traefik.http.routers.gitea-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.gitea-web.entrypoints=web"
- "traefik.http.routers.gitea-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.gitea-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.gitea-web-secure.entrypoints=web-secure"
- "traefik.http.routers.gitea-web-secure.middlewares=security-headers@file"
- "traefik.http.services.gitea-web-secure.loadbalancer.server.port=3000"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
runner:
image: gitea/act_runner:latest
container_name: gitea_runner
privileged: true
command: ["act_runner", "daemon", "--config", "/data/config.yaml"]
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
GITEA_INSTANCE_URL: https://${TRAEFIK_HOST}
GITEA_RUNNER_REGISTRATION_TOKEN: ${RUNNER_TOKEN}
GITEA_RUNNER_NAME: docker-runner
GITEA_RUNNER_LABELS: ubuntu-latest:docker://catthehacker/ubuntu:act-latest,ubuntu-22.04:docker://catthehacker/ubuntu:act-22.04,ubuntu-20.04:docker://catthehacker/ubuntu:act-20.04
DOCKER_HOST: unix:///var/run/docker.sock
volumes:
- ../.data/gitea/runner:/data
- /var/run/docker.sock:/var/run/docker.sock
- ./runner-config.yaml:/data/config.yaml:ro
labels:
- "com.centurylinklabs.watchtower.enable=true"
restart: always
networks:
- compose_network
db:
image: postgres:16-alpine
container_name: gitea_db
environment:
POSTGRES_DB: gitea
POSTGRES_USER: gitea
POSTGRES_PASSWORD: gitea
volumes:
- ../.data/gitea/db:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

32
gitea/runner-config.yaml Normal file
View File

@@ -0,0 +1,32 @@
log:
level: info
runner:
file: .runner
capacity: 1
envs: {}
env_file: .env
timeout: 3h
insecure: false
fetch_timeout: 5s
fetch_interval: 2s
labels: []
cache:
enabled: true
dir: ""
host: ""
port: 0
external_server: ""
container:
network: ""
privileged: false
options: "-v /var/run/docker.sock:/var/run/docker.sock"
workdir_parent: ""
valid_volumes: []
docker_host: ""
force_pull: false
host:
workdir_parent: ""

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

84
immich/compose.yml Normal file
View File

@@ -0,0 +1,84 @@
---
services:
immich:
image: ghcr.io/immich-app/immich-server:release
container_name: immich
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
DB_HOSTNAME: immich_db
DB_PORT: 5432
DB_USERNAME: immich
DB_PASSWORD: immich
DB_DATABASE_NAME: immich
REDIS_HOSTNAME: immich_redis
REDIS_PORT: 6379
IMMICH_MACHINE_LEARNING_URL: http://immich_ml:3003
volumes:
- ../.data/immich/upload:/usr/src/app/upload
- /etc/localtime:/etc/localtime:ro
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.immich-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.immich-web.middlewares=immich-redirect-web-secure"
- "traefik.http.routers.immich-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.immich-web.entrypoints=web"
- "traefik.http.routers.immich-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.immich-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.immich-web-secure.entrypoints=web-secure"
- "traefik.http.routers.immich-web-secure.middlewares=security-headers@file"
- "traefik.http.services.immich-web-secure.loadbalancer.server.port=2283"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
ml:
image: ghcr.io/immich-app/immich-machine-learning:release
container_name: immich_ml
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
volumes:
- ../.data/immich/model-cache:/cache
restart: always
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
redis:
image: redis:7-alpine
container_name: immich_redis
restart: always
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
db:
image: ghcr.io/immich-app/postgres:14-vectorchord0.4.3-pgvectors0.2.0
container_name: immich_db
environment:
POSTGRES_DB: immich
POSTGRES_USER: immich
POSTGRES_PASSWORD: immich
POSTGRES_INITDB_ARGS: --data-checksums
volumes:
- ../.data/immich/db:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

59
joplin/compose.yml Normal file
View File

@@ -0,0 +1,59 @@
---
services:
joplin:
image: joplin/server:latest
container_name: joplin
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
APP_PORT: 22300
APP_BASE_URL: https://${TRAEFIK_HOST}
DB_CLIENT: pg
POSTGRES_HOST: joplin_db
POSTGRES_PORT: 5432
POSTGRES_DATABASE: joplin
POSTGRES_USER: joplin
POSTGRES_PASSWORD: joplin
MAILER_ENABLED: 1
MAILER_HOST: mailpit
MAILER_PORT: 1025
MAILER_SECURE: 0
depends_on:
db:
condition: service_healthy
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.joplin-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.joplin-web.middlewares=joplin-redirect-web-secure"
- "traefik.http.routers.joplin-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.joplin-web.entrypoints=web"
- "traefik.http.routers.joplin-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.joplin-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.joplin-web-secure.entrypoints=web-secure"
- "traefik.http.routers.joplin-web-secure.middlewares=security-headers@file"
- "traefik.http.services.joplin-web-secure.loadbalancer.server.port=22300"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
db:
image: postgres:16-alpine
container_name: joplin_db
environment:
POSTGRES_DB: joplin
POSTGRES_USER: joplin
POSTGRES_PASSWORD: joplin
volumes:
- ../.data/joplin/db:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

29
mailpit/compose.yml Normal file
View File

@@ -0,0 +1,29 @@
---
services:
mailpit:
image: axllent/mailpit:latest
container_name: mailpit
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
MP_SMTP_AUTH_ACCEPT_ANY: 1
MP_SMTP_AUTH_ALLOW_INSECURE: 1
MP_MAX_MESSAGES: 5000
MP_SMTP_RELAY_ALL: "true"
MP_SMTP_RELAY_HOST: ${SMTP_RELAY_HOST}
MP_SMTP_RELAY_PORT: ${SMTP_RELAY_PORT}
MP_SMTP_RELAY_USERNAME: ${SMTP_RELAY_USERNAME}
MP_SMTP_RELAY_PASSWORD: ${SMTP_RELAY_PASSWORD}
MP_SMTP_RELAY_AUTH: plain
MP_SMTP_RELAY_TLS: "true"
MP_UI_BIND_ADDR: 0.0.0.0:0
volumes:
- ../.data/mailpit:/data
restart: always
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

68
mattermost/compose.yml Normal file
View File

@@ -0,0 +1,68 @@
---
services:
mattermost:
image: mattermost/mattermost-team-edition:latest
container_name: mattermost
security_opt:
- no-new-privileges:true
pids_limit: 200
tmpfs:
- /tmp
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
MM_SQLSETTINGS_DRIVERNAME: postgres
MM_SQLSETTINGS_DATASOURCE: postgres://mattermost:mattermost@mattermost_db:5432/mattermost?sslmode=disable&connect_timeout=10
MM_BLEVESETTINGS_INDEXDIR: /mattermost/bleve-indexes
MM_SERVICESETTINGS_SITEURL: https://${TRAEFIK_HOST}
MM_SERVICESETTINGS_ENABLELOCALMODE: "true"
MM_EMAILSETTINGS_ENABLESMTPAUTH: "false"
MM_EMAILSETTINGS_SMTPSERVER: mailpit
MM_EMAILSETTINGS_SMTPPORT: "1025"
MM_EMAILSETTINGS_CONNECTIONSECURITY: ""
MM_EMAILSETTINGS_FEEDBACKNAME: Mattermost
volumes:
- ../.data/mattermost/config:/mattermost/config:rw
- ../.data/mattermost/data:/mattermost/data:rw
- ../.data/mattermost/plugins:/mattermost/plugins:rw
- ../.data/mattermost/client-plugins:/mattermost/client/plugins:rw
- ../.data/mattermost/bleve:/mattermost/bleve-indexes:rw
depends_on:
db:
condition: service_healthy
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.mattermost-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.mattermost-web.middlewares=mattermost-redirect-web-secure"
- "traefik.http.routers.mattermost-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.mattermost-web.entrypoints=web"
- "traefik.http.routers.mattermost-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.mattermost-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.mattermost-web-secure.entrypoints=web-secure"
- "traefik.http.routers.mattermost-web-secure.middlewares=security-headers@file"
- "traefik.http.services.mattermost-web-secure.loadbalancer.server.port=8065"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
db:
image: postgres:16-alpine
container_name: mattermost_db
environment:
POSTGRES_DB: mattermost
POSTGRES_USER: mattermost
POSTGRES_PASSWORD: mattermost
volumes:
- ../.data/mattermost/db:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

122
sexy/compose.yml Normal file
View File

@@ -0,0 +1,122 @@
---
services:
sexy:
image: dev.pivoine.art/valknar/sexy:latest
container_name: sexy
user: node
working_dir: /home/node/app/packages/frontend
command: ["node", "build/index.js"]
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
NODE_ENV: production
PUBLIC_API_URL: https://${TRAEFIK_HOST}/api
PUBLIC_URL: https://${TRAEFIK_HOST}
PUBLIC_UMAMI_ID: ""
volumes:
- ../.data/sexy/bundle:/home/node/app/packages/bundle
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.sexy-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.sexy-web.middlewares=sexy-redirect-web-secure"
- "traefik.http.routers.sexy-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.sexy-web.entrypoints=web"
- "traefik.http.routers.sexy-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.sexy-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.sexy-web-secure.entrypoints=web-secure"
- "traefik.http.middlewares.sexy-compress.compress=true"
- "traefik.http.routers.sexy-web-secure.middlewares=sexy-compress"
- "traefik.http.services.sexy-web-secure.loadbalancer.server.port=3000"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
directus:
image: directus/directus:11.12.0
container_name: sexy_directus
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
SECRET: ${DIRECTUS_SECRET}
DB_CLIENT: pg
DB_HOST: sexy_db
DB_PORT: 5432
DB_DATABASE: directus
DB_USER: directus
DB_PASSWORD: directus
CACHE_ENABLED: "true"
CACHE_AUTO_PURGE: "true"
CACHE_STORE: redis
REDIS: redis://sexy_redis:6379
WEBSOCKETS_ENABLED: "true"
PUBLIC_URL: https://${TRAEFIK_HOST}/api
CORS_ENABLED: "true"
CORS_ORIGIN: https://${TRAEFIK_HOST}
SESSION_COOKIE_SECURE: "true"
SESSION_COOKIE_SAME_SITE: strict
SESSION_COOKIE_DOMAIN: ${TRAEFIK_HOST}
EXTENSIONS_PATH: ./extensions
EXTENSIONS_AUTO_RELOAD: "false"
CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC: https://${TRAEFIK_HOST}
EMAIL_TRANSPORT: smtp
EMAIL_SMTP_HOST: mailpit
EMAIL_SMTP_PORT: 1025
USER_REGISTER_URL_ALLOW_LIST: https://${TRAEFIK_HOST}/signup/verify
PASSWORD_RESET_URL_ALLOW_LIST: https://${TRAEFIK_HOST}/password/reset
volumes:
- ../.data/sexy/uploads:/directus/uploads
- ../.data/sexy/bundle:/directus/extensions/sexy.pivoine.art
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.sexy-directus-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.sexy-directus-web.middlewares=sexy-directus-redirect-web-secure"
- "traefik.http.routers.sexy-directus-web.rule=Host(`${TRAEFIK_HOST}`) && PathPrefix(`/api`)"
- "traefik.http.routers.sexy-directus-web.entrypoints=web"
- "traefik.http.routers.sexy-directus-web-secure.rule=Host(`${TRAEFIK_HOST}`) && PathPrefix(`/api`)"
- "traefik.http.routers.sexy-directus-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.sexy-directus-web-secure.entrypoints=web-secure"
- "traefik.http.middlewares.sexy-directus-compress.compress=true"
- "traefik.http.middlewares.sexy-directus-strip.stripprefix.prefixes=/api"
- "traefik.http.routers.sexy-directus-web-secure.middlewares=sexy-directus-strip,sexy-directus-compress"
- "traefik.http.services.sexy-directus-web-secure.loadbalancer.server.port=8055"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
redis:
image: redis:7-alpine
container_name: sexy_redis
restart: always
healthcheck:
test: ["CMD-SHELL", "redis-cli ping | grep PONG"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
db:
image: postgres:16-alpine
container_name: sexy_db
environment:
POSTGRES_DB: directus
POSTGRES_USER: directus
POSTGRES_PASSWORD: directus
volumes:
- ../.data/sexy/db:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

48
traefik/compose.yml Normal file
View File

@@ -0,0 +1,48 @@
---
services:
traefik:
image: traefik:latest
container_name: traefik
command:
- "--api.dashboard=false"
- "--ping=true"
- "--log.level=INFO"
- "--accesslog=true"
- "--global.sendAnonymousUsage=false"
- "--global.checkNewVersion=true"
- "--providers.docker=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=${NETWORK_NAME}"
- "--providers.file.directory=/etc/traefik/dynamic"
- "--providers.file.watch=true"
- "--entrypoints.web.address=:80"
- "--entrypoints.web-secure.address=:443"
- "--entrypoints.web.http.redirections.entryPoint.to=web-secure"
- "--entrypoints.web.http.redirections.entryPoint.scheme=https"
- "--entrypoints.web.http.redirections.entryPoint.permanent=true"
- "--entrypoints.web-secure.http.middlewares=security-headers@file"
- "--certificatesresolvers.resolver.acme.tlschallenge=true"
- "--certificatesresolvers.resolver.acme.email=${ACME_EMAIL}"
- "--certificatesresolvers.resolver.acme.storage=/letsencrypt/acme.json"
ports:
- "80:80"
- "443:443"
volumes:
- ../.data/traefik/letsencrypt:/letsencrypt
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./dynamic:/etc/traefik/dynamic:ro
restart: always
healthcheck:
test: ["CMD", "traefik", "healthcheck", "--ping"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
labels:
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

View File

@@ -0,0 +1,43 @@
tls:
options:
default:
minVersion: VersionTLS12
cipherSuites:
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
- TLS_AES_128_GCM_SHA256
- TLS_AES_256_GCM_SHA384
- TLS_CHACHA20_POLY1305_SHA256
curvePreferences:
- CurveP521
- CurveP384
sniStrict: true
http:
middlewares:
security-headers:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
customFrameOptionsValue: "SAMEORIGIN"
browserXssFilter: true
contentTypeNosniff: true
referrerPolicy: "strict-origin-when-cross-origin"
customResponseHeaders:
X-Robots-Tag: "none,noarchive,nosnippet,notranslate,noimageindex"
Permissions-Policy: "camera=(), microphone=(), geolocation=(), payment=(), usb=(), magnetometer=(), accelerometer=(), gyroscope=()"
X-Content-Type-Options: "nosniff"
X-Frame-Options: "SAMEORIGIN"
rate-limit:
rateLimit:
average: 100
burst: 50
period: 1s
api-rate-limit:
rateLimit:
average: 30
burst: 15
period: 1s

55
umami/compose.yml Normal file
View File

@@ -0,0 +1,55 @@
---
services:
umami:
image: ghcr.io/umami-software/umami:latest
container_name: umami
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
DATABASE_URL: postgresql://umami:umami@db:5432/umami
APP_SECRET: ${APP_SECRET}
depends_on:
db:
condition: service_healthy
init: true
restart: always
healthcheck:
test: ["CMD-SHELL", "curl http://localhost:3000/api/heartbeat"]
interval: 5s
timeout: 5s
retries: 5
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.umami-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.umami-web.middlewares=umami-redirect-web-secure"
- "traefik.http.routers.umami-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.umami-web.entrypoints=web"
- "traefik.http.routers.umami-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.umami-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.umami-web-secure.entrypoints=web-secure"
- "traefik.http.routers.umami-web-secure.middlewares=security-headers@file"
- "traefik.http.services.umami-web-secure.loadbalancer.server.port=3000"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
db:
image: postgres:15-alpine
container_name: umami_db
environment:
POSTGRES_DB: umami
POSTGRES_USER: umami
POSTGRES_PASSWORD: umami
volumes:
- ../.data/umami/db:/var/lib/postgresql/data
restart: always
healthcheck:
test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
interval: 5s
timeout: 5s
retries: 5
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

38
vaultwarden/compose.yml Normal file
View File

@@ -0,0 +1,38 @@
---
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
environment:
TZ: ${TIMEZONE:-Europe/Amsterdam}
DOMAIN: https://${TRAEFIK_HOST}
WEBSOCKET_ENABLED: "true"
SIGNUPS_ALLOWED: "true"
INVITATIONS_ALLOWED: "true"
SHOW_PASSWORD_HINT: "false"
SMTP_HOST: mailpit
SMTP_FROM_NAME: Vaultwarden
SMTP_SECURITY: off
SMTP_PORT: 1025
volumes:
- ../.data/vaultwarden:/data
restart: always
labels:
- "traefik.enable=true"
- "traefik.http.middlewares.vaultwarden-redirect-web-secure.redirectscheme.scheme=https"
- "traefik.http.routers.vaultwarden-web.middlewares=vaultwarden-redirect-web-secure"
- "traefik.http.routers.vaultwarden-web.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.vaultwarden-web.entrypoints=web"
- "traefik.http.routers.vaultwarden-web-secure.rule=Host(`${TRAEFIK_HOST}`)"
- "traefik.http.routers.vaultwarden-web-secure.tls.certresolver=resolver"
- "traefik.http.routers.vaultwarden-web-secure.entrypoints=web-secure"
- "traefik.http.routers.vaultwarden-web-secure.middlewares=security-headers@file"
- "traefik.http.services.vaultwarden-web-secure.loadbalancer.server.port=80"
- "traefik.docker.network=${NETWORK_NAME}"
- "com.centurylinklabs.watchtower.enable=true"
networks:
- compose_network
networks:
compose_network:
name: ${NETWORK_NAME}
external: true

22
watchtower/compose.yml Normal file
View File

@@ -0,0 +1,22 @@
---
services:
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
environment:
DOCKER_API_VERSION: "1.44"
WATCHTOWER_POLL_INTERVAL: 300
WATCHTOWER_LABEL_ENABLE: "true"
WATCHTOWER_CLEANUP: "true"
WATCHTOWER_INCLUDE_STOPPED: "false"
WATCHTOWER_INCLUDE_RESTARTING: "true"
WATCHTOWER_RUN_ONCE: "false"
WATCHTOWER_NOTIFICATIONS: ${NOTIFICATIONS:-}
WATCHTOWER_NOTIFICATION_URL: ${NOTIFICATION_URL:-}
WATCHTOWER_LOG_LEVEL: info
WATCHTOWER_ROLLING_RESTART: "false"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
restart: always
labels:
- "com.centurylinklabs.watchtower.enable=true"