From 411f7d4604bddee62f6f16098efd079c08203908 Mon Sep 17 00:00:00 2001 From: Matt Date: Sat, 17 Jan 2026 13:17:09 +0100 Subject: [PATCH] feat: Add production deployment stack - docker-compose.yml with Traefik integration - docker-compose.simple.yml for direct port exposure - .env.example with all configuration options - setup.sh script for initial deployment Usage: cd deploy/ cp .env.example .env # Edit .env with your values ./setup.sh docker compose up -d Co-Authored-By: Claude Opus 4.5 --- deploy/.env.example | 44 +++++++++++++++++++ deploy/docker-compose.simple.yml | 54 +++++++++++++++++++++++ deploy/docker-compose.yml | 74 ++++++++++++++++++++++++++++++++ deploy/setup.sh | 69 +++++++++++++++++++++++++++++ 4 files changed, 241 insertions(+) create mode 100644 deploy/.env.example create mode 100644 deploy/docker-compose.simple.yml create mode 100644 deploy/docker-compose.yml create mode 100644 deploy/setup.sh diff --git a/deploy/.env.example b/deploy/.env.example new file mode 100644 index 0000000..d4e4338 --- /dev/null +++ b/deploy/.env.example @@ -0,0 +1,44 @@ +# LetsBe Hub Production Configuration +# Copy this file to .env and fill in the values + +# ============================================================================= +# REQUIRED - Must be set before deployment +# ============================================================================= + +# Hub public URL (used for auth callbacks and runner communication) +HUB_URL=https://hub.yourdomain.com +HUB_DOMAIN=hub.yourdomain.com + +# Database password (generate a strong random password) +POSTGRES_PASSWORD=CHANGE_ME_STRONG_PASSWORD_HERE + +# NextAuth secret (generate with: openssl rand -base64 32) +NEXTAUTH_SECRET=CHANGE_ME_GENERATE_WITH_OPENSSL_RAND_BASE64_32 + +# Credential encryption key (generate with: openssl rand -hex 32) +CREDENTIAL_ENCRYPTION_KEY=CHANGE_ME_GENERATE_WITH_OPENSSL_RAND_HEX_32 + +# Settings encryption key (generate with: openssl rand -hex 32) +SETTINGS_ENCRYPTION_KEY=CHANGE_ME_GENERATE_WITH_OPENSSL_RAND_HEX_32 + +# ============================================================================= +# OPTIONAL - Defaults are usually fine +# ============================================================================= + +# Database settings +POSTGRES_USER=letsbe_hub +POSTGRES_DB=letsbe_hub + +# Hub image tag (default: master) +HUB_IMAGE_TAG=master + +# Ansible Runner settings +DOCKER_REGISTRY_URL=code.letsbe.solutions +DOCKER_IMAGE_NAME=letsbe/ansible-runner +DOCKER_IMAGE_TAG=master +DOCKER_MAX_CONCURRENT=3 + +# Host paths for job configs (runner containers need access) +# These directories will be created automatically by Docker +JOBS_HOST_DIR=/opt/letsbe-hub/jobs +LOGS_HOST_DIR=/opt/letsbe-hub/logs diff --git a/deploy/docker-compose.simple.yml b/deploy/docker-compose.simple.yml new file mode 100644 index 0000000..3e3ac2d --- /dev/null +++ b/deploy/docker-compose.simple.yml @@ -0,0 +1,54 @@ +# Simpler version without Traefik - exposes port 3000 directly +# Use this if you have your own reverse proxy or want direct access + +services: + db: + image: postgres:16-alpine + container_name: letsbe-hub-db + env_file: .env + environment: + POSTGRES_USER: ${POSTGRES_USER:-letsbe_hub} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-letsbe_hub} + volumes: + - hub-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-letsbe_hub} -d ${POSTGRES_DB:-letsbe_hub}"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + + hub: + image: code.letsbe.solutions/letsbe/hub:${HUB_IMAGE_TAG:-master} + container_name: letsbe-hub-app + env_file: .env + ports: + - "3000:3000" + environment: + DATABASE_URL: postgresql://${POSTGRES_USER:-letsbe_hub}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-letsbe_hub} + NEXTAUTH_URL: ${HUB_URL} + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} + AUTH_TRUST_HOST: "true" + HUB_URL: ${HUB_URL} + CREDENTIAL_ENCRYPTION_KEY: ${CREDENTIAL_ENCRYPTION_KEY} + SETTINGS_ENCRYPTION_KEY: ${SETTINGS_ENCRYPTION_KEY} + DOCKER_REGISTRY_URL: ${DOCKER_REGISTRY_URL:-code.letsbe.solutions} + DOCKER_IMAGE_NAME: ${DOCKER_IMAGE_NAME:-letsbe/ansible-runner} + DOCKER_IMAGE_TAG: ${DOCKER_IMAGE_TAG:-master} + DOCKER_MAX_CONCURRENT: ${DOCKER_MAX_CONCURRENT:-3} + JOBS_HOST_DIR: ${JOBS_HOST_DIR:-/opt/letsbe-hub/jobs} + LOGS_HOST_DIR: ${LOGS_HOST_DIR:-/opt/letsbe-hub/logs} + volumes: + - /var/run/docker.sock:/var/run/docker.sock + - ${JOBS_HOST_DIR:-/opt/letsbe-hub/jobs}:/app/jobs + - ${LOGS_HOST_DIR:-/opt/letsbe-hub/logs}:/app/logs + user: "0:0" + depends_on: + db: + condition: service_healthy + restart: unless-stopped + +volumes: + hub-db-data: + name: letsbe-hub-db diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml new file mode 100644 index 0000000..27ee963 --- /dev/null +++ b/deploy/docker-compose.yml @@ -0,0 +1,74 @@ +services: + db: + image: postgres:16-alpine + container_name: letsbe-hub-db + env_file: .env + environment: + POSTGRES_USER: ${POSTGRES_USER:-letsbe_hub} + POSTGRES_PASSWORD: ${POSTGRES_PASSWORD} + POSTGRES_DB: ${POSTGRES_DB:-letsbe_hub} + volumes: + - hub-db-data:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-letsbe_hub} -d ${POSTGRES_DB:-letsbe_hub}"] + interval: 5s + timeout: 5s + retries: 5 + restart: unless-stopped + networks: + - hub-internal + + hub: + image: code.letsbe.solutions/letsbe/hub:${HUB_IMAGE_TAG:-master} + container_name: letsbe-hub-app + env_file: .env + environment: + # Database + DATABASE_URL: postgresql://${POSTGRES_USER:-letsbe_hub}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-letsbe_hub} + # Auth + NEXTAUTH_URL: ${HUB_URL} + NEXTAUTH_SECRET: ${NEXTAUTH_SECRET} + AUTH_TRUST_HOST: "true" + # Hub URL (for runner callbacks) + HUB_URL: ${HUB_URL} + # Encryption keys + CREDENTIAL_ENCRYPTION_KEY: ${CREDENTIAL_ENCRYPTION_KEY} + SETTINGS_ENCRYPTION_KEY: ${SETTINGS_ENCRYPTION_KEY} + # Docker spawner config (for ansible runner) + DOCKER_REGISTRY_URL: ${DOCKER_REGISTRY_URL:-code.letsbe.solutions} + DOCKER_IMAGE_NAME: ${DOCKER_IMAGE_NAME:-letsbe/ansible-runner} + DOCKER_IMAGE_TAG: ${DOCKER_IMAGE_TAG:-master} + DOCKER_MAX_CONCURRENT: ${DOCKER_MAX_CONCURRENT:-3} + # Host paths for job configs (runner containers need access) + JOBS_HOST_DIR: ${JOBS_HOST_DIR:-/opt/letsbe-hub/jobs} + LOGS_HOST_DIR: ${LOGS_HOST_DIR:-/opt/letsbe-hub/logs} + volumes: + # Docker socket for spawning runner containers + - /var/run/docker.sock:/var/run/docker.sock + # Job configs (bind mount to host path so runners can access) + - ${JOBS_HOST_DIR:-/opt/letsbe-hub/jobs}:/app/jobs + - ${LOGS_HOST_DIR:-/opt/letsbe-hub/logs}:/app/logs + # Run as root to access Docker socket + user: "0:0" + depends_on: + db: + condition: service_healthy + restart: unless-stopped + networks: + - hub-internal + - traefik + labels: + - "traefik.enable=true" + - "traefik.http.routers.hub.rule=Host(`${HUB_DOMAIN}`)" + - "traefik.http.routers.hub.entrypoints=websecure" + - "traefik.http.routers.hub.tls.certresolver=letsencrypt" + - "traefik.http.services.hub.loadbalancer.server.port=3000" + +volumes: + hub-db-data: + name: letsbe-hub-db + +networks: + hub-internal: + traefik: + external: true diff --git a/deploy/setup.sh b/deploy/setup.sh new file mode 100644 index 0000000..69c425b --- /dev/null +++ b/deploy/setup.sh @@ -0,0 +1,69 @@ +#!/bin/bash +# +# LetsBe Hub Production Setup Script +# +# Usage: ./setup.sh +# + +set -e + +echo "=== LetsBe Hub Setup ===" +echo "" + +# Check if .env exists +if [[ ! -f .env ]]; then + echo "ERROR: .env file not found!" + echo "Copy .env.example to .env and fill in the values first." + exit 1 +fi + +# Source .env to get variables +source .env + +# Validate required variables +REQUIRED_VARS=( + "HUB_URL" + "HUB_DOMAIN" + "POSTGRES_PASSWORD" + "NEXTAUTH_SECRET" + "CREDENTIAL_ENCRYPTION_KEY" + "SETTINGS_ENCRYPTION_KEY" +) + +for var in "${REQUIRED_VARS[@]}"; do + if [[ -z "${!var}" || "${!var}" == *"CHANGE_ME"* ]]; then + echo "ERROR: $var is not set or still has placeholder value" + exit 1 + fi +done + +echo "1. Creating job directories..." +JOBS_DIR="${JOBS_HOST_DIR:-/opt/letsbe-hub/jobs}" +LOGS_DIR="${LOGS_HOST_DIR:-/opt/letsbe-hub/logs}" +mkdir -p "$JOBS_DIR" "$LOGS_DIR" +chmod 755 "$JOBS_DIR" "$LOGS_DIR" +echo " Created: $JOBS_DIR" +echo " Created: $LOGS_DIR" +echo "" + +echo "2. Logging into Gitea registry..." +echo " (You will need your Gitea username and password/token)" +docker login code.letsbe.solutions +echo "" + +echo "3. Pulling images..." +docker pull code.letsbe.solutions/letsbe/hub:${HUB_IMAGE_TAG:-master} +docker pull code.letsbe.solutions/letsbe/ansible-runner:${DOCKER_IMAGE_TAG:-master} +docker pull postgres:16-alpine +echo "" + +echo "4. Creating traefik network (if not exists)..." +docker network create traefik 2>/dev/null || true +echo "" + +echo "=== Setup Complete ===" +echo "" +echo "Now run: docker compose up -d" +echo "" +echo "Then configure Docker Hub and Gitea credentials in:" +echo " ${HUB_URL}/admin/settings"