From 57ff7b4e5fe4d029ae1e89d5f7db64752362d6cd Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Fri, 13 Feb 2026 13:44:57 +0100 Subject: [PATCH 01/14] conf(all): simplify the installation with interactive script --- .env.example | 47 +-- .env.local.example | 18 +- README.md | 4 +- compose.custom_certs.yml | 166 --------- compose.custom_certs_external_postgres.yml | 140 ------- compose.external_postgres.yml | 145 -------- compose.local.yml | 35 +- compose.with_postgres_migration.yml | 316 ---------------- compose.yml | 84 ++++- install.sh | 408 +++++++++++++++++++++ scripts/preflight.sh | 236 ++++++++++++ scripts/update.sh | 89 +++++ versions.env | 2 + 13 files changed, 845 insertions(+), 845 deletions(-) delete mode 100644 compose.custom_certs.yml delete mode 100644 compose.custom_certs_external_postgres.yml delete mode 100644 compose.external_postgres.yml delete mode 100644 compose.with_postgres_migration.yml create mode 100755 install.sh create mode 100755 scripts/preflight.sh create mode 100755 scripts/update.sh create mode 100644 versions.env diff --git a/.env.example b/.env.example index 962df46..3b5d4a8 100644 --- a/.env.example +++ b/.env.example @@ -3,35 +3,26 @@ # Documentation: https://getplumber.io/docs/installation/docker-compose/ # ########################################################################## -# Main -DOMAIN_NAME="plumber." -CERTIFICATE_EMAIL="tech@getplumber.io" -JOBS_GITLAB_URL="https://" -FRONTEND_IMAGE_TAG="v2.32.5" -BACKEND_IMAGE_TAG="v2.36.4" +# Required configuration +DOMAIN_NAME="" +JOBS_GITLAB_URL="" ORGANIZATION="" +GITLAB_OAUTH2_CLIENT_ID="" +GITLAB_OAUTH2_CLIENT_SECRET="" -# Secrets -GITLAB_OAUTH2_CLIENT_ID="REPLACE_ME_BY_CLIENT_ID" -GITLAB_OAUTH2_CLIENT_SECRET="REPLACE_ME_BY_CLIENT_SECRET" -SECRET_KEY="REPLACE_ME_BY_SECRET_KEY" -JOBS_DB_PASSWORD="REPLACE_ME_BY_JOBS_DB_PASSWORD" -JOBS_REDIS_PASSWORD="REPLACE_ME_BY_JOBS_REDIS_PASSWORD" +# Auto-generated secrets (populated by install script) +SECRET_KEY="" +JOBS_DB_PASSWORD="" +JOBS_REDIS_PASSWORD="" -# Edit only if you use external database -JOBS_DB_USER="jobs" -JOBS_DB_NAME="jobs" -JOBS_DB_HOST="postgres" -JOBS_DB_PORT="5432" -JOBS_DB_SSLMODE="disable" -JOBS_DB_TIMEZONE="Europe/Paris" +# Deployment profile (set by install script) +# Options: letsencrypt or custom-certs, combined with internal-db or omit for external +COMPOSE_PROFILES="letsencrypt,internal-db" -############################################ -# Below configuration shouldn't be updated # -############################################ - -# Shared configuration -FRONTEND_DOMAIN="$DOMAIN_NAME" -API_DOMAIN="$DOMAIN_NAME" -API_PATH="/api" -API_URL="$API_DOMAIN$API_PATH" +# Optional: Override database defaults (only needed when NOT using internal-db profile) +# JOBS_DB_HOST="your-db-host" +# JOBS_DB_PORT="5432" +# JOBS_DB_USER="jobs" +# JOBS_DB_NAME="jobs" +# JOBS_DB_SSLMODE="disable" +# JOBS_DB_TIMEZONE="Europe/Paris" diff --git a/.env.local.example b/.env.local.example index f677d8c..b4093b5 100644 --- a/.env.local.example +++ b/.env.local.example @@ -1,12 +1,15 @@ ############################################################################### -# Plumber configuration file # +# Plumber local configuration file # # Documentation: https://getplumber.io/docs/installation/local-docker-compose # ############################################################################### # Main JOBS_GITLAB_URL="GITLAB_INSTANCE_URL" +<<<<<<< HEAD FRONTEND_IMAGE_TAG="v2.32.5" BACKEND_IMAGE_TAG="v2.36.4" +======= +>>>>>>> 03b7b4b (conf(all): simplify the installation with interactive script) ORGANIZATION="" # Secrets @@ -15,16 +18,3 @@ GITLAB_OAUTH2_CLIENT_SECRET="REPLACE_ME_BY_CLIENT_SECRET" SECRET_KEY="REPLACE_ME_BY_SECRET_KEY" JOBS_DB_PASSWORD="REPLACE_ME_BY_JOBS_DB_PASSWORD" JOBS_REDIS_PASSWORD="REPLACE_ME_BY_JOBS_REDIS_PASSWORD" - -############################################ -# Below configuration shouldn't be updated # -############################################ - -# Shared configuration -FRONTEND_DOMAIN="localhost:3000" -API_DOMAIN="http://localhost:3001" -API_PATH="/api" - -# Jobs database -JOBS_DB_USER="jobs" -JOBS_DB_NAME="jobs" diff --git a/README.md b/README.md index 606b1eb..f9392c6 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,4 @@ Two installation methods: You are welcome to help us improve this repository! -🎮 Open an Issue or create Pull Requests from your fork - -For the [Helm chart](charts/plumber/README.md), there is a dedicated [contributing page](charts/plumber/CONTIBUTING.md). +🎮 Open an Issue or create Pull Requests from your fork \ No newline at end of file diff --git a/compose.custom_certs.yml b/compose.custom_certs.yml deleted file mode 100644 index c3db464..0000000 --- a/compose.custom_certs.yml +++ /dev/null @@ -1,166 +0,0 @@ -name: "plumber" - -x-backend-common: &backend-common - image: docker.io/getplumber/backend:$BACKEND_IMAGE_TAG - env_file: .env - environment: - - JOBS_LISTEN_ADDR=0.0.0.0 - - JOBS_LISTEN_PORT=80 - - JOBS_CORS_ORIGIN=https://$DOMAIN_NAME - - JOBS_FRONTEND_URL=https://$DOMAIN_NAME - - JOBS_SESSION_TTL=168h - - JOBS_DB_HOST=postgres - - JOBS_DB_PORT=5432 - - JOBS_DB_SSLMODE=disable - - JOBS_DB_TIMEZONE=Europe/Paris - - JOBS_API_DOMAIN=https://$API_URL - - LOG_LEVEL=info - - LOG_FORMATTER=text - - JOBS_REDIS_HOST=redis - - JOBS_REDIS_PORT=6379 - - JOBS_REDIS_DB=0 - - JOBS_REDIS_USER=default - - JOBS_REDIS_SET_NAMESPACES_TTL=30s - - GITLEAKS_PATH=/opt/gitleaks - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - networks: - - intranet - -services: - - ############ - # Plumber # - ############ - - frontend: - image: docker.io/getplumber/frontend:$FRONTEND_IMAGE_TAG - env_file: .env - restart: unless-stopped - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - expose: - - "3000" - labels: - - "traefik.http.routers.front.rule=Host(`${FRONTEND_DOMAIN}`)" - - "traefik.http.routers.front.entrypoints=websecure" - - "traefik.http.routers.front.tls=true" - networks: - - intranet - - backend: - <<: *backend-common - expose: - - "80" - - "9090" - labels: - - "traefik.http.routers.api.rule=Host(\"$API_DOMAIN\")&&PathPrefix(\"$API_PATH\")" - - "traefik.http.routers.api.entrypoints=websecure" - - "traefik.http.routers.api.tls=true" - - worker: - <<: *backend-common - command: ["--worker"] - deploy: - mode: replicated - replicas: 5 - expose: - - "9090" - labels: - - "traefik.enable=false" - - - ##################### - # External services # - ##################### - - redis: - image: redis:8.4 - pull_policy: always - restart: unless-stopped - env_file: - - .env - command: - - redis-server - - --requirepass $JOBS_REDIS_PASSWORD - healthcheck: - test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] - interval: 5s - timeout: 3s - retries: 20 - start_period: 5s - expose: - - "6379" - networks: - - intranet - - postgres: - image: postgres:18 - pull_policy: always - restart: unless-stopped - volumes: - - postgres-data:/var/lib/postgresql - env_file: .env - environment: - - POSTGRES_USER=$JOBS_DB_USER - - POSTGRES_PASSWORD=$JOBS_DB_PASSWORD - - POSTGRES_DB=$JOBS_DB_NAME - - PGDATA=/var/lib/postgresql/18/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"] - interval: 5s - timeout: 3s - retries: 20 - start_period: 10s - expose: - - "5432" - networks: - - intranet - - ################# - # Reverse proxy # - ################# - - traefik: - image: traefik:3.6 - pull_policy: always - restart: unless-stopped - depends_on: - - frontend - - backend - ports: - - "80:80" - - "443:443" - command: - - --log.level=DEBUG - - --accesslog=true - - --entrypoints.web.address=:80 - - --entrypoints.web.http.redirections.entryPoint.to=websecure - - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true - - --entrypoints.websecure.address=:443 - - --providers.docker=true - - --providers.file.filename=/etc/traefik/certs.yml - labels: - - "traefik.enable=false" - - "traefik.http.routers.traefik.entrypoints=websecure" - - "traefik.http.routers.traefik.tls=true" - - "traefik.http.routers.traefik.service=api@internal" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./.docker/traefik/certs:/certs - - ./.docker/traefik/certs.yml:/etc/traefik/certs.yml - networks: - - intranet - -networks: - intranet: - -volumes: - postgres-data: diff --git a/compose.custom_certs_external_postgres.yml b/compose.custom_certs_external_postgres.yml deleted file mode 100644 index 690de60..0000000 --- a/compose.custom_certs_external_postgres.yml +++ /dev/null @@ -1,140 +0,0 @@ -name: "plumber" - -x-backend-common: &backend-common - image: docker.io/getplumber/backend:$BACKEND_IMAGE_TAG - env_file: .env - environment: - - JOBS_LISTEN_ADDR=0.0.0.0 - - JOBS_LISTEN_PORT=80 - - JOBS_CORS_ORIGIN=https://$DOMAIN_NAME - - JOBS_FRONTEND_URL=https://$DOMAIN_NAME - - JOBS_SESSION_TTL=168h - - JOBS_API_DOMAIN=https://$API_URL - - LOG_LEVEL=info - - LOG_FORMATTER=text - - JOBS_REDIS_HOST=redis - - JOBS_REDIS_PORT=6379 - - JOBS_REDIS_DB=0 - - JOBS_REDIS_USER=default - - JOBS_REDIS_SET_NAMESPACES_TTL=30s - - GITLEAKS_PATH=/opt/gitleaks - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - networks: - - intranet - -services: - - ############ - # Plumber # - ############ - - frontend: - image: docker.io/getplumber/frontend:$FRONTEND_IMAGE_TAG - env_file: .env - restart: unless-stopped - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - expose: - - "3000" - labels: - - "traefik.http.routers.front.rule=Host(`${FRONTEND_DOMAIN}`)" - - "traefik.http.routers.front.entrypoints=websecure" - - "traefik.http.routers.front.tls=true" - networks: - - intranet - - backend: - <<: *backend-common - expose: - - "80" - - "9090" - labels: - - "traefik.http.routers.api.rule=Host(\"$API_DOMAIN\")&&PathPrefix(\"$API_PATH\")" - - "traefik.http.routers.api.entrypoints=websecure" - - "traefik.http.routers.api.tls=true" - - worker: - <<: *backend-common - command: ["--worker"] - deploy: - mode: replicated - replicas: 5 - expose: - - "9090" - labels: - - "traefik.enable=false" - - - ##################### - # External services # - ##################### - - redis: - image: redis:8.4 - pull_policy: always - restart: unless-stopped - env_file: - - .env - command: - - redis-server - - --requirepass $JOBS_REDIS_PASSWORD - healthcheck: - test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] - interval: 5s - timeout: 3s - retries: 20 - start_period: 5s - expose: - - "6379" - networks: - - intranet - - - ################# - # Reverse proxy # - ################# - - traefik: - image: traefik:3.6 - pull_policy: always - restart: unless-stopped - depends_on: - - frontend - - backend - ports: - - "80:80" - - "443:443" - command: - - --log.level=DEBUG - - --accesslog=true - - --entrypoints.web.address=:80 - - --entrypoints.web.http.redirections.entryPoint.to=websecure - - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true - - --entrypoints.websecure.address=:443 - - --providers.docker=true - - --providers.file.filename=/etc/traefik/certs.yml - labels: - - "traefik.enable=false" - - "traefik.http.routers.traefik.entrypoints=websecure" - - "traefik.http.routers.traefik.tls=true" - - "traefik.http.routers.traefik.service=api@internal" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - ./.docker/traefik/certs:/certs - - ./.docker/traefik/certs.yml:/etc/traefik/certs.yml - networks: - - intranet - -networks: - intranet: - -volumes: - postgres-data: diff --git a/compose.external_postgres.yml b/compose.external_postgres.yml deleted file mode 100644 index d68d3bf..0000000 --- a/compose.external_postgres.yml +++ /dev/null @@ -1,145 +0,0 @@ -name: "plumber" - -x-backend-common: &backend-common - image: docker.io/getplumber/backend:$BACKEND_IMAGE_TAG - env_file: .env - environment: - - JOBS_LISTEN_ADDR=0.0.0.0 - - JOBS_LISTEN_PORT=80 - - JOBS_CORS_ORIGIN=https://$DOMAIN_NAME - - JOBS_FRONTEND_URL=https://$DOMAIN_NAME - - JOBS_SESSION_TTL=168h - - JOBS_API_DOMAIN=https://$API_URL - - LOG_LEVEL=info - - LOG_FORMATTER=text - - JOBS_REDIS_HOST=redis - - JOBS_REDIS_PORT=6379 - - JOBS_REDIS_DB=0 - - JOBS_REDIS_USER=default - - JOBS_REDIS_SET_NAMESPACES_TTL=30s - - GITLEAKS_PATH=/opt/gitleaks - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - networks: - - intranet - -services: - - ############ - # Plumber # - ############ - - frontend: - image: docker.io/getplumber/frontend:$FRONTEND_IMAGE_TAG - env_file: .env - restart: unless-stopped - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - expose: - - "3000" - labels: - - "traefik.http.routers.front.rule=Host(`${FRONTEND_DOMAIN}`)" - - "traefik.http.routers.front.entrypoints=websecure" - - "traefik.http.routers.front.tls=true" - - "traefik.http.routers.front.tls.certresolver=le" - networks: - - intranet - - backend: - <<: *backend-common - expose: - - "80" - - "9090" - labels: - - "traefik.http.routers.api.rule=Host(\"$API_DOMAIN\")&&PathPrefix(\"$API_PATH\")" - - "traefik.http.routers.api.entrypoints=websecure" - - "traefik.http.routers.api.tls=true" - - "traefik.http.routers.api.tls.certresolver=le" - - worker: - <<: *backend-common - command: ["--worker"] - deploy: - mode: replicated - replicas: 5 - expose: - - "9090" - labels: - - "traefik.enable=false" - - - ##################### - # External services # - ##################### - - redis: - image: redis:8.4 - pull_policy: always - restart: unless-stopped - env_file: - - .env - command: - - redis-server - - --requirepass $JOBS_REDIS_PASSWORD - healthcheck: - test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] - interval: 5s - timeout: 3s - retries: 20 - start_period: 5s - expose: - - "6379" - networks: - - intranet - - - ################# - # Reverse proxy # - ################# - - traefik: - image: traefik:3.6 - pull_policy: always - restart: unless-stopped - depends_on: - - frontend - - backend - ports: - - "80:80" - - "443:443" - command: - - --log.level=DEBUG - - --accesslog=true - - --entrypoints.web.address=:80 - - --entrypoints.web.http.redirections.entryPoint.to=websecure - - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true - - --entrypoints.websecure.address=:443 - - --providers.docker=true - - --certificatesresolvers.le.acme.caserver=https://acme-v02.api.letsencrypt.org/directory - - --certificatesresolvers.le.acme.storage=/acme/acme.json - - --certificatesresolvers.le.acme.tlschallenge=true - labels: - - "traefik.enable=false" - - "traefik.http.routers.traefik.entrypoints=websecure" - - "traefik.http.routers.traefik.tls=true" - - "traefik.http.routers.traefik.tls.certresolver=le" - - "traefik.http.routers.traefik.service=api@internal" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - traefik-acme:/acme - networks: - - intranet - -networks: - intranet: - -volumes: - postgres-data: - traefik-acme: diff --git a/compose.local.yml b/compose.local.yml index 1f955cd..3efc28c 100644 --- a/compose.local.yml +++ b/compose.local.yml @@ -1,19 +1,21 @@ name: "plumber-local" x-backend-common: &backend-common - image: docker.io/getplumber/backend:$BACKEND_IMAGE_TAG + image: docker.io/getplumber/backend:${BACKEND_IMAGE_TAG} env_file: .env environment: - JOBS_LISTEN_ADDR=0.0.0.0 - JOBS_LISTEN_PORT=3000 - - JOBS_CORS_ORIGIN=http://$FRONTEND_DOMAIN - - JOBS_FRONTEND_URL=http://$FRONTEND_DOMAIN + - JOBS_CORS_ORIGIN=http://localhost:3000 + - JOBS_FRONTEND_URL=http://localhost:3000 - JOBS_SESSION_TTL=168h - - JOBS_DB_HOST=postgres - - JOBS_DB_PORT=5432 - - JOBS_DB_SSLMODE=disable - - JOBS_DB_TIMEZONE=Europe/Paris - - JOBS_API_DOMAIN=$API_DOMAIN$API_PATH + - JOBS_DB_HOST=${JOBS_DB_HOST:-postgres} + - JOBS_DB_PORT=${JOBS_DB_PORT:-5432} + - JOBS_DB_USER=${JOBS_DB_USER:-jobs} + - JOBS_DB_NAME=${JOBS_DB_NAME:-jobs} + - JOBS_DB_SSLMODE=${JOBS_DB_SSLMODE:-disable} + - JOBS_DB_TIMEZONE=${JOBS_DB_TIMEZONE:-Europe/Paris} + - JOBS_API_DOMAIN=http://localhost:3001/api - LOG_LEVEL=info - LOG_FORMATTER=text - JOBS_REDIS_HOST=redis @@ -22,6 +24,9 @@ x-backend-common: &backend-common - JOBS_REDIS_USER=default - JOBS_REDIS_SET_NAMESPACES_TTL=30s - GITLEAKS_PATH=/opt/gitleaks + - FRONTEND_DOMAIN=localhost:3000 + - API_DOMAIN=http://localhost:3001 + - API_PATH=/api restart: unless-stopped depends_on: postgres: @@ -40,8 +45,12 @@ services: ############ frontend: - image: docker.io/getplumber/frontend:$FRONTEND_IMAGE_TAG + image: docker.io/getplumber/frontend:${FRONTEND_IMAGE_TAG} env_file: .env + environment: + - FRONTEND_DOMAIN=localhost:3000 + - API_DOMAIN=http://localhost:3001 + - API_PATH=/api restart: unless-stopped volumes: - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ @@ -74,7 +83,7 @@ services: env_file: .env command: - redis-server - - --requirepass $JOBS_REDIS_PASSWORD + - --requirepass ${JOBS_REDIS_PASSWORD} healthcheck: test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] interval: 5s @@ -94,9 +103,9 @@ services: - postgres-data:/var/lib/postgresql env_file: .env environment: - - POSTGRES_USER=$JOBS_DB_USER - - POSTGRES_PASSWORD=$JOBS_DB_PASSWORD - - POSTGRES_DB=$JOBS_DB_NAME + - POSTGRES_USER=${JOBS_DB_USER:-jobs} + - POSTGRES_PASSWORD=${JOBS_DB_PASSWORD} + - POSTGRES_DB=${JOBS_DB_NAME:-jobs} - PGDATA=/var/lib/postgresql/18/data healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"] diff --git a/compose.with_postgres_migration.yml b/compose.with_postgres_migration.yml deleted file mode 100644 index 562139c..0000000 --- a/compose.with_postgres_migration.yml +++ /dev/null @@ -1,316 +0,0 @@ -# This compose file cannot be used like that, it's a working example of compose -# configutation with postgres migration - -name: "plumber" - -x-backend-common: &backend-common - image: docker.io/getplumber/backend:$BACKEND_IMAGE_TAG - env_file: .env - environment: - - JOBS_LISTEN_ADDR=0.0.0.0 - - JOBS_LISTEN_PORT=80 - - JOBS_CORS_ORIGIN=https://$DOMAIN_NAME - - JOBS_FRONTEND_URL=https://$DOMAIN_NAME - - JOBS_SESSION_TTL=168h - - JOBS_DB_HOST=postgres - - JOBS_DB_PORT=5432 - - JOBS_DB_SSLMODE=disable - - JOBS_DB_TIMEZONE=Europe/Paris - - JOBS_API_DOMAIN=https://$API_URL - - LOG_LEVEL=info - - LOG_FORMATTER=text - - JOBS_REDIS_HOST=redis - - JOBS_REDIS_PORT=6379 - - JOBS_REDIS_DB=0 - - JOBS_REDIS_USER=default - - JOBS_REDIS_SET_NAMESPACES_TTL=30s - - GITLEAKS_PATH=/opt/gitleaks - restart: unless-stopped - depends_on: - postgres: - condition: service_healthy - redis: - condition: service_healthy - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - networks: - - intranet - -services: - - ############ - # Plumber # - ############ - - frontend: - image: docker.io/getplumber/frontend:$FRONTEND_IMAGE_TAG - env_file: .env - restart: unless-stopped - volumes: - - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ - expose: - - "3000" - labels: - - "traefik.http.routers.front.rule=Host(`${FRONTEND_DOMAIN}`)" - - "traefik.http.routers.front.entrypoints=websecure" - - "traefik.http.routers.front.tls=true" - - "traefik.http.routers.front.tls.certresolver=le" - networks: - - intranet - - backend: - <<: *backend-common - expose: - - "80" - - "9090" - labels: - - "traefik.http.routers.api.rule=Host(\"$API_DOMAIN\")&&PathPrefix(\"$API_PATH\")" - - "traefik.http.routers.api.entrypoints=websecure" - - "traefik.http.routers.api.tls=true" - - "traefik.http.routers.api.tls.certresolver=le" - - worker: - <<: *backend-common - command: ["--worker"] - deploy: - mode: replicated - replicas: 5 - expose: - - "9090" - labels: - - "traefik.enable=false" - - - ##################### - # External services # - ##################### - - redis: - image: redis:8.4 - pull_policy: always - restart: unless-stopped - env_file: - - .env - command: - - redis-server - - --requirepass $JOBS_REDIS_PASSWORD - healthcheck: - test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] - interval: 5s - timeout: 3s - retries: 20 - start_period: 5s - expose: - - "6379" - networks: - - intranet - - # Step 1: Prepare legacy data structure (move pg13 data to versioned path) - postgres_prepare: - image: alpine:3.20 - restart: "no" - volumes: - - postgres-data:/var/lib/postgresql - command: - - /bin/sh - - -c - - | - set -eu - echo "=== Preparing PostgreSQL data directories ===" - - # Ensure version-specific directories exist - mkdir -p /var/lib/postgresql/13 /var/lib/postgresql/18 - - # Check various locations where PG13 data might exist - if [ -f /var/lib/postgresql/13/data/PG_VERSION ]; then - echo "PostgreSQL 13 already at correct location: /var/lib/postgresql/13/data" - elif [ -f /var/lib/postgresql/data/PG_VERSION ] && [ "$$(cat /var/lib/postgresql/data/PG_VERSION)" = "13" ]; then - echo "Found PostgreSQL 13 at: /var/lib/postgresql/data" - echo "Moving to: /var/lib/postgresql/13/data" - mv /var/lib/postgresql/data /var/lib/postgresql/13/data - elif [ -f /var/lib/postgresql/PG_VERSION ] && [ "$$(cat /var/lib/postgresql/PG_VERSION)" = "13" ]; then - echo "Found PostgreSQL 13 at volume root: /var/lib/postgresql" - echo "Moving to: /var/lib/postgresql/13/data" - mkdir -p /var/lib/postgresql/13/data - # Move all PG files/dirs except version-specific directories - find /var/lib/postgresql -maxdepth 1 -mindepth 1 ! -name '13' ! -name '18' -exec mv {} /var/lib/postgresql/13/data/ \; - else - echo "No PostgreSQL 13 data found (fresh install or already migrated and cleaned)" - fi - - echo "=== Preparation complete ===" - - # Step 2: Upgrade from PostgreSQL 13 to 18 using pg_upgrade - postgres_upgrade: - image: postgres:18.0 - pull_policy: always - restart: "no" - volumes: - - postgres-data:/var/lib/postgresql - depends_on: - postgres_prepare: - condition: service_completed_successfully - env_file: .env - environment: - - DEBIAN_FRONTEND=noninteractive - command: - - /bin/bash - - -c - - | - set -eu - echo "=== PostgreSQL 13 → 18 Upgrade Check ===" - - # Check if pg18 already exists (idempotency) - if [ -f /var/lib/postgresql/18/data/PG_VERSION ]; then - echo "✓ PostgreSQL 18 already initialized (version: $$(cat /var/lib/postgresql/18/data/PG_VERSION))" - echo "✓ Skipping upgrade (idempotent)" - exit 0 - fi - - # Check if pg13 data exists - if [ ! -f /var/lib/postgresql/13/data/PG_VERSION ]; then - echo "✓ No PostgreSQL 13 cluster found" - echo "✓ Fresh installation - will initialize as PostgreSQL 18" - exit 0 - fi - - echo "Found PostgreSQL 13 cluster at: /var/lib/postgresql/13/data" - echo "Starting upgrade process..." - - # Fix ownership and permissions of the old cluster (may have been changed during move) - echo "Fixing ownership and permissions of PostgreSQL 13 data directory..." - chown -R postgres:postgres /var/lib/postgresql/13 - chmod 700 /var/lib/postgresql/13/data - - # Install PostgreSQL 13 binaries (needed for pg_upgrade) - echo "Installing PostgreSQL 13 binaries..." - apt-get update -qq - apt-get install -y -qq postgresql-13 > /dev/null 2>&1 - - # Initialize new PostgreSQL 18 cluster - echo "Initializing PostgreSQL 18 cluster..." - mkdir -p /var/lib/postgresql/18/data - chown -R postgres:postgres /var/lib/postgresql/18 - - # Check if old cluster has checksums enabled - OLD_CHECKSUMS=$$(su postgres -c "/usr/lib/postgresql/13/bin/pg_controldata /var/lib/postgresql/13/data | grep 'Data page checksum version' | awk '{print \$$NF}'") - - # Use the same database superuser from old cluster - INITDB_USER="$${JOBS_DB_USER:-postgres}" - echo "Initializing new cluster with superuser: $$INITDB_USER" - - if [ "$$OLD_CHECKSUMS" = "0" ]; then - echo "Old cluster has checksums disabled, initializing new cluster without checksums" - su postgres -c "/usr/lib/postgresql/18/bin/initdb -D /var/lib/postgresql/18/data --no-data-checksums -U $$INITDB_USER" - else - echo "Old cluster has checksums enabled, initializing new cluster with checksums" - su postgres -c "/usr/lib/postgresql/18/bin/initdb -D /var/lib/postgresql/18/data --data-checksums -U $$INITDB_USER" - fi - - # Run pg_upgrade check first - echo "Running pg_upgrade --check..." - cd /var/lib/postgresql - su postgres -c "/usr/lib/postgresql/18/bin/pg_upgrade \ - --old-bindir=/usr/lib/postgresql/13/bin \ - --new-bindir=/usr/lib/postgresql/18/bin \ - --old-datadir=/var/lib/postgresql/13/data \ - --new-datadir=/var/lib/postgresql/18/data \ - --username=$${JOBS_DB_USER:-postgres} \ - --check" || { - echo "ERROR: pg_upgrade --check failed. Please review the error above." - exit 1 - } - - # Perform actual upgrade - echo "Running pg_upgrade (this may take several minutes)..." - su postgres -c "/usr/lib/postgresql/18/bin/pg_upgrade \ - --old-bindir=/usr/lib/postgresql/13/bin \ - --new-bindir=/usr/lib/postgresql/18/bin \ - --old-datadir=/var/lib/postgresql/13/data \ - --new-datadir=/var/lib/postgresql/18/data \ - --username=$${JOBS_DB_USER:-postgres} \ - --link" || { - echo "ERROR: pg_upgrade failed. Please review the error above." - exit 1 - } - - # Copy pg_hba.conf from old cluster (preserves existing authentication rules) - echo "Copying pg_hba.conf from old cluster..." - cp /var/lib/postgresql/13/data/pg_hba.conf /var/lib/postgresql/18/data/pg_hba.conf - chown postgres:postgres /var/lib/postgresql/18/data/pg_hba.conf - - echo "=== ✓ Upgrade completed successfully! ===" - echo "Note: Old PostgreSQL 13 data preserved at: /var/lib/postgresql/13/data" - echo " You can remove it after verifying the migration." - - # Step 3: Run PostgreSQL 18 - postgres: - image: postgres:18 - pull_policy: always - restart: unless-stopped - volumes: - - postgres-data:/var/lib/postgresql - env_file: .env - environment: - - POSTGRES_USER=$JOBS_DB_USER - - POSTGRES_PASSWORD=$JOBS_DB_PASSWORD - - POSTGRES_DB=$JOBS_DB_NAME - - PGDATA=/var/lib/postgresql/18/data - healthcheck: - test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"] - interval: 5s - timeout: 3s - retries: 20 - start_period: 10s - expose: - - "5432" - networks: - - intranet - depends_on: - postgres_upgrade: - condition: service_completed_successfully - - ################# - # Reverse proxy # - ################# - - traefik: - image: traefik:3.6 - pull_policy: always - restart: unless-stopped - depends_on: - - frontend - - backend - ports: - - "80:80" - - "443:443" - command: - - --log.level=DEBUG - - --accesslog=true - - --entrypoints.web.address=:80 - - --entrypoints.web.http.redirections.entryPoint.to=websecure - - --entrypoints.web.http.redirections.entryPoint.scheme=https - - --entrypoints.web.http.redirections.entrypoint.permanent=true - - --entrypoints.websecure.address=:443 - - --providers.docker=true - - --certificatesresolvers.le.acme.caserver=https://acme-v02.api.letsencrypt.org/directory - - --certificatesresolvers.le.acme.storage=/acme/acme.json - - --certificatesresolvers.le.acme.tlschallenge=true - labels: - - "traefik.enable=false" - - "traefik.http.routers.traefik.entrypoints=websecure" - - "traefik.http.routers.traefik.tls=true" - - "traefik.http.routers.traefik.tls.certresolver=le" - - "traefik.http.routers.traefik.service=api@internal" - volumes: - - /var/run/docker.sock:/var/run/docker.sock - - traefik-acme:/acme - networks: - - intranet - -networks: - intranet: - -volumes: - postgres-data: - traefik-acme: diff --git a/compose.yml b/compose.yml index a944039..358e72a 100644 --- a/compose.yml +++ b/compose.yml @@ -1,19 +1,21 @@ name: "plumber" x-backend-common: &backend-common - image: docker.io/getplumber/backend:$BACKEND_IMAGE_TAG + image: docker.io/getplumber/backend:${BACKEND_IMAGE_TAG} env_file: .env environment: - JOBS_LISTEN_ADDR=0.0.0.0 - JOBS_LISTEN_PORT=80 - - JOBS_CORS_ORIGIN=https://$DOMAIN_NAME - - JOBS_FRONTEND_URL=https://$DOMAIN_NAME + - JOBS_CORS_ORIGIN=https://${DOMAIN_NAME} + - JOBS_FRONTEND_URL=https://${DOMAIN_NAME} - JOBS_SESSION_TTL=168h - - JOBS_DB_HOST=postgres - - JOBS_DB_PORT=5432 - - JOBS_DB_SSLMODE=disable - - JOBS_DB_TIMEZONE=Europe/Paris - - JOBS_API_DOMAIN=https://$API_URL + - JOBS_DB_HOST=${JOBS_DB_HOST:-postgres} + - JOBS_DB_PORT=${JOBS_DB_PORT:-5432} + - JOBS_DB_USER=${JOBS_DB_USER:-jobs} + - JOBS_DB_NAME=${JOBS_DB_NAME:-jobs} + - JOBS_DB_SSLMODE=${JOBS_DB_SSLMODE:-disable} + - JOBS_DB_TIMEZONE=${JOBS_DB_TIMEZONE:-Europe/Paris} + - JOBS_API_DOMAIN=https://${DOMAIN_NAME}/api - LOG_LEVEL=info - LOG_FORMATTER=text - JOBS_REDIS_HOST=redis @@ -22,10 +24,15 @@ x-backend-common: &backend-common - JOBS_REDIS_USER=default - JOBS_REDIS_SET_NAMESPACES_TTL=30s - GITLEAKS_PATH=/opt/gitleaks + - FRONTEND_DOMAIN=${DOMAIN_NAME} + - API_DOMAIN=${DOMAIN_NAME} + - API_PATH=/api + - API_URL=${DOMAIN_NAME}/api restart: unless-stopped depends_on: postgres: condition: service_healthy + required: false redis: condition: service_healthy volumes: @@ -40,15 +47,20 @@ services: ############ frontend: - image: docker.io/getplumber/frontend:$FRONTEND_IMAGE_TAG + image: docker.io/getplumber/frontend:${FRONTEND_IMAGE_TAG} env_file: .env + environment: + - FRONTEND_DOMAIN=${DOMAIN_NAME} + - API_DOMAIN=${DOMAIN_NAME} + - API_PATH=/api + - API_URL=${DOMAIN_NAME}/api restart: unless-stopped volumes: - ./.docker/ca-certificates:/usr/local/share/ca-certificates/ expose: - "3000" labels: - - "traefik.http.routers.front.rule=Host(`${FRONTEND_DOMAIN}`)" + - "traefik.http.routers.front.rule=Host(`${DOMAIN_NAME}`)" - "traefik.http.routers.front.entrypoints=websecure" - "traefik.http.routers.front.tls=true" - "traefik.http.routers.front.tls.certresolver=le" @@ -61,7 +73,7 @@ services: - "80" - "9090" labels: - - "traefik.http.routers.api.rule=Host(\"$API_DOMAIN\")&&PathPrefix(\"$API_PATH\")" + - "traefik.http.routers.api.rule=Host(`${DOMAIN_NAME}`)&&PathPrefix(`/api`)" - "traefik.http.routers.api.entrypoints=websecure" - "traefik.http.routers.api.tls=true" - "traefik.http.routers.api.tls.certresolver=le" @@ -90,7 +102,7 @@ services: - .env command: - redis-server - - --requirepass $JOBS_REDIS_PASSWORD + - --requirepass ${JOBS_REDIS_PASSWORD} healthcheck: test: ["CMD-SHELL", "redis-cli -a \"$$JOBS_REDIS_PASSWORD\" ping"] interval: 5s @@ -99,10 +111,13 @@ services: start_period: 5s expose: - "6379" + labels: + - "traefik.enable=false" networks: - intranet postgres: + profiles: ["internal-db"] image: postgres:18 pull_policy: always restart: unless-stopped @@ -110,10 +125,12 @@ services: - postgres-data:/var/lib/postgresql env_file: .env environment: - - POSTGRES_USER=$JOBS_DB_USER - - POSTGRES_PASSWORD=$JOBS_DB_PASSWORD - - POSTGRES_DB=$JOBS_DB_NAME + - POSTGRES_USER=${JOBS_DB_USER:-jobs} + - POSTGRES_PASSWORD=${JOBS_DB_PASSWORD} + - POSTGRES_DB=${JOBS_DB_NAME:-jobs} - PGDATA=/var/lib/postgresql/18/data + labels: + - "traefik.enable=false" healthcheck: test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB -h 127.0.0.1 -p 5432"] interval: 5s @@ -129,7 +146,8 @@ services: # Reverse proxy # ################# - traefik: + traefik-le: + profiles: ["letsencrypt"] image: traefik:3.6 pull_policy: always restart: unless-stopped @@ -153,16 +171,42 @@ services: - --certificatesresolvers.le.acme.tlschallenge=true labels: - "traefik.enable=false" - - "traefik.http.routers.traefik.entrypoints=websecure" - - "traefik.http.routers.traefik.tls=true" - - "traefik.http.routers.traefik.tls.certresolver=le" - - "traefik.http.routers.traefik.service=api@internal" volumes: - /var/run/docker.sock:/var/run/docker.sock - traefik-acme:/acme networks: - intranet + traefik-custom-certs: + profiles: ["custom-certs"] + image: traefik:3.6 + pull_policy: always + restart: unless-stopped + depends_on: + - frontend + - backend + ports: + - "80:80" + - "443:443" + command: + - --log.level=DEBUG + - --accesslog=true + - --entrypoints.web.address=:80 + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --entrypoints.web.http.redirections.entrypoint.permanent=true + - --entrypoints.websecure.address=:443 + - --providers.docker=true + - --providers.file.filename=/etc/traefik/certs.yml + labels: + - "traefik.enable=false" + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ./.docker/traefik/certs:/certs + - ./.docker/traefik/certs.yml:/etc/traefik/certs.yml + networks: + - intranet + networks: intranet: diff --git a/install.sh b/install.sh new file mode 100755 index 0000000..7036b34 --- /dev/null +++ b/install.sh @@ -0,0 +1,408 @@ +#!/bin/bash + +# Plumber Installer +# Interactive setup wizard for self-managed Plumber instances. +# +# Usage: +# # Option A: One-liner (clones the repo automatically) +# curl -fsSL https://raw.githubusercontent.com/getplumber/platform/main/install.sh | bash +# +# # Option B: From a cloned repository +# git clone https://github.com/getplumber/platform.git plumber-platform +# cd plumber-platform +# ./install.sh + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +DIM='\033[2m' +NC='\033[0m' + +REPO_URL="https://github.com/getplumber/platform.git" +REPO_DIR="plumber-platform" + +# ============================================================================= +# Helpers +# ============================================================================= + +prompt() { + local VARNAME="$1" + local MESSAGE="$2" + local DEFAULT="${3:-}" + local VALUE="" + + while true; do + if [ -n "$DEFAULT" ]; then + echo -ne "${BOLD}${MESSAGE}${NC} ${DIM}(${DEFAULT})${NC}: " + else + echo -ne "${BOLD}${MESSAGE}${NC}: " + fi + read -r VALUE + VALUE="${VALUE:-$DEFAULT}" + + if [ -n "$VALUE" ]; then + eval "$VARNAME=\"$VALUE\"" + return + fi + echo -e "${RED} This field is required.${NC}" + done +} + +prompt_secret() { + local VARNAME="$1" + local MESSAGE="$2" + local VALUE="" + local CHAR="" + + while true; do + VALUE="" + echo -ne "${BOLD}${MESSAGE}${NC}: " + while IFS= read -rs -n1 CHAR; do + if [[ -z "$CHAR" ]]; then + break + elif [[ "$CHAR" == $'\x7f' ]] || [[ "$CHAR" == $'\b' ]]; then + if [ -n "$VALUE" ]; then + VALUE="${VALUE%?}" + echo -ne "\b \b" + fi + else + VALUE+="$CHAR" + echo -ne "*" + fi + done + echo "" + + if [ -n "$VALUE" ]; then + eval "$VARNAME=\"$VALUE\"" + return + fi + echo -e "${RED} This field is required.${NC}" + done +} + +prompt_optional() { + local VARNAME="$1" + local MESSAGE="$2" + local DEFAULT="${3:-}" + + if [ -n "$DEFAULT" ]; then + echo -ne "${BOLD}${MESSAGE}${NC} ${DIM}(${DEFAULT})${NC}: " + else + echo -ne "${BOLD}${MESSAGE}${NC} ${DIM}(leave empty to skip)${NC}: " + fi + read -r VALUE + VALUE="${VALUE:-$DEFAULT}" + eval "$VARNAME=\"$VALUE\"" +} + +prompt_choice() { + local VARNAME="$1" + local MESSAGE="$2" + shift 2 + local OPTIONS=("$@") + local NUM_OPTIONS=${#OPTIONS[@]} + + echo -e "${BOLD}${MESSAGE}${NC}" + for i in "${!OPTIONS[@]}"; do + echo " $((i + 1)). ${OPTIONS[$i]}" + done + + while true; do + echo -ne "${BOLD}Choice${NC} ${DIM}(1-${NUM_OPTIONS})${NC}: " + read -r CHOICE + if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "$NUM_OPTIONS" ]; then + eval "$VARNAME=\"$CHOICE\"" + return + fi + echo -e "${RED} Please enter a number between 1 and ${NUM_OPTIONS}.${NC}" + done +} + +prompt_confirm() { + local MESSAGE="$1" + local DEFAULT="${2:-Y}" + + if [ "$DEFAULT" = "Y" ]; then + echo -ne "${BOLD}${MESSAGE}${NC} ${DIM}(Y/n)${NC}: " + else + echo -ne "${BOLD}${MESSAGE}${NC} ${DIM}(y/N)${NC}: " + fi + read -r REPLY + REPLY="${REPLY:-$DEFAULT}" + + case "$REPLY" in + [yY][eE][sS]|[yY]) return 0 ;; + *) return 1 ;; + esac +} + +# ============================================================================= +# Step 1: Detect context and clone if needed +# ============================================================================= + +echo "" +echo -e "${BOLD}╔══════════════════════════════════════╗${NC}" +echo -e "${BOLD}║ Plumber Installer ║${NC}" +echo -e "${BOLD}╚══════════════════════════════════════╝${NC}" +echo "" + +if [ ! -f compose.yml ] || [ ! -f versions.env ]; then + echo "Plumber repository not detected. Cloning..." + echo "" + + # Check git is available + if ! command -v git &> /dev/null; then + echo -e "${RED}Error:${NC} Git is required but not installed." + echo " Install it: https://git-scm.com/downloads" + exit 1 + fi + + # Check docker is available + if ! command -v docker &> /dev/null; then + echo -e "${RED}Error:${NC} Docker is required but not installed." + echo " Install it: https://docs.docker.com/get-docker/" + exit 1 + fi + + git clone "$REPO_URL" "$REPO_DIR" + cd "$REPO_DIR" + echo "" + echo -e "${GREEN}✓${NC} Repository cloned to $(pwd)" + echo "" +fi + +# ============================================================================= +# Step 2: Run pre-config checks +# ============================================================================= + +echo "Running pre-flight checks..." +if ! bash scripts/preflight.sh --pre; then + echo "" + echo -e "${RED}Pre-flight checks failed. Please fix the issues above and try again.${NC}" + exit 1 +fi + +# ============================================================================= +# Step 3: Interactive configuration +# ============================================================================= + +echo "" +echo -e "${BOLD}Configuration${NC}" +echo "───────────────────────────────────────" +echo "" + +# Domain name +prompt DOMAIN_NAME "Plumber domain name (e.g. plumber.example.com)" + +# GitLab URL +prompt JOBS_GITLAB_URL "GitLab instance URL (e.g. https://gitlab.example.com)" +# Strip trailing slash +JOBS_GITLAB_URL="${JOBS_GITLAB_URL%/}" + +# Organization +echo "" +echo -e "${DIM}Leave empty to connect Plumber to the entire GitLab instance.${NC}" +echo -e "${DIM}Or enter a group path to limit Plumber to that group.${NC}" +prompt_optional ORGANIZATION "GitLab group path" + +# GitLab OIDC +echo "" +echo "───────────────────────────────────────" +echo -e "${BOLD}GitLab OIDC Application${NC}" +echo "" + +# Ensure GitLab URL has https:// scheme for clickable terminal links +GITLAB_BASE_URL="${JOBS_GITLAB_URL}" +if [[ ! "$GITLAB_BASE_URL" =~ ^https?:// ]]; then + GITLAB_BASE_URL="https://${GITLAB_BASE_URL}" +fi + +# Build direct link to the GitLab application creation page +if [ -n "${ORGANIZATION}" ]; then + GITLAB_APP_URL="${GITLAB_BASE_URL}/groups/${ORGANIZATION}/-/settings/applications" +else + GITLAB_APP_URL="${GITLAB_BASE_URL}/admin/applications" +fi + +echo " 1. Open this link to create a new application:" +echo "" +echo -e " ${BOLD}${GITLAB_APP_URL}${NC}" +echo "" +echo " 2. Fill in the following:" +echo -e " - Name: ${BOLD}Plumber${NC}" +echo -e " - Redirect URI: ${BOLD}https://${DOMAIN_NAME}/api/auth/gitlab/callback${NC}" +echo -e " - Confidential: ${BOLD}yes${NC} (keep the box checked)" +echo -e " - Scopes: ${BOLD}api${NC}" +echo "" +echo " 3. Click Save and copy the credentials below" +echo "" + +prompt GITLAB_OAUTH2_CLIENT_ID "Application ID" +prompt_secret GITLAB_OAUTH2_CLIENT_SECRET "Secret" + +# Certificate method +echo "" +echo "───────────────────────────────────────" +prompt_choice CERT_CHOICE "TLS certificate method:" \ + "Let's Encrypt (automatic, server must be reachable from internet)" \ + "Custom certificates (provide your own .pem files)" + +if [ "$CERT_CHOICE" = "1" ]; then + CERT_PROFILE="letsencrypt" +else + CERT_PROFILE="custom-certs" + echo "" + echo -e "${DIM}Place your certificate files at:${NC}" + echo " .docker/traefik/certs/plumber_fullchain.pem" + echo " .docker/traefik/certs/plumber_privkey.pem" + + if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then + echo -e " ${GREEN}✓${NC} Certificate files found" + else + echo -e " ${YELLOW}!${NC} Certificate files not found yet (add them before starting)" + fi +fi + +# Database +echo "" +echo "───────────────────────────────────────" +prompt_choice DB_CHOICE "Database:" \ + "Internal (managed PostgreSQL container)" \ + "External (connect to your own PostgreSQL)" + +if [ "$DB_CHOICE" = "1" ]; then + DB_PROFILE=",internal-db" + EXT_DB_VARS="" +else + DB_PROFILE="" + echo "" + prompt JOBS_DB_HOST "Database host" + prompt_optional JOBS_DB_PORT "Database port" "5432" + prompt_optional JOBS_DB_USER "Database user" "jobs" + prompt_optional JOBS_DB_NAME "Database name" "jobs" + prompt_optional JOBS_DB_SSLMODE "SSL mode" "disable" + prompt_optional JOBS_DB_TIMEZONE "Timezone" "Europe/Paris" + + EXT_DB_VARS=$(cat < .env < /dev/null; then + if docker info &> /dev/null; then + pass "Docker is installed and running" + else + fail "Docker is installed but not running (start Docker daemon)" + fi + else + fail "Docker is not installed (https://docs.docker.com/get-docker/)" + fi + + # Check Docker Compose v2.20.2+ is available + if docker compose version &> /dev/null; then + COMPOSE_VERSION=$(docker compose version --short 2>/dev/null || echo "0.0.0") + MAJOR=$(echo "$COMPOSE_VERSION" | cut -d. -f1 | sed 's/v//') + MINOR=$(echo "$COMPOSE_VERSION" | cut -d. -f2) + PATCH=$(echo "$COMPOSE_VERSION" | cut -d. -f3) + + if [ "$MAJOR" -gt 2 ] || ([ "$MAJOR" -eq 2 ] && [ "$MINOR" -gt 20 ]) || ([ "$MAJOR" -eq 2 ] && [ "$MINOR" -eq 20 ] && [ "$PATCH" -ge 2 ]); then + pass "Docker Compose v${COMPOSE_VERSION} (>= 2.20.2 required)" + else + fail "Docker Compose v${COMPOSE_VERSION} is too old (>= 2.20.2 required)" + fi + else + fail "Docker Compose plugin is not installed (https://docs.docker.com/compose/install/)" + fi + + # Check git is available + if command -v git &> /dev/null; then + pass "Git is installed" + else + fail "Git is not installed" + fi + + # Check openssl is available (needed for secret generation) + if command -v openssl &> /dev/null; then + pass "OpenSSL is installed (for secret generation)" + else + fail "OpenSSL is not installed (needed to generate secrets)" + fi + + # Check ports 80 and 443 are available + for PORT in 80 443; do + if command -v ss &> /dev/null; then + if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then + fail "Port ${PORT} is already in use" + else + pass "Port ${PORT} is available" + fi + elif command -v lsof &> /dev/null; then + if lsof -i ":${PORT}" -sTCP:LISTEN &> /dev/null; then + fail "Port ${PORT} is already in use" + else + pass "Port ${PORT} is available" + fi + else + warn "Port ${PORT}: cannot check (install lsof or ss)" + fi + done +} + +# ============================================================================= +# Post-config checks (.env required) +# ============================================================================= +run_post_checks() { + echo "" + echo "Post-config checks" + echo "───────────────────────────────────────" + + # Check .env exists + if [ ! -f .env ]; then + fail ".env file does not exist (run ./install.sh first)" + return + fi + pass ".env file exists" + + # Source .env for validation + set -a + source .env + set +a + + # Check required variables are set and non-empty + REQUIRED_VARS="DOMAIN_NAME JOBS_GITLAB_URL GITLAB_OAUTH2_CLIENT_ID GITLAB_OAUTH2_CLIENT_SECRET SECRET_KEY JOBS_DB_PASSWORD JOBS_REDIS_PASSWORD COMPOSE_PROFILES" + for VAR in $REQUIRED_VARS; do + VALUE="${!VAR:-}" + if [ -z "$VALUE" ]; then + fail "$VAR is not set" + elif echo "$VALUE" | grep -qi "REPLACE_ME"; then + fail "$VAR still contains a placeholder value" + else + pass "$VAR is set" + fi + done + + # Check image tags are present + for VAR in FRONTEND_IMAGE_TAG BACKEND_IMAGE_TAG; do + VALUE="${!VAR:-}" + if [ -z "$VALUE" ]; then + fail "$VAR is not set (run ./install.sh or ./scripts/update.sh)" + else + pass "$VAR=${VALUE}" + fi + done + + # Check COMPOSE_PROFILES is valid + PROFILES="${COMPOSE_PROFILES:-}" + if [ -n "$PROFILES" ]; then + HAS_TRAEFIK=false + if echo "$PROFILES" | grep -q "letsencrypt"; then + HAS_TRAEFIK=true + fi + if echo "$PROFILES" | grep -q "custom-certs"; then + HAS_TRAEFIK=true + fi + if [ "$HAS_TRAEFIK" = false ]; then + fail "COMPOSE_PROFILES must include 'letsencrypt' or 'custom-certs'" + else + pass "COMPOSE_PROFILES has a valid traefik profile" + fi + fi + + # Check DNS resolution for DOMAIN_NAME + DOMAIN="${DOMAIN_NAME:-}" + if [ -n "$DOMAIN" ]; then + if command -v dig &> /dev/null; then + if dig +short "$DOMAIN" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then + pass "DNS resolves for ${DOMAIN}" + else + warn "DNS does not resolve for ${DOMAIN} (ensure your DNS record is configured)" + fi + elif command -v nslookup &> /dev/null; then + if nslookup "$DOMAIN" &> /dev/null; then + pass "DNS resolves for ${DOMAIN}" + else + warn "DNS does not resolve for ${DOMAIN} (ensure your DNS record is configured)" + fi + else + warn "Cannot check DNS (install dig or nslookup)" + fi + fi + + # Check GitLab URL is reachable + GITLAB_URL="${JOBS_GITLAB_URL:-}" + if [ -n "$GITLAB_URL" ]; then + if curl -sf --max-time 10 "${GITLAB_URL}" -o /dev/null 2>/dev/null; then + pass "GitLab instance is reachable at ${GITLAB_URL}" + else + warn "Cannot reach GitLab at ${GITLAB_URL} (check URL and network)" + fi + fi + + # Check custom cert files exist if custom-certs profile is active + if echo "${COMPOSE_PROFILES:-}" | grep -q "custom-certs"; then + if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then + pass "Custom certificate files found" + else + fail "Custom certificates profile is active but cert files are missing" + echo -e " Expected: .docker/traefik/certs/plumber_fullchain.pem" + echo -e " Expected: .docker/traefik/certs/plumber_privkey.pem" + fi + fi +} + +# ============================================================================= +# Main +# ============================================================================= + +# Determine which checks to run based on arguments +MODE="${1:-all}" + +case "$MODE" in + --pre) + run_pre_checks + ;; + --post) + run_post_checks + ;; + *) + run_pre_checks + run_post_checks + ;; +esac + +echo "" +if [ "$ERRORS" -gt 0 ]; then + echo -e "${RED}$ERRORS check(s) failed.${NC} Please fix the issues above before proceeding." + exit 1 +else + echo -e "${GREEN}All checks passed.${NC}" + exit 0 +fi diff --git a/scripts/update.sh b/scripts/update.sh new file mode 100755 index 0000000..7054e0e --- /dev/null +++ b/scripts/update.sh @@ -0,0 +1,89 @@ +#!/bin/bash + +# Plumber Update Script +# Updates your self-managed Plumber instance to the latest version. +# +# Usage: +# ./scripts/update.sh +# +# This script: +# 1. Pulls the latest changes from the git repository +# 2. Syncs image tags from versions.env into .env +# 3. Restarts containers with the new images + +set -euo pipefail + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BOLD='\033[1m' +NC='\033[0m' + +# Ensure we're in the repository root +if [ ! -f compose.yml ] || [ ! -f versions.env ]; then + echo -e "${RED}Error:${NC} This script must be run from the Plumber platform repository root." + exit 1 +fi + +# Ensure .env exists +if [ ! -f .env ]; then + echo -e "${RED}Error:${NC} .env file not found. Run ./install.sh first." + exit 1 +fi + +echo "" +echo -e "${BOLD}Updating Plumber...${NC}" +echo "───────────────────────────────────────" + +# Step 1: Pull latest changes +echo "" +echo "Pulling latest changes..." +git pull +echo -e "${GREEN}✓${NC} Repository updated" + +# Step 2: Read new image tags from versions.env +echo "" +echo "Syncing image versions..." +source versions.env + +if [ -z "${FRONTEND_IMAGE_TAG:-}" ] || [ -z "${BACKEND_IMAGE_TAG:-}" ]; then + echo -e "${RED}Error:${NC} versions.env is missing image tags." + exit 1 +fi + +# Update or append FRONTEND_IMAGE_TAG in .env +if grep -q "^FRONTEND_IMAGE_TAG=" .env 2>/dev/null; then + sed -i."" "s|^FRONTEND_IMAGE_TAG=.*|FRONTEND_IMAGE_TAG=${FRONTEND_IMAGE_TAG}|" .env + rm -f .env."" 2>/dev/null || true +else + echo "" >> .env + echo "FRONTEND_IMAGE_TAG=${FRONTEND_IMAGE_TAG}" >> .env +fi + +# Update or append BACKEND_IMAGE_TAG in .env +if grep -q "^BACKEND_IMAGE_TAG=" .env 2>/dev/null; then + sed -i."" "s|^BACKEND_IMAGE_TAG=.*|BACKEND_IMAGE_TAG=${BACKEND_IMAGE_TAG}|" .env + rm -f .env."" 2>/dev/null || true +else + echo "BACKEND_IMAGE_TAG=${BACKEND_IMAGE_TAG}" >> .env +fi + +# Ensure COMPOSE_PROFILES is set (migration for users upgrading from old format) +if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then + echo -e "${YELLOW}!${NC} COMPOSE_PROFILES not found in .env, adding default (letsencrypt,internal-db)" + echo "" >> .env + echo "COMPOSE_PROFILES=letsencrypt,internal-db" >> .env +fi + +echo -e "${GREEN}✓${NC} Frontend: ${FRONTEND_IMAGE_TAG}" +echo -e "${GREEN}✓${NC} Backend: ${BACKEND_IMAGE_TAG}" + +# Step 3: Restart containers +echo "" +echo "Restarting containers..." +docker compose up -d + +echo "" +echo -e "${GREEN}✓ Plumber has been updated successfully!${NC}" +echo "" diff --git a/versions.env b/versions.env new file mode 100644 index 0000000..72a0a6c --- /dev/null +++ b/versions.env @@ -0,0 +1,2 @@ +FRONTEND_IMAGE_TAG=v2.32.4 +BACKEND_IMAGE_TAG=v2.36.4 From e34a4bc8dc160a7ae895d65e645bd986e8c03779 Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Fri, 13 Feb 2026 16:17:59 +0100 Subject: [PATCH 02/14] conf(all): ensure backward compatibility --- scripts/update.sh | 139 +++++++++++++++++++++++++++++++++++++--------- 1 file changed, 114 insertions(+), 25 deletions(-) diff --git a/scripts/update.sh b/scripts/update.sh index 7054e0e..ece7ab4 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -9,7 +9,8 @@ # This script: # 1. Pulls the latest changes from the git repository # 2. Syncs image tags from versions.env into .env -# 3. Restarts containers with the new images +# 3. Migrates old .env format if needed (auto-detects COMPOSE_PROFILES) +# 4. Restarts containers with the new images set -euo pipefail @@ -18,6 +19,7 @@ RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BOLD='\033[1m' +DIM='\033[2m' NC='\033[0m' # Ensure we're in the repository root @@ -32,17 +34,35 @@ if [ ! -f .env ]; then exit 1 fi +# ============================================================================= +# Helper: update or append a variable in .env +# ============================================================================= +set_env_var() { + local KEY="$1" + local VALUE="$2" + if grep -q "^${KEY}=" .env 2>/dev/null; then + sed -i."" "s|^${KEY}=.*|${KEY}=${VALUE}|" .env + rm -f .env."" 2>/dev/null || true + else + echo "${KEY}=${VALUE}" >> .env + fi +} + echo "" echo -e "${BOLD}Updating Plumber...${NC}" echo "───────────────────────────────────────" +# ============================================================================= # Step 1: Pull latest changes +# ============================================================================= echo "" echo "Pulling latest changes..." git pull echo -e "${GREEN}✓${NC} Repository updated" -# Step 2: Read new image tags from versions.env +# ============================================================================= +# Step 2: Sync image tags from versions.env +# ============================================================================= echo "" echo "Syncing image versions..." source versions.env @@ -52,34 +72,103 @@ if [ -z "${FRONTEND_IMAGE_TAG:-}" ] || [ -z "${BACKEND_IMAGE_TAG:-}" ]; then exit 1 fi -# Update or append FRONTEND_IMAGE_TAG in .env -if grep -q "^FRONTEND_IMAGE_TAG=" .env 2>/dev/null; then - sed -i."" "s|^FRONTEND_IMAGE_TAG=.*|FRONTEND_IMAGE_TAG=${FRONTEND_IMAGE_TAG}|" .env - rm -f .env."" 2>/dev/null || true -else - echo "" >> .env - echo "FRONTEND_IMAGE_TAG=${FRONTEND_IMAGE_TAG}" >> .env -fi +set_env_var "FRONTEND_IMAGE_TAG" "${FRONTEND_IMAGE_TAG}" +set_env_var "BACKEND_IMAGE_TAG" "${BACKEND_IMAGE_TAG}" -# Update or append BACKEND_IMAGE_TAG in .env -if grep -q "^BACKEND_IMAGE_TAG=" .env 2>/dev/null; then - sed -i."" "s|^BACKEND_IMAGE_TAG=.*|BACKEND_IMAGE_TAG=${BACKEND_IMAGE_TAG}|" .env - rm -f .env."" 2>/dev/null || true -else - echo "BACKEND_IMAGE_TAG=${BACKEND_IMAGE_TAG}" >> .env -fi +echo -e "${GREEN}✓${NC} Frontend: ${FRONTEND_IMAGE_TAG}" +echo -e "${GREEN}✓${NC} Backend: ${BACKEND_IMAGE_TAG}" -# Ensure COMPOSE_PROFILES is set (migration for users upgrading from old format) +# ============================================================================= +# Step 3: Migrate COMPOSE_PROFILES if missing (old .env format) +# ============================================================================= if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then - echo -e "${YELLOW}!${NC} COMPOSE_PROFILES not found in .env, adding default (letsencrypt,internal-db)" - echo "" >> .env - echo "COMPOSE_PROFILES=letsencrypt,internal-db" >> .env -fi + echo "" + echo -e "${YELLOW}Migration required:${NC} COMPOSE_PROFILES is not set in your .env file." + echo "" + echo -e "${DIM}Starting with this version, Plumber uses Docker Compose profiles instead of${NC}" + echo -e "${DIM}separate compose files. Your .env needs a COMPOSE_PROFILES variable.${NC}" + echo "" -echo -e "${GREEN}✓${NC} Frontend: ${FRONTEND_IMAGE_TAG}" -echo -e "${GREEN}✓${NC} Backend: ${BACKEND_IMAGE_TAG}" + # Source .env to read current config for auto-detection + set -a + source .env + set +a + + # --- Auto-detect TLS method --- + if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then + DETECTED_CERT="custom-certs" + CERT_REASON="custom certificate files found in .docker/traefik/certs/" + else + DETECTED_CERT="letsencrypt" + CERT_REASON="no custom certificate files found (using Let's Encrypt)" + fi + + # --- Auto-detect database --- + CURRENT_DB_HOST="${JOBS_DB_HOST:-postgres}" + if [ "$CURRENT_DB_HOST" != "postgres" ]; then + DETECTED_DB="" + DB_REASON="JOBS_DB_HOST is set to '${CURRENT_DB_HOST}' (external database)" + else + DETECTED_DB=",internal-db" + DB_REASON="JOBS_DB_HOST is 'postgres' or unset (internal database)" + fi + + DETECTED_PROFILES="${DETECTED_CERT}${DETECTED_DB}" + + echo " Auto-detected configuration:" + echo -e " TLS: ${BOLD}${DETECTED_CERT}${NC} ${DIM}(${CERT_REASON})${NC}" + if [ -n "$DETECTED_DB" ]; then + echo -e " Database: ${BOLD}internal${NC} ${DIM}(${DB_REASON})${NC}" + else + echo -e " Database: ${BOLD}external${NC} ${DIM}(${DB_REASON})${NC}" + fi + echo "" + echo -e " COMPOSE_PROFILES=${BOLD}${DETECTED_PROFILES}${NC}" + echo "" + + while true; do + echo -ne "${BOLD}Is this correct?${NC} ${DIM}(Y/n)${NC}: " + read -r REPLY + REPLY="${REPLY:-Y}" + case "$REPLY" in + [yY][eE][sS]|[yY]) + set_env_var "COMPOSE_PROFILES" "${DETECTED_PROFILES}" + echo -e "${GREEN}✓${NC} COMPOSE_PROFILES=${DETECTED_PROFILES} added to .env" + break + ;; + [nN][oO]|[nN]) + echo "" + echo " Available profiles:" + echo " 1. letsencrypt,internal-db (Let's Encrypt + managed PostgreSQL)" + echo " 2. custom-certs,internal-db (Custom certs + managed PostgreSQL)" + echo " 3. letsencrypt (Let's Encrypt + external PostgreSQL)" + echo " 4. custom-certs (Custom certs + external PostgreSQL)" + echo "" + while true; do + echo -ne "${BOLD}Choice${NC} ${DIM}(1-4)${NC}: " + read -r CHOICE + case "$CHOICE" in + 1) PROFILES="letsencrypt,internal-db"; break ;; + 2) PROFILES="custom-certs,internal-db"; break ;; + 3) PROFILES="letsencrypt"; break ;; + 4) PROFILES="custom-certs"; break ;; + *) echo -e "${RED} Please enter a number between 1 and 4.${NC}" ;; + esac + done + set_env_var "COMPOSE_PROFILES" "${PROFILES}" + echo -e "${GREEN}✓${NC} COMPOSE_PROFILES=${PROFILES} added to .env" + break + ;; + *) + echo -e "${RED} Please answer Y or N.${NC}" + ;; + esac + done +fi -# Step 3: Restart containers +# ============================================================================= +# Step 4: Restart containers +# ============================================================================= echo "" echo "Restarting containers..." docker compose up -d From a2f0ec2c7eacfdfcc8834c9503586e8689bdab5f Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Mon, 16 Feb 2026 11:20:04 +0100 Subject: [PATCH 03/14] fix(compose): fix custom certificates usage --- .env.example | 1 + compose.yml | 4 ++-- install.sh | 3 +++ scripts/update.sh | 25 +++++++++++++++++++++---- 4 files changed, 27 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index 3b5d4a8..ba9bc13 100644 --- a/.env.example +++ b/.env.example @@ -18,6 +18,7 @@ JOBS_REDIS_PASSWORD="" # Deployment profile (set by install script) # Options: letsencrypt or custom-certs, combined with internal-db or omit for external COMPOSE_PROFILES="letsencrypt,internal-db" +CERT_RESOLVER="le" # Optional: Override database defaults (only needed when NOT using internal-db profile) # JOBS_DB_HOST="your-db-host" diff --git a/compose.yml b/compose.yml index 358e72a..7967d52 100644 --- a/compose.yml +++ b/compose.yml @@ -63,7 +63,7 @@ services: - "traefik.http.routers.front.rule=Host(`${DOMAIN_NAME}`)" - "traefik.http.routers.front.entrypoints=websecure" - "traefik.http.routers.front.tls=true" - - "traefik.http.routers.front.tls.certresolver=le" + - "traefik.http.routers.front.tls.certresolver=${CERT_RESOLVER:-}" networks: - intranet @@ -76,7 +76,7 @@ services: - "traefik.http.routers.api.rule=Host(`${DOMAIN_NAME}`)&&PathPrefix(`/api`)" - "traefik.http.routers.api.entrypoints=websecure" - "traefik.http.routers.api.tls=true" - - "traefik.http.routers.api.tls.certresolver=le" + - "traefik.http.routers.api.tls.certresolver=${CERT_RESOLVER:-}" worker: <<: *backend-common diff --git a/install.sh b/install.sh index 7036b34..c386004 100755 --- a/install.sh +++ b/install.sh @@ -253,8 +253,10 @@ prompt_choice CERT_CHOICE "TLS certificate method:" \ if [ "$CERT_CHOICE" = "1" ]; then CERT_PROFILE="letsencrypt" + CERT_RESOLVER="le" else CERT_PROFILE="custom-certs" + CERT_RESOLVER="" echo "" echo -e "${DIM}Place your certificate files at:${NC}" echo " .docker/traefik/certs/plumber_fullchain.pem" @@ -355,6 +357,7 @@ JOBS_REDIS_PASSWORD="${JOBS_REDIS_PASSWORD}" # Deployment profile COMPOSE_PROFILES="${COMPOSE_PROFILES}" +CERT_RESOLVER="${CERT_RESOLVER}" # Image versions (managed by scripts/update.sh) FRONTEND_IMAGE_TAG="${FRONTEND_IMAGE_TAG}" diff --git a/scripts/update.sh b/scripts/update.sh index ece7ab4..3457882 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -133,6 +133,11 @@ if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then case "$REPLY" in [yY][eE][sS]|[yY]) set_env_var "COMPOSE_PROFILES" "${DETECTED_PROFILES}" + if [ "$DETECTED_CERT" = "letsencrypt" ]; then + set_env_var "CERT_RESOLVER" "le" + else + set_env_var "CERT_RESOLVER" "" + fi echo -e "${GREEN}✓${NC} COMPOSE_PROFILES=${DETECTED_PROFILES} added to .env" break ;; @@ -148,14 +153,15 @@ if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then echo -ne "${BOLD}Choice${NC} ${DIM}(1-4)${NC}: " read -r CHOICE case "$CHOICE" in - 1) PROFILES="letsencrypt,internal-db"; break ;; - 2) PROFILES="custom-certs,internal-db"; break ;; - 3) PROFILES="letsencrypt"; break ;; - 4) PROFILES="custom-certs"; break ;; + 1) PROFILES="letsencrypt,internal-db"; RESOLVER="le"; break ;; + 2) PROFILES="custom-certs,internal-db"; RESOLVER=""; break ;; + 3) PROFILES="letsencrypt"; RESOLVER="le"; break ;; + 4) PROFILES="custom-certs"; RESOLVER=""; break ;; *) echo -e "${RED} Please enter a number between 1 and 4.${NC}" ;; esac done set_env_var "COMPOSE_PROFILES" "${PROFILES}" + set_env_var "CERT_RESOLVER" "${RESOLVER}" echo -e "${GREEN}✓${NC} COMPOSE_PROFILES=${PROFILES} added to .env" break ;; @@ -166,6 +172,17 @@ if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then done fi +# Ensure CERT_RESOLVER is set (derive from COMPOSE_PROFILES if missing) +if ! grep -q "^CERT_RESOLVER=" .env 2>/dev/null; then + source .env + if echo "${COMPOSE_PROFILES:-}" | grep -q "letsencrypt"; then + set_env_var "CERT_RESOLVER" "le" + else + set_env_var "CERT_RESOLVER" "" + fi + echo -e "${GREEN}✓${NC} CERT_RESOLVER added to .env" +fi + # ============================================================================= # Step 4: Restart containers # ============================================================================= From e4bc27c9943012360d347adf5e2dec0676285d8e Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Mon, 16 Feb 2026 11:52:42 +0100 Subject: [PATCH 04/14] fix(compose): fix the external db interactive setup --- install.sh | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/install.sh b/install.sh index c386004..25a8fea 100755 --- a/install.sh +++ b/install.sh @@ -284,10 +284,16 @@ else echo "" prompt JOBS_DB_HOST "Database host" prompt_optional JOBS_DB_PORT "Database port" "5432" - prompt_optional JOBS_DB_USER "Database user" "jobs" - prompt_optional JOBS_DB_NAME "Database name" "jobs" + prompt JOBS_DB_USER "Database user" + prompt_optional JOBS_DB_NAME "Database name" "plumber" + prompt_secret JOBS_DB_PASSWORD_EXT "Database password" + echo "" + echo -e "${DIM}SSL mode options: disable, require, verify-ca${NC}" prompt_optional JOBS_DB_SSLMODE "SSL mode" "disable" - prompt_optional JOBS_DB_TIMEZONE "Timezone" "Europe/Paris" + + # Detect server timezone + SERVER_TZ=$(timedatectl show --property=Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo "UTC") + prompt_optional JOBS_DB_TIMEZONE "Timezone" "${SERVER_TZ}" EXT_DB_VARS=$(cat < Date: Mon, 16 Feb 2026 12:22:04 +0100 Subject: [PATCH 05/14] conf(compose): add custom CA in install script --- install.sh | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/install.sh b/install.sh index 25a8fea..f806f94 100755 --- a/install.sh +++ b/install.sh @@ -267,6 +267,32 @@ else else echo -e " ${YELLOW}!${NC} Certificate files not found yet (add them before starting)" fi + + # Custom CA + echo "" + echo -e "${BOLD}Custom Certificate Authority${NC}" + echo "" + echo -e "${DIM}If your GitLab instance or your Plumber certificates are signed by${NC}" + echo -e "${DIM}a custom Certificate Authority (private CA), Plumber needs the root${NC}" + echo -e "${DIM}CA certificate to trust those connections.${NC}" + echo "" + + if prompt_confirm "Are you using a custom CA?" "N"; then + echo "" + echo " Add your root CA certificate file (.pem or .crt) to:" + echo "" + echo -e " ${BOLD}.docker/ca-certificates/${NC}" + echo "" + + mkdir -p .docker/ca-certificates + + CA_FILES=$(find .docker/ca-certificates -maxdepth 1 -type f \( -name "*.pem" -o -name "*.crt" \) 2>/dev/null | wc -l | tr -d ' ') + if [ "$CA_FILES" -gt 0 ]; then + echo -e " ${GREEN}✓${NC} Found ${CA_FILES} CA certificate(s) in .docker/ca-certificates/" + else + echo -e " ${YELLOW}!${NC} No CA certificates found yet (add them before starting)" + fi + fi fi # Database From 32e71002811f47a7a05af2ffdae882f9ca773f9a Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Mon, 16 Feb 2026 16:34:23 +0100 Subject: [PATCH 06/14] conf(compose): add local mode in the install script --- install.sh | 256 +++++++++++++++++++++++++++++-------------- scripts/preflight.sh | 114 +++++++++++-------- 2 files changed, 240 insertions(+), 130 deletions(-) diff --git a/install.sh b/install.sh index f806f94..f314dbf 100755 --- a/install.sh +++ b/install.sh @@ -61,7 +61,8 @@ prompt_secret() { while true; do VALUE="" echo -ne "${BOLD}${MESSAGE}${NC}: " - while IFS= read -rs -n1 CHAR; do + stty -echo 2>/dev/null || true + while IFS= read -r -n1 CHAR; do if [[ -z "$CHAR" ]]; then break elif [[ "$CHAR" == $'\x7f' ]] || [[ "$CHAR" == $'\b' ]]; then @@ -74,6 +75,7 @@ prompt_secret() { echo -ne "*" fi done + stty echo 2>/dev/null || true echo "" if [ -n "$VALUE" ]; then @@ -176,18 +178,32 @@ if [ ! -f compose.yml ] || [ ! -f versions.env ]; then fi # ============================================================================= -# Step 2: Run pre-config checks +# Step 2: Choose deployment type +# ============================================================================= + +prompt_choice DEPLOY_TYPE "Deployment type:" \ + "Production (domain, TLS, reverse proxy)" \ + "Local (localhost, no TLS)" +echo "" + +# ============================================================================= +# Step 3: Run pre-config checks # ============================================================================= echo "Running pre-flight checks..." -if ! bash scripts/preflight.sh --pre; then +if [ "$DEPLOY_TYPE" = "2" ]; then + PREFLIGHT_FLAGS="--pre --local" +else + PREFLIGHT_FLAGS="--pre" +fi +if ! bash scripts/preflight.sh $PREFLIGHT_FLAGS; then echo "" echo -e "${RED}Pre-flight checks failed. Please fix the issues above and try again.${NC}" exit 1 fi # ============================================================================= -# Step 3: Interactive configuration +# Step 4: Interactive configuration # ============================================================================= echo "" @@ -195,8 +211,31 @@ echo -e "${BOLD}Configuration${NC}" echo "───────────────────────────────────────" echo "" -# Domain name -prompt DOMAIN_NAME "Plumber domain name (e.g. plumber.example.com)" +# Domain name (production only) +if [ "$DEPLOY_TYPE" = "1" ]; then + prompt DOMAIN_NAME "Plumber domain name (e.g. plumber.example.com)" + + # Check DNS resolution + DNS_OK=false + if command -v dig &> /dev/null; then + if dig +short "$DOMAIN_NAME" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then + DNS_OK=true + fi + elif command -v nslookup &> /dev/null; then + if nslookup "$DOMAIN_NAME" &> /dev/null; then + DNS_OK=true + fi + fi + + if [ "$DNS_OK" = true ]; then + echo -e "${GREEN}✓${NC} DNS resolves for ${DOMAIN_NAME}" + else + echo -e "${RED}!${NC} DNS does not resolve for ${DOMAIN_NAME}" + echo -e "${DIM} Create a DNS A record pointing ${DOMAIN_NAME} to your server's public IP.${NC}" + echo -e "${DIM} You can continue the setup and configure DNS before starting Plumber.${NC}" + fi + echo "" +fi # GitLab URL prompt JOBS_GITLAB_URL "GitLab instance URL (e.g. https://gitlab.example.com)" @@ -232,9 +271,15 @@ echo " 1. Open this link to create a new application:" echo "" echo -e " ${BOLD}${GITLAB_APP_URL}${NC}" echo "" +if [ "$DEPLOY_TYPE" = "1" ]; then + REDIRECT_URI="https://${DOMAIN_NAME}/api/auth/gitlab/callback" +else + REDIRECT_URI="http://localhost:3001/api/auth/gitlab/callback" +fi + echo " 2. Fill in the following:" echo -e " - Name: ${BOLD}Plumber${NC}" -echo -e " - Redirect URI: ${BOLD}https://${DOMAIN_NAME}/api/auth/gitlab/callback${NC}" +echo -e " - Redirect URI: ${BOLD}${REDIRECT_URI}${NC}" echo -e " - Confidential: ${BOLD}yes${NC} (keep the box checked)" echo -e " - Scopes: ${BOLD}api${NC}" echo "" @@ -244,84 +289,86 @@ echo "" prompt GITLAB_OAUTH2_CLIENT_ID "Application ID" prompt_secret GITLAB_OAUTH2_CLIENT_SECRET "Secret" -# Certificate method -echo "" -echo "───────────────────────────────────────" -prompt_choice CERT_CHOICE "TLS certificate method:" \ - "Let's Encrypt (automatic, server must be reachable from internet)" \ - "Custom certificates (provide your own .pem files)" +# Certificate method & Database (production only) +EXT_DB_VARS="" -if [ "$CERT_CHOICE" = "1" ]; then - CERT_PROFILE="letsencrypt" - CERT_RESOLVER="le" -else - CERT_PROFILE="custom-certs" - CERT_RESOLVER="" +if [ "$DEPLOY_TYPE" = "1" ]; then echo "" - echo -e "${DIM}Place your certificate files at:${NC}" - echo " .docker/traefik/certs/plumber_fullchain.pem" - echo " .docker/traefik/certs/plumber_privkey.pem" - - if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then - echo -e " ${GREEN}✓${NC} Certificate files found" + echo "───────────────────────────────────────" + prompt_choice CERT_CHOICE "TLS certificate method:" \ + "Let's Encrypt (automatic, server must be reachable from internet)" \ + "Custom certificates (provide your own .pem files)" + + if [ "$CERT_CHOICE" = "1" ]; then + CERT_PROFILE="letsencrypt" + CERT_RESOLVER="le" else - echo -e " ${YELLOW}!${NC} Certificate files not found yet (add them before starting)" - fi + CERT_PROFILE="custom-certs" + CERT_RESOLVER="" + echo "" + echo -e "${DIM}Place your certificate files at:${NC}" + echo " .docker/traefik/certs/plumber_fullchain.pem" + echo " .docker/traefik/certs/plumber_privkey.pem" - # Custom CA - echo "" - echo -e "${BOLD}Custom Certificate Authority${NC}" - echo "" - echo -e "${DIM}If your GitLab instance or your Plumber certificates are signed by${NC}" - echo -e "${DIM}a custom Certificate Authority (private CA), Plumber needs the root${NC}" - echo -e "${DIM}CA certificate to trust those connections.${NC}" - echo "" + if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then + echo -e " ${GREEN}✓${NC} Certificate files found" + else + echo -e " ${YELLOW}!${NC} Certificate files not found yet (add them before starting)" + fi - if prompt_confirm "Are you using a custom CA?" "N"; then + # Custom CA echo "" - echo " Add your root CA certificate file (.pem or .crt) to:" + echo -e "${BOLD}Custom Certificate Authority${NC}" echo "" - echo -e " ${BOLD}.docker/ca-certificates/${NC}" + echo -e "${DIM}If your GitLab instance or your Plumber certificates are signed by${NC}" + echo -e "${DIM}a custom Certificate Authority (private CA), Plumber needs the root${NC}" + echo -e "${DIM}CA certificate to trust those connections.${NC}" echo "" - mkdir -p .docker/ca-certificates + if prompt_confirm "Are you using a custom CA?" "N"; then + echo "" + echo " Add your root CA certificate file (.pem or .crt) to:" + echo "" + echo -e " ${BOLD}.docker/ca-certificates/${NC}" + echo "" - CA_FILES=$(find .docker/ca-certificates -maxdepth 1 -type f \( -name "*.pem" -o -name "*.crt" \) 2>/dev/null | wc -l | tr -d ' ') - if [ "$CA_FILES" -gt 0 ]; then - echo -e " ${GREEN}✓${NC} Found ${CA_FILES} CA certificate(s) in .docker/ca-certificates/" - else - echo -e " ${YELLOW}!${NC} No CA certificates found yet (add them before starting)" + mkdir -p .docker/ca-certificates + + CA_FILES=$(find .docker/ca-certificates -maxdepth 1 -type f \( -name "*.pem" -o -name "*.crt" \) 2>/dev/null | wc -l | tr -d ' ') + if [ "$CA_FILES" -gt 0 ]; then + echo -e " ${GREEN}✓${NC} Found ${CA_FILES} CA certificate(s) in .docker/ca-certificates/" + else + echo -e " ${YELLOW}!${NC} No CA certificates found yet (add them before starting)" + fi fi fi -fi -# Database -echo "" -echo "───────────────────────────────────────" -prompt_choice DB_CHOICE "Database:" \ - "Internal (managed PostgreSQL container)" \ - "External (connect to your own PostgreSQL)" - -if [ "$DB_CHOICE" = "1" ]; then - DB_PROFILE=",internal-db" - EXT_DB_VARS="" -else - DB_PROFILE="" + # Database echo "" - prompt JOBS_DB_HOST "Database host" - prompt_optional JOBS_DB_PORT "Database port" "5432" - prompt JOBS_DB_USER "Database user" - prompt_optional JOBS_DB_NAME "Database name" "plumber" - prompt_secret JOBS_DB_PASSWORD_EXT "Database password" - echo "" - echo -e "${DIM}SSL mode options: disable, require, verify-ca${NC}" - prompt_optional JOBS_DB_SSLMODE "SSL mode" "disable" + echo "───────────────────────────────────────" + prompt_choice DB_CHOICE "Database:" \ + "Internal (managed PostgreSQL container)" \ + "External (connect to your own PostgreSQL)" - # Detect server timezone - SERVER_TZ=$(timedatectl show --property=Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo "UTC") - prompt_optional JOBS_DB_TIMEZONE "Timezone" "${SERVER_TZ}" + if [ "$DB_CHOICE" = "1" ]; then + DB_PROFILE=",internal-db" + else + DB_PROFILE="" + echo "" + prompt JOBS_DB_HOST "Database host" + prompt_optional JOBS_DB_PORT "Database port" "5432" + prompt JOBS_DB_USER "Database user" + prompt_optional JOBS_DB_NAME "Database name" "plumber" + prompt_secret JOBS_DB_PASSWORD_EXT "Database password" + echo "" + echo -e "${DIM}SSL mode options: disable, require, verify-ca${NC}" + prompt_optional JOBS_DB_SSLMODE "SSL mode" "disable" - EXT_DB_VARS=$(cat </dev/null || cat /etc/timezone 2>/dev/null || echo "UTC") + prompt_optional JOBS_DB_TIMEZONE "Timezone" "${SERVER_TZ}" + + EXT_DB_VARS=$(cat < .env < .env < .env < /dev/null; then if ss -tlnp 2>/dev/null | grep -q ":${PORT} "; then fail "Port ${PORT} is already in use" @@ -125,7 +133,11 @@ run_post_checks() { set +a # Check required variables are set and non-empty - REQUIRED_VARS="DOMAIN_NAME JOBS_GITLAB_URL GITLAB_OAUTH2_CLIENT_ID GITLAB_OAUTH2_CLIENT_SECRET SECRET_KEY JOBS_DB_PASSWORD JOBS_REDIS_PASSWORD COMPOSE_PROFILES" + if [ "$LOCAL_MODE" = true ]; then + REQUIRED_VARS="JOBS_GITLAB_URL GITLAB_OAUTH2_CLIENT_ID GITLAB_OAUTH2_CLIENT_SECRET SECRET_KEY JOBS_DB_PASSWORD JOBS_REDIS_PASSWORD" + else + REQUIRED_VARS="DOMAIN_NAME JOBS_GITLAB_URL GITLAB_OAUTH2_CLIENT_ID GITLAB_OAUTH2_CLIENT_SECRET SECRET_KEY JOBS_DB_PASSWORD JOBS_REDIS_PASSWORD COMPOSE_PROFILES" + fi for VAR in $REQUIRED_VARS; do VALUE="${!VAR:-}" if [ -z "$VALUE" ]; then @@ -147,40 +159,54 @@ run_post_checks() { fi done - # Check COMPOSE_PROFILES is valid - PROFILES="${COMPOSE_PROFILES:-}" - if [ -n "$PROFILES" ]; then - HAS_TRAEFIK=false - if echo "$PROFILES" | grep -q "letsencrypt"; then - HAS_TRAEFIK=true - fi - if echo "$PROFILES" | grep -q "custom-certs"; then - HAS_TRAEFIK=true - fi - if [ "$HAS_TRAEFIK" = false ]; then - fail "COMPOSE_PROFILES must include 'letsencrypt' or 'custom-certs'" - else - pass "COMPOSE_PROFILES has a valid traefik profile" + # Production-only checks + if [ "$LOCAL_MODE" = false ]; then + # Check COMPOSE_PROFILES is valid + PROFILES="${COMPOSE_PROFILES:-}" + if [ -n "$PROFILES" ]; then + HAS_TRAEFIK=false + if echo "$PROFILES" | grep -q "letsencrypt"; then + HAS_TRAEFIK=true + fi + if echo "$PROFILES" | grep -q "custom-certs"; then + HAS_TRAEFIK=true + fi + if [ "$HAS_TRAEFIK" = false ]; then + fail "COMPOSE_PROFILES must include 'letsencrypt' or 'custom-certs'" + else + pass "COMPOSE_PROFILES has a valid traefik profile" + fi fi - fi - # Check DNS resolution for DOMAIN_NAME - DOMAIN="${DOMAIN_NAME:-}" - if [ -n "$DOMAIN" ]; then - if command -v dig &> /dev/null; then - if dig +short "$DOMAIN" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then - pass "DNS resolves for ${DOMAIN}" + # Check DNS resolution for DOMAIN_NAME + DOMAIN="${DOMAIN_NAME:-}" + if [ -n "$DOMAIN" ]; then + if command -v dig &> /dev/null; then + if dig +short "$DOMAIN" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$'; then + pass "DNS resolves for ${DOMAIN}" + else + fail "DNS does not resolve for ${DOMAIN} (ensure your DNS record is configured)" + fi + elif command -v nslookup &> /dev/null; then + if nslookup "$DOMAIN" &> /dev/null; then + pass "DNS resolves for ${DOMAIN}" + else + fail "DNS does not resolve for ${DOMAIN} (ensure your DNS record is configured)" + fi else - warn "DNS does not resolve for ${DOMAIN} (ensure your DNS record is configured)" + warn "Cannot check DNS (install dig or nslookup)" fi - elif command -v nslookup &> /dev/null; then - if nslookup "$DOMAIN" &> /dev/null; then - pass "DNS resolves for ${DOMAIN}" + fi + + # Check custom cert files exist if custom-certs profile is active + if echo "${COMPOSE_PROFILES:-}" | grep -q "custom-certs"; then + if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then + pass "Custom certificate files found" else - warn "DNS does not resolve for ${DOMAIN} (ensure your DNS record is configured)" + fail "Custom certificates profile is active but cert files are missing" + echo -e " Expected: .docker/traefik/certs/plumber_fullchain.pem" + echo -e " Expected: .docker/traefik/certs/plumber_privkey.pem" fi - else - warn "Cannot check DNS (install dig or nslookup)" fi fi @@ -193,31 +219,27 @@ run_post_checks() { warn "Cannot reach GitLab at ${GITLAB_URL} (check URL and network)" fi fi - - # Check custom cert files exist if custom-certs profile is active - if echo "${COMPOSE_PROFILES:-}" | grep -q "custom-certs"; then - if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then - pass "Custom certificate files found" - else - fail "Custom certificates profile is active but cert files are missing" - echo -e " Expected: .docker/traefik/certs/plumber_fullchain.pem" - echo -e " Expected: .docker/traefik/certs/plumber_privkey.pem" - fi - fi } # ============================================================================= # Main # ============================================================================= -# Determine which checks to run based on arguments -MODE="${1:-all}" +# Parse arguments +MODE="all" +for ARG in "$@"; do + case "$ARG" in + --pre) MODE="pre" ;; + --post) MODE="post" ;; + --local) LOCAL_MODE=true ;; + esac +done case "$MODE" in - --pre) + pre) run_pre_checks ;; - --post) + post) run_post_checks ;; *) From 84aae8cf84e887a91d20d59d4c08f526cc231e47 Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Mon, 16 Feb 2026 17:35:08 +0100 Subject: [PATCH 07/14] conf(compose): add custom certs & CA in pre-flight checks --- scripts/preflight.sh | 49 +++++++++++++++++++++++++++++++++++++++----- 1 file changed, 44 insertions(+), 5 deletions(-) diff --git a/scripts/preflight.sh b/scripts/preflight.sh index 192e16f..8a8dffb 100755 --- a/scripts/preflight.sh +++ b/scripts/preflight.sh @@ -198,14 +198,53 @@ run_post_checks() { fi fi - # Check custom cert files exist if custom-certs profile is active + # Check custom cert files if custom-certs profile is active if echo "${COMPOSE_PROFILES:-}" | grep -q "custom-certs"; then - if [ -f .docker/traefik/certs/plumber_fullchain.pem ] && [ -f .docker/traefik/certs/plumber_privkey.pem ]; then + CERT_DIR=".docker/traefik/certs" + FULLCHAIN="${CERT_DIR}/plumber_fullchain.pem" + PRIVKEY="${CERT_DIR}/plumber_privkey.pem" + + if [ -f "$FULLCHAIN" ] && [ -f "$PRIVKEY" ]; then pass "Custom certificate files found" + + # Validate certificate is a valid PEM + if openssl x509 -in "$FULLCHAIN" -noout 2>/dev/null; then + pass "Certificate ${FULLCHAIN} is a valid PEM" + else + fail "Certificate ${FULLCHAIN} is not a valid PEM file" + fi + + # Validate private key is a valid PEM + if openssl rsa -in "$PRIVKEY" -check -noout >/dev/null 2>&1 || openssl ec -in "$PRIVKEY" -check -noout >/dev/null 2>&1; then + pass "Private key ${PRIVKEY} is valid" + else + fail "Private key ${PRIVKEY} is not a valid PEM key file" + fi else fail "Custom certificates profile is active but cert files are missing" - echo -e " Expected: .docker/traefik/certs/plumber_fullchain.pem" - echo -e " Expected: .docker/traefik/certs/plumber_privkey.pem" + echo -e " Expected: ${FULLCHAIN}" + echo -e " Expected: ${PRIVKEY}" + fi + + # Check custom CA certificates + CA_DIR=".docker/ca-certificates" + if [ -d "$CA_DIR" ]; then + CA_COUNT=$(find "$CA_DIR" -maxdepth 1 -type f \( -name "*.pem" -o -name "*.crt" \) 2>/dev/null | wc -l | tr -d ' ') + if [ "$CA_COUNT" -gt 0 ]; then + pass "Found ${CA_COUNT} custom CA certificate(s) in ${CA_DIR}/" + for CA_FILE in "$CA_DIR"/*.pem "$CA_DIR"/*.crt; do + [ -f "$CA_FILE" ] || continue + if openssl x509 -in "$CA_FILE" -noout 2>/dev/null; then + pass "CA certificate $(basename "$CA_FILE") is a valid PEM" + else + fail "CA certificate $(basename "$CA_FILE") is not a valid PEM file" + fi + done + else + fail "No CA certificates found in ${CA_DIR}/ (add .pem or .crt files if using a custom CA)" + fi + else + fail "Directory ${CA_DIR}/ does not exist (create it and add CA certs if using a custom CA)" fi fi fi @@ -216,7 +255,7 @@ run_post_checks() { if curl -sf --max-time 10 "${GITLAB_URL}" -o /dev/null 2>/dev/null; then pass "GitLab instance is reachable at ${GITLAB_URL}" else - warn "Cannot reach GitLab at ${GITLAB_URL} (check URL and network)" + fail "Cannot reach GitLab at ${GITLAB_URL} (check URL and network)" fi fi } From 521d1b5179ae5cbdb615854a87ae9120d71c5d33 Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Mon, 16 Feb 2026 17:53:21 +0100 Subject: [PATCH 08/14] fix(compose): fix the update script --- install.sh | 4 ++-- scripts/update.sh | 38 ++++++++++++++++++++++++++------------ 2 files changed, 28 insertions(+), 14 deletions(-) diff --git a/install.sh b/install.sh index f314dbf..541d669 100755 --- a/install.sh +++ b/install.sh @@ -440,7 +440,7 @@ SECRET_KEY="${SECRET_KEY}" JOBS_DB_PASSWORD="${JOBS_DB_PASSWORD}" JOBS_REDIS_PASSWORD="${JOBS_REDIS_PASSWORD}" -# Image versions (managed by scripts/update.sh) +# Image versions (managed by versions.env) FRONTEND_IMAGE_TAG="${FRONTEND_IMAGE_TAG}" BACKEND_IMAGE_TAG="${BACKEND_IMAGE_TAG}" ENV @@ -470,7 +470,7 @@ JOBS_REDIS_PASSWORD="${JOBS_REDIS_PASSWORD}" COMPOSE_PROFILES="${COMPOSE_PROFILES}" CERT_RESOLVER="${CERT_RESOLVER}" -# Image versions (managed by scripts/update.sh) +# Image versions (managed by versions.env) FRONTEND_IMAGE_TAG="${FRONTEND_IMAGE_TAG}" BACKEND_IMAGE_TAG="${BACKEND_IMAGE_TAG}" ${EXT_DB_VARS} diff --git a/scripts/update.sh b/scripts/update.sh index 3457882..4dee2d2 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -7,10 +7,11 @@ # ./scripts/update.sh # # This script: -# 1. Pulls the latest changes from the git repository -# 2. Syncs image tags from versions.env into .env -# 3. Migrates old .env format if needed (auto-detects COMPOSE_PROFILES) -# 4. Restarts containers with the new images +# 1. Stops running containers (using current compose config) +# 2. Pulls the latest changes from the git repository +# 3. Loads image tags from versions.env and removes them from .env +# 4. Migrates old .env format if needed (auto-detects COMPOSE_PROFILES) +# 5. Starts containers with the new images set -euo pipefail @@ -53,7 +54,15 @@ echo -e "${BOLD}Updating Plumber...${NC}" echo "───────────────────────────────────────" # ============================================================================= -# Step 1: Pull latest changes +# Step 1: Stop containers (before pulling, so old compose config is used) +# ============================================================================= +echo "" +echo "Stopping containers..." +docker compose down --remove-orphans +echo -e "${GREEN}✓${NC} Containers stopped" + +# ============================================================================= +# Step 2: Pull latest changes # ============================================================================= echo "" echo "Pulling latest changes..." @@ -61,25 +70,30 @@ git pull echo -e "${GREEN}✓${NC} Repository updated" # ============================================================================= -# Step 2: Sync image tags from versions.env +# Step 3: Load image tags from versions.env # ============================================================================= echo "" -echo "Syncing image versions..." +echo "Loading image versions..." +set -a source versions.env +set +a if [ -z "${FRONTEND_IMAGE_TAG:-}" ] || [ -z "${BACKEND_IMAGE_TAG:-}" ]; then echo -e "${RED}Error:${NC} versions.env is missing image tags." exit 1 fi -set_env_var "FRONTEND_IMAGE_TAG" "${FRONTEND_IMAGE_TAG}" -set_env_var "BACKEND_IMAGE_TAG" "${BACKEND_IMAGE_TAG}" +# Remove image tags from .env if present (versions.env is the source of truth) +sed -i."" '/^FRONTEND_IMAGE_TAG=/d' .env 2>/dev/null || true +sed -i."" '/^BACKEND_IMAGE_TAG=/d' .env 2>/dev/null || true +sed -i."" '/^# Image versions/d' .env 2>/dev/null || true +rm -f .env."" 2>/dev/null || true echo -e "${GREEN}✓${NC} Frontend: ${FRONTEND_IMAGE_TAG}" echo -e "${GREEN}✓${NC} Backend: ${BACKEND_IMAGE_TAG}" # ============================================================================= -# Step 3: Migrate COMPOSE_PROFILES if missing (old .env format) +# Step 4: Migrate COMPOSE_PROFILES if missing (old .env format) # ============================================================================= if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then echo "" @@ -184,10 +198,10 @@ if ! grep -q "^CERT_RESOLVER=" .env 2>/dev/null; then fi # ============================================================================= -# Step 4: Restart containers +# Step 5: Start containers with new images # ============================================================================= echo "" -echo "Restarting containers..." +echo "Starting containers..." docker compose up -d echo "" From c4ab1b9d21f51728a35dac27095fa819f114c751 Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Mon, 16 Feb 2026 19:53:05 +0100 Subject: [PATCH 09/14] docs(README): add one-line install command --- README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index f9392c6..60d414c 100644 --- a/README.md +++ b/README.md @@ -7,10 +7,13 @@ This project contains resources to setup a self-managed instance of [Plumber](ht ## Installation -Two installation methods: +- 🐳 **Docker Compose** — [Documentation](https://getplumber.io/docs/installation/docker-compose/) -- 🐳 [Docker compose](https://getplumber.io/docs/installation/docker-compose/) -- ☸️ [Kubernetes with Helm](https://getplumber.io/docs/installation/kubernetes/) + ```bash + curl -fsSL https://raw.githubusercontent.com/getplumber/platform/main/install.sh | bash + ``` + +- ☸️ **Kubernetes with Helm** — [Documentation](https://getplumber.io/docs/installation/kubernetes/) ## Contributions From 34d577569fad7d18122ff3a60c8e63220a7e3035 Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Tue, 17 Feb 2026 13:56:53 +0100 Subject: [PATCH 10/14] fix(compose): fix the local installation with script --- .env.local.example | 21 ++++++++------------- install.sh | 6 ++++-- scripts/update.sh | 35 ++++++++++++++++++++++------------- 3 files changed, 34 insertions(+), 28 deletions(-) diff --git a/.env.local.example b/.env.local.example index b4093b5..ff520e8 100644 --- a/.env.local.example +++ b/.env.local.example @@ -3,18 +3,13 @@ # Documentation: https://getplumber.io/docs/installation/local-docker-compose # ############################################################################### -# Main -JOBS_GITLAB_URL="GITLAB_INSTANCE_URL" -<<<<<<< HEAD -FRONTEND_IMAGE_TAG="v2.32.5" -BACKEND_IMAGE_TAG="v2.36.4" -======= ->>>>>>> 03b7b4b (conf(all): simplify the installation with interactive script) +# Required configuration +JOBS_GITLAB_URL="" ORGANIZATION="" +GITLAB_OAUTH2_CLIENT_ID="" +GITLAB_OAUTH2_CLIENT_SECRET="" -# Secrets -GITLAB_OAUTH2_CLIENT_ID="REPLACE_ME_BY_CLIENT_ID" -GITLAB_OAUTH2_CLIENT_SECRET="REPLACE_ME_BY_CLIENT_SECRET" -SECRET_KEY="REPLACE_ME_BY_SECRET_KEY" -JOBS_DB_PASSWORD="REPLACE_ME_BY_JOBS_DB_PASSWORD" -JOBS_REDIS_PASSWORD="REPLACE_ME_BY_JOBS_REDIS_PASSWORD" +# Auto-generated secrets (populated by install script) +SECRET_KEY="" +JOBS_DB_PASSWORD="" +JOBS_REDIS_PASSWORD="" diff --git a/install.sh b/install.sh index 541d669..e7b1c4a 100755 --- a/install.sh +++ b/install.sh @@ -244,8 +244,10 @@ JOBS_GITLAB_URL="${JOBS_GITLAB_URL%/}" # Organization echo "" -echo -e "${DIM}Leave empty to connect Plumber to the entire GitLab instance.${NC}" -echo -e "${DIM}Or enter a group path to limit Plumber to that group.${NC}" +echo -e "${DIM}Leave empty to connect Plumber to the entire GitLab instance${NC}" +echo -e "${DIM}(requires GitLab instance Admin to run onboarding).${NC}" +echo -e "${DIM}Or enter a group path to limit Plumber to that group${NC}" +echo -e "${DIM}(requires at least Maintainer role in the group).${NC}" prompt_optional ORGANIZATION "GitLab group path" # GitLab OIDC diff --git a/scripts/update.sh b/scripts/update.sh index 4dee2d2..91e7b6c 100755 --- a/scripts/update.sh +++ b/scripts/update.sh @@ -9,7 +9,7 @@ # This script: # 1. Stops running containers (using current compose config) # 2. Pulls the latest changes from the git repository -# 3. Loads image tags from versions.env and removes them from .env +# 3. Loads image tags from versions.env and syncs them into .env # 4. Migrates old .env format if needed (auto-detects COMPOSE_PROFILES) # 5. Starts containers with the new images @@ -49,8 +49,19 @@ set_env_var() { fi } +# ============================================================================= +# Detect deployment type (local vs production) +# ============================================================================= +if grep -q "^DOMAIN_NAME=" .env 2>/dev/null; then + DEPLOY_TYPE="production" + COMPOSE_CMD="docker compose" +else + DEPLOY_TYPE="local" + COMPOSE_CMD="docker compose -f compose.local.yml" +fi + echo "" -echo -e "${BOLD}Updating Plumber...${NC}" +echo -e "${BOLD}Updating Plumber (${DEPLOY_TYPE})...${NC}" echo "───────────────────────────────────────" # ============================================================================= @@ -58,7 +69,7 @@ echo "──────────────────────── # ============================================================================= echo "" echo "Stopping containers..." -docker compose down --remove-orphans +$COMPOSE_CMD down --remove-orphans echo -e "${GREEN}✓${NC} Containers stopped" # ============================================================================= @@ -83,19 +94,17 @@ if [ -z "${FRONTEND_IMAGE_TAG:-}" ] || [ -z "${BACKEND_IMAGE_TAG:-}" ]; then exit 1 fi -# Remove image tags from .env if present (versions.env is the source of truth) -sed -i."" '/^FRONTEND_IMAGE_TAG=/d' .env 2>/dev/null || true -sed -i."" '/^BACKEND_IMAGE_TAG=/d' .env 2>/dev/null || true -sed -i."" '/^# Image versions/d' .env 2>/dev/null || true -rm -f .env."" 2>/dev/null || true +# Sync image tags into .env (versions.env is the source of truth) +set_env_var "FRONTEND_IMAGE_TAG" "${FRONTEND_IMAGE_TAG}" +set_env_var "BACKEND_IMAGE_TAG" "${BACKEND_IMAGE_TAG}" echo -e "${GREEN}✓${NC} Frontend: ${FRONTEND_IMAGE_TAG}" echo -e "${GREEN}✓${NC} Backend: ${BACKEND_IMAGE_TAG}" # ============================================================================= -# Step 4: Migrate COMPOSE_PROFILES if missing (old .env format) +# Step 4: Migrate COMPOSE_PROFILES if missing (old .env format, production only) # ============================================================================= -if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then +if [ "$DEPLOY_TYPE" = "production" ] && ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then echo "" echo -e "${YELLOW}Migration required:${NC} COMPOSE_PROFILES is not set in your .env file." echo "" @@ -186,8 +195,8 @@ if ! grep -q "^COMPOSE_PROFILES=" .env 2>/dev/null; then done fi -# Ensure CERT_RESOLVER is set (derive from COMPOSE_PROFILES if missing) -if ! grep -q "^CERT_RESOLVER=" .env 2>/dev/null; then +# Ensure CERT_RESOLVER is set (derive from COMPOSE_PROFILES if missing, production only) +if [ "$DEPLOY_TYPE" = "production" ] && ! grep -q "^CERT_RESOLVER=" .env 2>/dev/null; then source .env if echo "${COMPOSE_PROFILES:-}" | grep -q "letsencrypt"; then set_env_var "CERT_RESOLVER" "le" @@ -202,7 +211,7 @@ fi # ============================================================================= echo "" echo "Starting containers..." -docker compose up -d +$COMPOSE_CMD up -d echo "" echo -e "${GREEN}✓ Plumber has been updated successfully!${NC}" From d05d64168d2e5c31e7fd5fab2c1b9795bc466793 Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Mon, 16 Feb 2026 19:53:53 +0100 Subject: [PATCH 11/14] conf(compose): upgrade back & front and bump chart --- charts/plumber/Chart.yaml | 4 ++-- charts/plumber/values.yaml | 4 ++-- versions.env | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/charts/plumber/Chart.yaml b/charts/plumber/Chart.yaml index eee345e..6bb0914 100644 --- a/charts/plumber/Chart.yaml +++ b/charts/plumber/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: plumber description: Helm chart for Plumber type: application -version: "1.0.2" -appVersion: "1.0.2" +version: "1.0.3" +appVersion: "1.0.3" home: https://github.com/getplumber/platform/ maintainers: - name: devpro diff --git a/charts/plumber/values.yaml b/charts/plumber/values.yaml index 7e97658..81a9230 100644 --- a/charts/plumber/values.yaml +++ b/charts/plumber/values.yaml @@ -68,7 +68,7 @@ backend: type: backend name: plumber-backend image: docker.io/getplumber/backend - tag: v2.36.4 + tag: v2.36.5 replicaCount: 1 revisionHistoryLimit: 5 port: 3000 @@ -150,7 +150,7 @@ worker: type: backend name: plumber-worker image: docker.io/getplumber/backend - tag: v2.36.4 + tag: v2.36.5 replicaCount: 5 revisionHistoryLimit: 5 args: diff --git a/versions.env b/versions.env index 72a0a6c..4fc4c12 100644 --- a/versions.env +++ b/versions.env @@ -1,2 +1,2 @@ -FRONTEND_IMAGE_TAG=v2.32.4 -BACKEND_IMAGE_TAG=v2.36.4 +FRONTEND_IMAGE_TAG=v2.32.5 +BACKEND_IMAGE_TAG=v2.36.5 From 1c368d2922830c14468a894ba4dd42bb1f25162e Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Tue, 17 Feb 2026 14:58:31 +0100 Subject: [PATCH 12/14] fix(install): fix stind input issue with curl pipe --- install.sh | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/install.sh b/install.sh index e7b1c4a..ebcc184 100755 --- a/install.sh +++ b/install.sh @@ -41,7 +41,7 @@ prompt() { else echo -ne "${BOLD}${MESSAGE}${NC}: " fi - read -r VALUE + read -r VALUE < /dev/tty VALUE="${VALUE:-$DEFAULT}" if [ -n "$VALUE" ]; then @@ -61,8 +61,8 @@ prompt_secret() { while true; do VALUE="" echo -ne "${BOLD}${MESSAGE}${NC}: " - stty -echo 2>/dev/null || true - while IFS= read -r -n1 CHAR; do + stty -echo < /dev/tty 2>/dev/null || true + while IFS= read -r -n1 CHAR < /dev/tty; do if [[ -z "$CHAR" ]]; then break elif [[ "$CHAR" == $'\x7f' ]] || [[ "$CHAR" == $'\b' ]]; then @@ -75,7 +75,7 @@ prompt_secret() { echo -ne "*" fi done - stty echo 2>/dev/null || true + stty echo < /dev/tty 2>/dev/null || true echo "" if [ -n "$VALUE" ]; then @@ -96,7 +96,7 @@ prompt_optional() { else echo -ne "${BOLD}${MESSAGE}${NC} ${DIM}(leave empty to skip)${NC}: " fi - read -r VALUE + read -r VALUE < /dev/tty VALUE="${VALUE:-$DEFAULT}" eval "$VARNAME=\"$VALUE\"" } @@ -115,7 +115,7 @@ prompt_choice() { while true; do echo -ne "${BOLD}Choice${NC} ${DIM}(1-${NUM_OPTIONS})${NC}: " - read -r CHOICE + read -r CHOICE < /dev/tty if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "$NUM_OPTIONS" ]; then eval "$VARNAME=\"$CHOICE\"" return @@ -133,7 +133,7 @@ prompt_confirm() { else echo -ne "${BOLD}${MESSAGE}${NC} ${DIM}(y/N)${NC}: " fi - read -r REPLY + read -r REPLY < /dev/tty REPLY="${REPLY:-$DEFAULT}" case "$REPLY" in From 00c56906ff10cc5a7b42940d213c125ef087b485 Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Tue, 17 Feb 2026 15:08:04 +0100 Subject: [PATCH 13/14] fix(install): fix special character issue in install script --- install.sh | 125 ++++++++++++++++++++++++++++------------------------- 1 file changed, 65 insertions(+), 60 deletions(-) diff --git a/install.sh b/install.sh index ebcc184..e750572 100755 --- a/install.sh +++ b/install.sh @@ -45,7 +45,7 @@ prompt() { VALUE="${VALUE:-$DEFAULT}" if [ -n "$VALUE" ]; then - eval "$VARNAME=\"$VALUE\"" + printf -v "$VARNAME" '%s' "$VALUE" return fi echo -e "${RED} This field is required.${NC}" @@ -79,7 +79,7 @@ prompt_secret() { echo "" if [ -n "$VALUE" ]; then - eval "$VARNAME=\"$VALUE\"" + printf -v "$VARNAME" '%s' "$VALUE" return fi echo -e "${RED} This field is required.${NC}" @@ -98,7 +98,11 @@ prompt_optional() { fi read -r VALUE < /dev/tty VALUE="${VALUE:-$DEFAULT}" - eval "$VARNAME=\"$VALUE\"" + printf -v "$VARNAME" '%s' "$VALUE" +} + +env_line() { + printf "%s='%s'\n" "$1" "$2" } prompt_choice() { @@ -117,7 +121,7 @@ prompt_choice() { echo -ne "${BOLD}Choice${NC} ${DIM}(1-${NUM_OPTIONS})${NC}: " read -r CHOICE < /dev/tty if [[ "$CHOICE" =~ ^[0-9]+$ ]] && [ "$CHOICE" -ge 1 ] && [ "$CHOICE" -le "$NUM_OPTIONS" ]; then - eval "$VARNAME=\"$CHOICE\"" + printf -v "$VARNAME" '%s' "$CHOICE" return fi echo -e "${RED} Please enter a number between 1 and ${NUM_OPTIONS}.${NC}" @@ -292,7 +296,6 @@ prompt GITLAB_OAUTH2_CLIENT_ID "Application ID" prompt_secret GITLAB_OAUTH2_CLIENT_SECRET "Secret" # Certificate method & Database (production only) -EXT_DB_VARS="" if [ "$DEPLOY_TYPE" = "1" ]; then echo "" @@ -370,17 +373,6 @@ if [ "$DEPLOY_TYPE" = "1" ]; then SERVER_TZ=$(timedatectl show --property=Timezone --value 2>/dev/null || cat /etc/timezone 2>/dev/null || echo "UTC") prompt_optional JOBS_DB_TIMEZONE "Timezone" "${SERVER_TZ}" - EXT_DB_VARS=$(cat < .env < .env else # Production .env - cat > .env < .env fi echo -e "${GREEN}✓${NC} Configuration written to .env" From ee792c7e5ddf9ac60d3159e8b899c78c964b2c5b Mon Sep 17 00:00:00 2001 From: Thomas Boni Date: Tue, 17 Feb 2026 15:30:16 +0100 Subject: [PATCH 14/14] fix(install): do not always raise error in case of missing custom CA --- scripts/preflight.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/preflight.sh b/scripts/preflight.sh index 8a8dffb..afed831 100755 --- a/scripts/preflight.sh +++ b/scripts/preflight.sh @@ -226,7 +226,7 @@ run_post_checks() { echo -e " Expected: ${PRIVKEY}" fi - # Check custom CA certificates + # Check custom CA certificates (optional, only needed for private CAs) CA_DIR=".docker/ca-certificates" if [ -d "$CA_DIR" ]; then CA_COUNT=$(find "$CA_DIR" -maxdepth 1 -type f \( -name "*.pem" -o -name "*.crt" \) 2>/dev/null | wc -l | tr -d ' ') @@ -241,10 +241,10 @@ run_post_checks() { fi done else - fail "No CA certificates found in ${CA_DIR}/ (add .pem or .crt files if using a custom CA)" + warn "No CA certificates in ${CA_DIR}/ (add .pem or .crt files only if using a private CA)" fi else - fail "Directory ${CA_DIR}/ does not exist (create it and add CA certs if using a custom CA)" + pass "No custom CA directory (not needed for publicly-signed certificates)" fi fi fi