From e174807481d2c83c3680141215b4a1b8595fad4f Mon Sep 17 00:00:00 2001 From: ginnoir Date: Thu, 4 Jun 2026 15:14:49 -0500 Subject: [PATCH] feat: add full dev stack (Gitea, code-server, registry, DBX, Vault, BookStack, Plane) - dev-compose.yml: 16-service dev stack on dedicated devstack network - vault.hcl: Vault file-backend config; Vault initialized and unsealed - docker-compose.yml: add devstack as external network on caddy - Caddyfile: LAN-only site blocks for all 7 dev services; Plane routed via Caddy path handles (no bundled proxy container) - apply-compose.ps1: add -DevStack and -VaultUnseal flags - .env: dev stack secrets + all 5 Vault unseal keys + root token Dev URLs (LAN-only): gitea, code, registry, dbx, vault, docs, plane Stack managed by Portainer as dev-stack (id 7) --- .env | 30 ++++ Caddyfile | 50 ++++++ apply-compose.ps1 | 36 ++++- dev-compose.yml | 379 +++++++++++++++++++++++++++++++++++++++++++++ docker-compose.yml | 3 + vault.hcl | 11 ++ 6 files changed, 507 insertions(+), 2 deletions(-) create mode 100644 dev-compose.yml create mode 100644 vault.hcl diff --git a/.env b/.env index 8311185..dd9a881 100644 --- a/.env +++ b/.env @@ -106,3 +106,33 @@ CF_API_TOKEN=cfut_ijIT9HDjXWKgd0dtnyLqxv8L3deB7hSDivSqquwp905a6c51 # GitHub Actions self-hosted runner (fine-grained PAT, Administration:write on homelabstack) GITHUB_RUNNER_ACCESS_TOKEN=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 + +# ============================================================= +# DEV STACK +# ============================================================= +# Gitea +GITEA_DB_PASSWORD=gitea_d3vp4ss_9f2k + +# VS Code in browser (code-server) +CODE_SERVER_PASSWORD=c0de_s3rver_pass + +# BookStack wiki +BOOKSTACK_DB_PASSWORD=bookstack_d3v_pass +BOOKSTACK_DB_ROOT_PASSWORD=bookstack_r00t_pass + +# Plane project management +# Rotate with: openssl rand -hex 32 +PLANE_SECRET_KEY=60gp0byfz2dvffa45cxl20p1scy9xbpf6d8c5y0geejgkyp1b52 +PLANE_DB_PASSWORD=plane_d3v_p4ss_8x3m +PLANE_MINIO_USER=planeadmin +PLANE_MINIO_PASSWORD=plane_minio_p4ss + +# Vault unseal keys + root token (generated 2026-06-04 during init) +# Need any 3 of 5 keys to unseal after a restart. +# Re-unseal with: apply-compose.ps1 -VaultUnseal (or manually via SSH) +VAULT_UNSEAL_KEY_1=fDQVnopNwCDXkUehLdQTpSasymm6h4yGheBKUNdz+jHM +VAULT_UNSEAL_KEY_2=ix34PMxiwdpZQlzIBuxeWVgBGU/bhyomBR1BOA3NnGvJ +VAULT_UNSEAL_KEY_3=SWXVKnhoYsT6XNiUnWKgTgle2JwsRUNZzX4J3XK++l6n +VAULT_UNSEAL_KEY_4=rjSgX+lie2ulhJpm+yiWf3mY0hd+8JI6zbPlJPN5Scm+ +VAULT_UNSEAL_KEY_5=UIt78/Qi+rO+krJ4Nt0quTGHQlj+MYaXqt+kt7pAqDma +VAULT_ROOT_TOKEN=hvs.QmhZvkXnFfebbRC7JVFLcNPn diff --git a/Caddyfile b/Caddyfile index 1d4757a..810d556 100644 --- a/Caddyfile +++ b/Caddyfile @@ -213,4 +213,54 @@ homarr.ginnoir.com { router.ginnoir.com { import internal_only reverse_proxy 192.168.1.1 +} + +# ============================================================= +# DEV STACK — internal only +# ============================================================= +gitea.ginnoir.com { + import internal_only + reverse_proxy gitea:3000 +} + +code.ginnoir.com { + import internal_only + reverse_proxy code_server:8443 { + transport http { + tls_insecure_skip_verify + } + } +} + +registry.ginnoir.com { + import internal_only + reverse_proxy registry:5000 +} + +dbx.ginnoir.com { + import internal_only + reverse_proxy dbx:4224 +} + +vault.ginnoir.com { + import internal_only + reverse_proxy vault:8200 +} + +docs.ginnoir.com { + import internal_only + reverse_proxy bookstack:80 +} + +plane.ginnoir.com { + import internal_only + handle /api/* { + reverse_proxy plane_api:8000 + } + handle /auth/* { + reverse_proxy plane_api:8000 + } + handle { + reverse_proxy plane_web:3000 + } } \ No newline at end of file diff --git a/apply-compose.ps1 b/apply-compose.ps1 index 593912b..515a8ab 100644 --- a/apply-compose.ps1 +++ b/apply-compose.ps1 @@ -11,12 +11,16 @@ # powershell -File apply-compose.ps1 -Caddy # Caddyfile only + hot-reload Caddy # powershell -File apply-compose.ps1 -Compose # docker-compose.yml only + pull + up -d # powershell -File apply-compose.ps1 -Portainer # portainer-compose.yml only + up -d +# powershell -File apply-compose.ps1 -DevStack # dev-compose.yml + vault.hcl + up -d +# powershell -File apply-compose.ps1 -VaultUnseal # unseal vault after restart (uses keys from .env) param( [switch]$Compose, [switch]$Caddy, [switch]$EnvFile, - [switch]$Portainer + [switch]$Portainer, + [switch]$DevStack, + [switch]$VaultUnseal ) $ErrorActionPreference = "Stop" @@ -28,9 +32,11 @@ $composeLocal = Join-Path $PSScriptRoot "docker-compose.yml" $caddyLocal = Join-Path $PSScriptRoot "Caddyfile" $envLocal = Join-Path $PSScriptRoot ".env" $portainerLocal = Join-Path $PSScriptRoot "portainer-compose.yml" +$devComposeLocal = Join-Path $PSScriptRoot "dev-compose.yml" +$vaultConfigLocal = Join-Path $PSScriptRoot "vault.hcl" # Default (no flags): push everything. -if (-not $Compose -and -not $Caddy -and -not $EnvFile -and -not $Portainer) { +if (-not $Compose -and -not $Caddy -and -not $EnvFile -and -not $Portainer -and -not $DevStack -and -not $VaultUnseal) { $EnvFile = $true; $Caddy = $true; $Compose = $true } @@ -62,4 +68,30 @@ if ($Portainer) { ssh $server "docker compose -f ~/valhalla-lab/portainer-compose.yml up -d" } +if ($DevStack) { + Write-Host "Pushing dev-compose.yml ..." + scp $devComposeLocal "${server}:~/valhalla-lab/dev-compose.yml" + Write-Host "Pushing vault.hcl ..." + ssh $server "mkdir -p /config/vault/config /config/vault/data /config/vault/logs" + scp $vaultConfigLocal "${server}:/config/vault/config/vault.hcl" + Write-Host "Pulling latest dev images ..." + ssh $server "docker compose -f ~/valhalla-lab/dev-compose.yml --env-file ~/valhalla-lab/.env pull" + Write-Host "Applying dev stack (recreates only changed containers) ..." + ssh $server "docker compose -f ~/valhalla-lab/dev-compose.yml --env-file ~/valhalla-lab/.env up -d" +} + +if ($VaultUnseal) { + # Load the three unseal keys from .env + $envContent = Get-Content $envLocal | Where-Object { $_ -match "^VAULT_UNSEAL_KEY_[123]=" } + $keys = $envContent | ForEach-Object { ($_ -split "=", 2)[1] } + if ($keys.Count -lt 3) { + Write-Error "Could not find VAULT_UNSEAL_KEY_1/2/3 in .env" + exit 1 + } + Write-Host "Unsealing Vault (3 of 5 keys) ..." + foreach ($key in $keys) { + ssh $server "docker exec vault vault operator unseal $key 2>&1 | grep -E 'Sealed|Error'" + } +} + Write-Host "Done." diff --git a/dev-compose.yml b/dev-compose.yml new file mode 100644 index 0000000..c83bf41 --- /dev/null +++ b/dev-compose.yml @@ -0,0 +1,379 @@ +# dev-compose.yml +# Full developer tooling stack: Git hosting, browser IDE, container registry, +# database UI, secret management, documentation wiki, and project management. +# +# This file creates the `devstack` network. The main docker-compose.yml +# references it as external so Caddy can reach these services. +# +# Fresh-install order: +# docker compose -f portainer-compose.yml up -d # portainer_proxy network +# docker compose -f dev-compose.yml up -d # devstack network + dev tools +# dc up -d # application stack +# +# Apply changes: +# powershell -File apply-compose.ps1 -DevStack + +services: + + # ============================================================ + # GITEA (self-hosted Git) + # ============================================================ + gitea: + container_name: gitea + image: gitea/gitea:latest + restart: unless-stopped + networks: + - devstack + 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 + volumes: + - /config/gitea:/data + - /etc/localtime:/etc/localtime:ro + ports: + - "3030:3000" + - "2222:22" + depends_on: + postgres_gitea: + condition: service_healthy + + postgres_gitea: + container_name: postgres_gitea + image: postgres:16-alpine + restart: unless-stopped + networks: + - devstack + environment: + POSTGRES_USER: gitea + POSTGRES_PASSWORD: ${GITEA_DB_PASSWORD} + POSTGRES_DB: gitea + volumes: + - gitea_db_data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U gitea -d gitea"] + interval: 10s + timeout: 5s + retries: 5 + + # ============================================================ + # CODE-SERVER (VS Code in browser) + # ============================================================ + code-server: + container_name: code_server + image: lscr.io/linuxserver/code-server:latest + restart: unless-stopped + networks: + - devstack + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - PASSWORD=${CODE_SERVER_PASSWORD} + - SUDO_PASSWORD=${CODE_SERVER_PASSWORD} + - DEFAULT_WORKSPACE=/workspace + volumes: + - /config/code-server:/config + - /home/ginnoir:/workspace + ports: + - "8443:8443" + + # ============================================================ + # PRIVATE CONTAINER REGISTRY + # ============================================================ + registry: + container_name: registry + image: registry:2 + restart: unless-stopped + networks: + - devstack + volumes: + - /config/registry:/var/lib/registry + ports: + - "5000:5000" + + # ============================================================ + # DBX (Multi-database web UI) + # https://github.com/t8y2/dbx + # ============================================================ + dbx: + container_name: dbx + image: t8y2/dbx:latest + restart: unless-stopped + networks: + - devstack + volumes: + - dbx_data:/app/data + ports: + - "4224:4224" + + # ============================================================ + # VAULT (Secret management) + # One-time setup after first start: + # ssh ginnoir@valhalla + # docker exec -it vault vault operator init + # vault operator unseal (three times with unseal keys) + # ============================================================ + vault: + container_name: vault + image: hashicorp/vault:latest + restart: unless-stopped + networks: + - devstack + cap_add: + - IPC_LOCK + environment: + - VAULT_ADDR=http://0.0.0.0:8200 + volumes: + - /config/vault/config:/vault/config:ro + - /config/vault/data:/vault/data + - /config/vault/logs:/vault/logs + command: vault server -config=/vault/config/vault.hcl + ports: + - "8200:8200" + + # ============================================================ + # BOOKSTACK (Documentation / Wiki) + # ============================================================ + bookstack: + container_name: bookstack + image: lscr.io/linuxserver/bookstack:latest + restart: unless-stopped + networks: + - devstack + environment: + - PUID=1000 + - PGID=1000 + - TZ=${TZ} + - APP_URL=https://docs.ginnoir.com + - DB_HOST=mariadb_bookstack + - DB_PORT=3306 + - DB_USER=bookstack + - DB_PASS=${BOOKSTACK_DB_PASSWORD} + - DB_DATABASE=bookstack + 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:latest + restart: unless-stopped + networks: + - devstack + 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 (Project management) + # First start: log into https://plane.ginnoir.com and create a workspace. + # Caddy routes /api/ and /auth/ to plane-api, everything else to plane-web. + # ============================================================ + plane-web: + container_name: plane_web + image: makeplane/plane-frontend:latest + restart: unless-stopped + networks: + - devstack + command: node web/server.js + environment: + - NEXT_PUBLIC_API_BASE_URL=https://plane.ginnoir.com + + plane-api: + container_name: plane_api + image: makeplane/plane-backend:latest + restart: unless-stopped + networks: + - devstack + command: ./bin/docker-entrypoint-api.sh + 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/ + - 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: + - devstack + command: ./bin/docker-entrypoint-worker.sh + 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/ + - 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: + - devstack + command: ./bin/docker-entrypoint-beat.sh + 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/ + 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: + - devstack + command: ./bin/docker-entrypoint-migrator.sh + 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: + - devstack + 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; + " + environment: + - MINIO_USER=${PLANE_MINIO_USER} + - MINIO_PASSWORD=${PLANE_MINIO_PASSWORD} + + postgres_plane: + container_name: postgres_plane + image: postgres:16-alpine + restart: unless-stopped + networks: + - devstack + environment: + POSTGRES_USER: plane + POSTGRES_PASSWORD: ${PLANE_DB_PASSWORD} + POSTGRES_DB: plane + volumes: + - plane_db_data:/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 + networks: + - devstack + command: --save 60 1 --loglevel warning + volumes: + - plane_redis_data:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + + plane-minio: + container_name: plane_minio + image: minio/minio:latest + restart: unless-stopped + networks: + - devstack + command: server /data --console-address ":9001" + environment: + MINIO_ROOT_USER: ${PLANE_MINIO_USER} + MINIO_ROOT_PASSWORD: ${PLANE_MINIO_PASSWORD} + volumes: + - plane_minio_data:/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: + devstack: + name: devstack + +volumes: + gitea_db_data: + dbx_data: + plane_db_data: + plane_redis_data: + plane_minio_data: diff --git a/docker-compose.yml b/docker-compose.yml index fae3e08..ff53ec2 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -77,6 +77,7 @@ services: - authentik - management - portainer_proxy + - devstack ports: - "80:80" - "443:443" @@ -781,6 +782,8 @@ networks: management: portainer_proxy: external: true + devstack: + external: true volumes: minio_data: diff --git a/vault.hcl b/vault.hcl new file mode 100644 index 0000000..cd51bbf --- /dev/null +++ b/vault.hcl @@ -0,0 +1,11 @@ +storage "file" { + path = "/vault/data" +} + +listener "tcp" { + address = "0.0.0.0:8200" + tls_disable = true +} + +api_addr = "https://vault.ginnoir.com" +ui = true