# dev stack — developer tooling: Gitea, code-server, registry, DBX, BookStack, Plane. # (Renamed from "dev-stack"; private network renamed devstack -> dev. Vault moved # to the management plane. All named volumes converted to tiered bind mounts.) # # Volume -> bind-mount conversions: # gitea db -> /config/gitea/postgres (SSD) # dbx data -> /config/dbx (SSD) # plane db -> /config/plane/postgres (SSD) # plane redis -> /config/plane/redis (SSD) # plane minio -> /storage1/labdata/plane/minio (ZFS; attachments) # Tier-corrections (already bind-mounted, moved SSD -> ZFS): # registry -> /storage1/labdata/registry (image layers) # gitea repositories -> /storage1/labdata/gitea/repositories (git repos) # gitea lfs -> /storage1/labdata/gitea/lfs (large files) # Gitea config/avatars/indexers stay on SSD (/config/gitea). The repo/LFS split # is set via GITEA__repository__ROOT and GITEA__lfs__PATH pointing at ZFS mounts. services: # ============================================================ GITEA gitea: container_name: gitea image: gitea/gitea:latest restart: unless-stopped networks: [dev, edge] env_file: - stack.env environment: - USER_UID=1000 - USER_GID=1000 - GITEA__database__DB_TYPE=postgres - GITEA__database__HOST=postgres_gitea:5432 - GITEA__database__NAME=gitea - GITEA__database__USER=gitea - GITEA__database__PASSWD=${GITEA_DB_PASSWORD} - GITEA__server__DOMAIN=gitea.ginnoir.com - GITEA__server__ROOT_URL=https://gitea.ginnoir.com - GITEA__server__SSH_DOMAIN=gitea.ginnoir.com - GITEA__server__SSH_PORT=2222 # High-density git data on the ZFS pool; config stays on SSD (/data) - GITEA__repository__ROOT=/repos - GITEA__lfs__PATH=/repos-lfs volumes: - /config/gitea:/data - /storage1/labdata/gitea/repositories:/repos - /storage1/labdata/gitea/lfs:/repos-lfs - /etc/localtime:/etc/localtime:ro ports: - "3030:3000" - "2222:22" depends_on: postgres_gitea: condition: service_healthy gitea-runner: container_name: gitea_runner image: gitea/act_runner:latest restart: unless-stopped networks: [dev] env_file: - stack.env environment: - GITEA_INSTANCE_URL=https://gitea.ginnoir.com - GITEA_RUNNER_REGISTRATION_TOKEN=${GITEA_RUNNER_REGISTRATION_TOKEN} - GITEA_RUNNER_NAME=valhalla-dev-runner - GITEA_RUNNER_LABELS=ubuntu-latest:docker://node:24-bookworm,node-24:docker://node:24-bookworm volumes: - /config/gitea/act_runner:/data - /var/run/docker.sock:/var/run/docker.sock depends_on: gitea: condition: service_started postgres_gitea: container_name: postgres_gitea image: postgres:16-alpine restart: unless-stopped labels: - "com.centurylabs.watchtower.enable=false" networks: [dev] env_file: - stack.env environment: POSTGRES_USER: gitea POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD} POSTGRES_DB: gitea volumes: - /config/gitea/postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"] interval: 10s timeout: 5s retries: 5 # ============================================================ CODE-SERVER code-server: container_name: code_server image: lscr.io/linuxserver/code-server:latest restart: unless-stopped networks: [dev, edge] env_file: - stack.env environment: - PUID=1000 - PGID=1000 - TZ=${TZ} - DEFAULT_WORKSPACE=/workspace # Authentik forward_auth at the edge — local password auth disabled via config mount. volumes: - /config/code-server:/config - /config/code-server/code-server.yaml:/config/.config/code-server/config.yaml - /home/ginnoir:/workspace ports: - "8443:8443" # ============================================================ REGISTRY registry: container_name: registry image: registry:2 restart: unless-stopped networks: [dev, edge] volumes: - /storage1/labdata/registry:/var/lib/registry ports: - "5000:5000" registry-ui: container_name: registry_ui image: joxit/docker-registry-ui:latest restart: unless-stopped networks: [dev, edge] env_file: - stack.env environment: - SINGLE_REGISTRY=true - REGISTRY_TITLE=Valhalla Registry - NGINX_PROXY_PASS_URL=http://registry:5000 - SHOW_CONTENT_DIGEST=true - DELETE_IMAGES=true # ============================================================ DBX dbx: container_name: dbx image: t8y2/dbx:latest restart: unless-stopped networks: [dev, edge] volumes: - /config/dbx:/app/data ports: - "4224:4224" # ============================================================ BOOKSTACK bookstack: container_name: bookstack image: lscr.io/linuxserver/bookstack:latest restart: unless-stopped networks: [dev, edge] env_file: - stack.env environment: - PUID=1000 - PGID=1000 - TZ=${TZ} - APP_URL=https://docs.ginnoir.com - APP_KEY=${BOOKSTACK_APP_KEY} - DB_HOST=mariadb_bookstack - DB_PORT=3306 - DB_USER=bookstack - DB_PASS=${BOOKSTACK_DB_PASSWORD} - DB_DATABASE=bookstack - AUTH_METHOD=oidc - AUTH_AUTO_INITIATE=false - OIDC_NAME=Authentik - OIDC_ISSUER=https://auth.ginnoir.com/application/o/bookstack/ - OIDC_ISSUER_DISCOVER=true - OIDC_END_SESSION_ENDPOINT=true volumes: - /config/bookstack:/config ports: - "6875:80" depends_on: mariadb_bookstack: condition: service_healthy mariadb_bookstack: container_name: mariadb_bookstack image: lscr.io/linuxserver/mariadb:11.4.8 restart: unless-stopped labels: - "com.centurylabs.watchtower.enable=false" networks: [dev] env_file: - stack.env environment: - PUID=1000 - PGID=1000 - TZ=${TZ} - MYSQL_ROOT_PASSWORD=${BOOKSTACK_DB_ROOT_PASSWORD} - MYSQL_DATABASE=bookstack - MYSQL_USER=bookstack - MYSQL_PASSWORD=${BOOKSTACK_DB_PASSWORD} volumes: - /config/bookstack-db:/config healthcheck: test: ["CMD", "mysqladmin", "ping", "-h", "localhost"] interval: 10s timeout: 5s retries: 5 # ============================================================ PLANE plane-web: container_name: plane_web image: makeplane/plane-frontend:latest restart: unless-stopped networks: [dev, edge] command: node web/server.js env_file: - stack.env environment: - NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com - HOSTNAME=0.0.0.0 plane-admin: container_name: plane_admin image: makeplane/plane-admin:v0.23.1 restart: unless-stopped networks: [dev, edge] command: node admin/server.js env_file: - stack.env environment: - NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com - HOSTNAME=0.0.0.0 depends_on: plane-api: condition: service_started plane-web: condition: service_started plane-api: container_name: plane_api image: makeplane/plane-backend:latest restart: unless-stopped networks: [dev, edge] command: ./bin/docker-entrypoint-api.sh env_file: - stack.env environment: - SECRET_KEY=${PLANE_SECRET_KEY} - DEBUG=0 - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane - REDIS_URL=redis://redis_plane:6379/ - CELERY_BROKER_URL=redis://redis_plane:6379/0 - USE_MINIO=1 - AWS_REGION=us-east-1 - AWS_ACCESS_KEY_ID=${PLANE_MINIO_USER} - AWS_SECRET_ACCESS_KEY=${PLANE_MINIO_PASSWORD} - AWS_S3_ENDPOINT_URL=http://plane_minio:9000 - AWS_S3_BUCKET_NAME=uploads - WEB_URL=https://plane.ginnoir.com - CORS_ALLOWED_ORIGINS=https://plane.ginnoir.com - GUNICORN_WORKERS=2 depends_on: postgres_plane: condition: service_healthy redis_plane: condition: service_healthy plane-migrator: condition: service_completed_successfully plane-worker: container_name: plane_worker image: makeplane/plane-backend:latest restart: unless-stopped networks: [dev] command: ./bin/docker-entrypoint-worker.sh env_file: - stack.env environment: - SECRET_KEY=${PLANE_SECRET_KEY} - DEBUG=0 - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane - REDIS_URL=redis://redis_plane:6379/ - CELERY_BROKER_URL=redis://redis_plane:6379/0 - USE_MINIO=1 - AWS_REGION=us-east-1 - AWS_ACCESS_KEY_ID=${PLANE_MINIO_USER} - AWS_SECRET_ACCESS_KEY=${PLANE_MINIO_PASSWORD} - AWS_S3_ENDPOINT_URL=http://plane_minio:9000 - AWS_S3_BUCKET_NAME=uploads depends_on: postgres_plane: condition: service_healthy redis_plane: condition: service_healthy plane-migrator: condition: service_completed_successfully plane-beat: container_name: plane_beat image: makeplane/plane-backend:latest restart: unless-stopped networks: [dev] command: ./bin/docker-entrypoint-beat.sh env_file: - stack.env environment: - SECRET_KEY=${PLANE_SECRET_KEY} - DEBUG=0 - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane - REDIS_URL=redis://redis_plane:6379/ - CELERY_BROKER_URL=redis://redis_plane:6379/0 depends_on: postgres_plane: condition: service_healthy redis_plane: condition: service_healthy plane-migrator: condition: service_completed_successfully plane-migrator: container_name: plane_migrator image: makeplane/plane-backend:latest restart: "no" networks: [dev] command: ./bin/docker-entrypoint-migrator.sh env_file: - stack.env environment: - SECRET_KEY=${PLANE_SECRET_KEY} - DATABASE_URL=postgresql://plane:${PLANE_DB_PASSWORD}@postgres_plane:5432/plane - REDIS_URL=redis://redis_plane:6379/ depends_on: postgres_plane: condition: service_healthy redis_plane: condition: service_healthy plane-create-bucket: container_name: plane_create_bucket image: minio/mc:latest restart: "no" networks: [dev] depends_on: plane-minio: condition: service_healthy entrypoint: > /bin/sh -c " until /usr/bin/mc alias set plane_minio http://plane_minio:9000 $$MINIO_USER $$MINIO_PASSWORD; do sleep 1; done; /usr/bin/mc mb plane_minio/uploads --ignore-existing; exit 0; " env_file: - stack.env environment: - MINIO_USER=${PLANE_MINIO_USER} - MINIO_PASSWORD=${PLANE_MINIO_PASSWORD} postgres_plane: container_name: postgres_plane image: postgres:16-alpine restart: unless-stopped labels: - "com.centurylabs.watchtower.enable=false" networks: [dev] env_file: - stack.env environment: POSTGRES_USER: plane POSTGRES_PASSWORD: ${PLANE_DB_PASSWORD} POSTGRES_DB: plane volumes: - /config/plane/postgres:/var/lib/postgresql/data healthcheck: test: ["CMD-SHELL", "pg_isready -U plane -d plane"] interval: 10s timeout: 5s retries: 5 redis_plane: container_name: redis_plane image: redis:7-alpine restart: unless-stopped labels: - "com.centurylabs.watchtower.enable=false" networks: [dev] command: --save 60 1 --loglevel warning volumes: - /config/plane/redis:/data healthcheck: test: ["CMD", "redis-cli", "ping"] interval: 10s timeout: 5s retries: 5 plane-minio: container_name: plane_minio image: minio/minio:RELEASE.2025-09-07T16-13-09Z restart: unless-stopped labels: - "com.centurylabs.watchtower.enable=false" networks: [dev] command: server /data --console-address ":9001" env_file: - stack.env environment: MINIO_ROOT_USER: ${PLANE_MINIO_USER} MINIO_ROOT_PASSWORD: ${PLANE_MINIO_PASSWORD} volumes: - /storage1/labdata/plane/minio:/data healthcheck: test: ["CMD-SHELL", "curl -sf http://localhost:9000/minio/health/live || exit 1"] interval: 30s timeout: 20s retries: 3 start_period: 30s networks: dev: name: dev driver: bridge edge: name: edge external: true