#!/usr/bin/env bash # # Dezky local development bootstrap # Run this once when setting up the project for the first time. # # Usage: ./scripts/bootstrap.sh # set -euo pipefail # ──────────────────────────────────────── # Colors for output # ──────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BLUE='\033[0;34m' NC='\033[0m' info() { echo -e "${BLUE}[INFO]${NC} $*"; } ok() { echo -e "${GREEN}[OK]${NC} $*"; } warn() { echo -e "${YELLOW}[WARN]${NC} $*"; } error() { echo -e "${RED}[ERROR]${NC} $*" >&2; } # ──────────────────────────────────────── # Determine project root # ──────────────────────────────────────── SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" PROJECT_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" COMPOSE_DIR="$PROJECT_ROOT/infrastructure/docker-compose" CERTS_DIR="$COMPOSE_DIR/certs" cd "$PROJECT_ROOT" echo "" echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Dezky Local Development Bootstrap ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" # ──────────────────────────────────────── # Step 1: Check prerequisites # ──────────────────────────────────────── info "Step 1: Checking prerequisites..." check_command() { if ! command -v "$1" &> /dev/null; then error "$1 is not installed." echo " Install with: $2" exit 1 fi ok "$1 found: $(command -v "$1")" } check_command docker "Install Docker Desktop or OrbStack from https://orbstack.dev" check_command mkcert "brew install mkcert" check_command openssl "Should be preinstalled on macOS" check_command git "brew install git" if ! docker compose version &> /dev/null; then error "Docker Compose v2 not available." echo " Update Docker Desktop or install OrbStack." exit 1 fi ok "Docker Compose v2 available" # Check Docker daemon is running if ! docker info &> /dev/null; then error "Docker daemon not running. Start Docker Desktop / OrbStack first." exit 1 fi ok "Docker daemon running" echo "" # ──────────────────────────────────────── # Step 2: Configure git remote # ──────────────────────────────────────── info "Step 2: Configuring git remote..." GIT_REMOTE_URL="git@git.lastcloud.io:ronnibaslund/dezky.git" GIT_SSH_HOST="git.lastcloud.io" GIT_SSH_PORT="22222" if [[ -d "$PROJECT_ROOT/.git" ]]; then CURRENT_URL="$(git -C "$PROJECT_ROOT" remote get-url origin 2>/dev/null || true)" if [[ "$CURRENT_URL" == "$GIT_REMOTE_URL" ]]; then ok "Git remote 'origin' already set to $GIT_REMOTE_URL" elif [[ -n "$CURRENT_URL" ]]; then git -C "$PROJECT_ROOT" remote set-url origin "$GIT_REMOTE_URL" ok "Updated git remote 'origin' → $GIT_REMOTE_URL (was $CURRENT_URL)" else git -C "$PROJECT_ROOT" remote add origin "$GIT_REMOTE_URL" ok "Added git remote 'origin' → $GIT_REMOTE_URL" fi # Gitea's git SSH listens on a non-standard port. Without an ssh config # entry, git defaults to port 22 and the global "Host *" 1Password agent # offers too many keys — the server rejects the connection before the right # key is tried. Pin the host to port 22222 and the registered key only. if [[ "$(ssh -G "$GIT_SSH_HOST" 2>/dev/null | awk '/^port /{print $2}')" == "$GIT_SSH_PORT" ]]; then ok "SSH config already routes $GIT_SSH_HOST to port $GIT_SSH_PORT" else warn "$GIT_SSH_HOST is not pinned to port $GIT_SSH_PORT in your SSH config" echo "" echo "The following block is needed in ~/.ssh/config so git can reach Gitea:" echo "" echo " Host $GIT_SSH_HOST" echo " HostName $GIT_SSH_HOST" echo " Port $GIT_SSH_PORT" echo " User git" echo " IdentityFile ~/.ssh/id_ed25519" echo " IdentitiesOnly yes" echo "" read -p "Append this block to ~/.ssh/config automatically? [y/N] " -n 1 -r echo "" if [[ $REPLY =~ ^[Yy]$ ]]; then mkdir -p "$HOME/.ssh" { echo "" echo "# Gitea (lastcloud) — Git SSH on port $GIT_SSH_PORT. Force the registered" echo "# key only; the global \"Host *\" agent otherwise offers too many keys." echo "Host $GIT_SSH_HOST" echo " HostName $GIT_SSH_HOST" echo " Port $GIT_SSH_PORT" echo " User git" echo " IdentityFile ~/.ssh/id_ed25519" echo " IdentitiesOnly yes" } >> "$HOME/.ssh/config" chmod 600 "$HOME/.ssh/config" ok "Appended SSH config block for $GIT_SSH_HOST" else warn "Skipping SSH config — pushes to $GIT_SSH_HOST may fail until you add it" fi fi else warn "No .git directory in $PROJECT_ROOT — skipping git remote setup" fi echo "" # ──────────────────────────────────────── # Step 3: Generate TLS certificates # ──────────────────────────────────────── info "Step 3: Setting up TLS certificates..." mkdir -p "$CERTS_DIR" cd "$CERTS_DIR" if [[ -f "dezky.local.pem" && -f "dezky.local-key.pem" ]]; then ok "TLS certificates already exist in $CERTS_DIR" else info "Generating wildcard certificate for *.dezky.local..." if ! mkcert -CAROOT &> /dev/null; then warn "mkcert root CA not found. Running mkcert -install..." mkcert -install fi mkcert "*.dezky.local" "dezky.local" "localhost" "127.0.0.1" "::1" # Normalize filenames (mkcert adds counts) mv ./_wildcard.dezky.local+*.pem dezky.local.pem 2>/dev/null || true mv ./_wildcard.dezky.local+*-key.pem dezky.local-key.pem 2>/dev/null || true ok "TLS certificates generated" fi cd "$PROJECT_ROOT" echo "" # ──────────────────────────────────────── # Step 4: Update /etc/hosts # ──────────────────────────────────────── info "Step 4: Setting up /etc/hosts entries..." HOSTS_ENTRIES=( "dezky.local" "www.dezky.local" "app.dezky.local" "operator.dezky.local" "auth.dezky.local" "mail.dezky.local" "files.dezky.local" "office.dezky.local" "meet.dezky.local" "chat.dezky.local" "traefik.dezky.local" ) MISSING_ENTRIES=() for entry in "${HOSTS_ENTRIES[@]}"; do if ! grep -q "127.0.0.1[[:space:]]\+.*\b${entry}\b" /etc/hosts; then MISSING_ENTRIES+=("$entry") fi done if [[ ${#MISSING_ENTRIES[@]} -eq 0 ]]; then ok "All /etc/hosts entries already present" else warn "Missing /etc/hosts entries: ${MISSING_ENTRIES[*]}" echo "" echo "Add the following line to /etc/hosts (requires sudo):" echo "" echo "127.0.0.1 ${HOSTS_ENTRIES[*]}" echo "" read -p "Add these entries automatically? (requires sudo) [y/N] " -n 1 -r echo "" if [[ $REPLY =~ ^[Yy]$ ]]; then HOSTS_LINE="127.0.0.1 ${HOSTS_ENTRIES[*]}" echo "$HOSTS_LINE" | sudo tee -a /etc/hosts > /dev/null ok "Added /etc/hosts entries" else warn "Skipping /etc/hosts setup — you must add entries manually before continuing" fi fi echo "" # ──────────────────────────────────────── # Step 5: Generate .env file # ──────────────────────────────────────── info "Step 5: Setting up .env file..." if [[ -f "$PROJECT_ROOT/.env" ]]; then ok ".env file already exists" else info "Generating .env with secure random values..." cp "$PROJECT_ROOT/.env.example" "$PROJECT_ROOT/.env" # Replace all 'changeme_*' placeholders with actual random values if [[ "$OSTYPE" == "darwin"* ]]; then SED_INPLACE=(-i '') else SED_INPLACE=(-i) fi while IFS= read -r line; do if [[ "$line" =~ ^([A-Z_]+)=changeme ]]; then VAR_NAME="${BASH_REMATCH[1]}" if [[ "$VAR_NAME" == "AUTHENTIK_SECRET_KEY" ]]; then NEW_VALUE=$(openssl rand -hex 50) else NEW_VALUE=$(openssl rand -hex 32) fi sed "${SED_INPLACE[@]}" "s|^${VAR_NAME}=changeme.*|${VAR_NAME}=${NEW_VALUE}|" "$PROJECT_ROOT/.env" fi done < "$PROJECT_ROOT/.env.example" ok ".env generated with secure random values" warn "Default admin password generated. Check .env for AUTHENTIK_BOOTSTRAP_PASSWORD" fi echo "" # ──────────────────────────────────────── # Step 6: Pull Docker images # ──────────────────────────────────────── info "Step 6: Pulling Docker images (this may take a few minutes)..." cd "$COMPOSE_DIR" docker compose pull ok "All images pulled" echo "" # ──────────────────────────────────────── # Step 7: Start the stack in stages # ──────────────────────────────────────── info "Step 7: Starting services..." info "Starting database layer (postgres, mongo, redis)..." docker compose up -d postgres mongo redis info "Waiting for databases to be healthy..." sleep 10 for service in postgres mongo redis; do until [[ "$(docker inspect --format='{{.State.Health.Status}}' dezky-$service 2>/dev/null)" == "healthy" ]]; do echo -n "." sleep 2 done echo "" ok "$service is healthy" done info "Starting Traefik reverse proxy..." docker compose up -d traefik info "Starting Authentik..." docker compose up -d authentik-server authentik-worker info "Waiting 30 seconds for Authentik to bootstrap..." sleep 30 info "Starting application services..." docker compose up -d stalwart ocis collabora echo "" ok "Stack started" echo "" # ──────────────────────────────────────── # Final instructions # ──────────────────────────────────────── echo "╔══════════════════════════════════════════════════════════════╗" echo "║ Setup Complete ║" echo "╚══════════════════════════════════════════════════════════════╝" echo "" echo "Service URLs:" echo " Website: https://dezky.local" echo " Portal: https://app.dezky.local" echo " Authentik (auth): https://auth.dezky.local" echo " Mail (admin): https://mail.dezky.local" echo " Files (OCIS): https://files.dezky.local" echo " Office: https://office.dezky.local" echo " Traefik: https://traefik.dezky.local" echo "" echo "First-time Authentik admin login:" echo " URL: https://auth.dezky.local/if/flow/initial-setup/" echo " Email: admin@dezky.local" echo " Password: (see AUTHENTIK_BOOTSTRAP_PASSWORD in .env)" echo "" echo "Next steps:" echo " 1. Configure Authentik (see docs/AUTHENTIK-SETUP.md)" echo " 2. Configure OCIS OIDC provider in Authentik" echo " 3. Start building the portal in apps/portal/" echo "" echo "Useful commands:" echo " Logs: docker compose -f $COMPOSE_DIR/docker-compose.yml logs -f [service]" echo " Stop: docker compose -f $COMPOSE_DIR/docker-compose.yml down" echo " Reset: $PROJECT_ROOT/scripts/reset.sh" echo ""