58a2c8077d
Wraps Stalwart in EAS so iOS/Android native Mail/Calendar 'Exchange' accounts get two-way mail+calendar+contacts sync (BackendCombined: IMAP + CalDAV /dav/cal/%l/ + CardDAV, credentials pass through). - services/zpush: Z-Push 2.6.4 (AGPLv3, see LICENSE-NOTES.md) on php:8.2-apache-bookworm (trixie dropped libc-client); PHP 8 sysv sprintf fatal sed-patched; autodiscover dispatcher answers mobilesync schema, proxies outlook schema to Stalwart unchanged - prod: zpush Deployment (replicas:1, Recreate — file sync state), /Microsoft-Server-ActiveSync Ingress on mail.dezky.eu (no redirect, POST-heavy), autodiscover.dezky.eu repointed to the dispatcher, selectorless stalwart-imaps/-smtps Services (host-Stalwart is implicit-TLS only: 993/465, no plain 143/587 — verified on node1) - CI: build+deploy zpush like the other apps EAS tops out at 14.1: covers native mobile clients, NOT the Outlook mobile app (needs 16.1) and not new Outlook for Windows (no EAS).
337 lines
13 KiB
Bash
Executable File
337 lines
13 KiB
Bash
Executable File
#!/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"
|
|
"autodiscover.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 ""
|