#!/usr/bin/env bash # run.sh — Start the FreeIPA stack with correct cgroup namespace on any host. # # Usage: # ./run.sh # freeipa only # ./run.sh all # whole stack (freeipa + postgres + keycloak + checkmk # # + nextcloud + redis + nginx gateway) # ./run.sh down # stop and remove containers # # Why this exists: # The FreeIPA container runs systemd (/sbin/init) and requires the host cgroup # namespace on systems that use cgroup v2 (WSL2, recent Fedora/Ubuntu, etc.). # Docker Compose's schema does not expose a cgroupns_mode field, so this script # starts the freeipa container via `docker run --cgroupns host` and defers the # rest of the stack to `docker compose up`. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" cd "$SCRIPT_DIR" [[ -f .env ]] || { echo "ERROR: .env not found. Copy .env.example and fill in values."; exit 1; } # shellcheck disable=SC1091 source .env MODE="${1:-}" stop_freeipa() { docker rm -f freeipa 2>/dev/null || true } # Create the nextcloud role/database if they are missing. # # postgres/initdb/10-nextcloud.sh already does this, but the image only runs # initdb hooks when the data directory is empty. Stacks created before Nextcloud # existed have a populated keycloak-db volume, so the hook never fires there and # Nextcloud's installer would fail with "database nextcloud does not exist". # This covers that upgrade path and is a no-op on fresh installs. ensure_nextcloud_db() { local _pw="${NC_DB_PASSWORD:?set NC_DB_PASSWORD in .env}" echo "Waiting for PostgreSQL..." local _i for _i in $(seq 1 30); do docker compose exec -T postgres pg_isready -U keycloak &>/dev/null && break [[ $_i -eq 30 ]] && { echo "ERROR: PostgreSQL not ready after 60s."; exit 1; } sleep 2 done # psql has no CREATE ROLE/DATABASE IF NOT EXISTS, and CREATE DATABASE cannot # run inside a transaction (so no DO block either) — hence query-then-create. # # The password is handed to psql as a variable and interpolated with :'pw', # which applies psql's own literal quoting, so a password containing quotes # or backslashes cannot break (or inject into) the statement. # # That interpolation only happens for SQL read from stdin — `psql -c` sends # its argument to the server verbatim and would fail with a syntax error on # the literal ":'pw'". Hence the inner heredoc rather than -c. # # The outer heredoc is quoted ('SH') so $NCPW expands in the container, # where `docker compose exec -e` set it, and never on this host. docker compose exec -T -e NCPW="$_pw" postgres sh -s <<'SH' \ && echo "Nextcloud database ready." \ || { echo "ERROR: could not create the nextcloud database."; exit 1; } set -e psql -U keycloak -d keycloak -tAc "SELECT 1 FROM pg_roles WHERE rolname='nextcloud'" \ | grep -q 1 \ || psql -v ON_ERROR_STOP=1 -U keycloak -d keycloak -v pw="$NCPW" <<'Q' CREATE ROLE nextcloud LOGIN PASSWORD :'pw'; Q psql -U keycloak -d keycloak -tAc "SELECT 1 FROM pg_database WHERE datname='nextcloud'" \ | grep -q 1 \ || psql -v ON_ERROR_STOP=1 -U keycloak -d keycloak <<'Q' CREATE DATABASE nextcloud OWNER nextcloud ENCODING 'UTF8'; Q SH } start_freeipa() { stop_freeipa # Ensure the compose-managed network exists before docker run uses it docker compose up --no-start freeipa 2>/dev/null || true NETWORK=$(docker network ls --filter name=ipa-net --format '{{.Name}}' | grep ipa-net | head -1) if [[ -z "$NETWORK" ]]; then # Label the network exactly as compose would so a later # `docker compose up` (checkmk/postgres/keycloak) adopts it instead of # failing with "network exists but was not created by compose". docker network create --subnet=172.30.0.0/24 \ --label com.docker.compose.network=ipa-net \ --label com.docker.compose.project=image \ --label com.docker.compose.version=2 \ ipa-net NETWORK=ipa-net fi # Ensure the data volumes exist (cmk-creds is shared with the checkmk # container: CheckMK writes automation.secret there, the IPA container's # ansipa-checkmk-setup.sh reads it — without this mount the whole # dev_mon_* CheckMK integration silently never activates) docker volume create freeipa-data 2>/dev/null || true docker volume create cmk-creds 2>/dev/null || true docker run -d \ --name freeipa \ --hostname "${IPA_HOSTNAME:-ipa.example.com}" \ --privileged \ --cgroupns host \ --tmpfs /run \ --tmpfs /tmp \ -v /sys/fs/cgroup:/sys/fs/cgroup:rw \ -v freeipa-data:/data \ -v cmk-creds:/cmk-creds \ --network "$NETWORK" \ --ip 172.30.0.10 \ -e IPA_DOMAIN="${IPA_DOMAIN:?}" \ -e IPA_REALM="${IPA_REALM:-}" \ -e IPA_ADMIN_PASSWORD="${IPA_ADMIN_PASSWORD:?}" \ -e IPA_DM_PASSWORD="${IPA_DM_PASSWORD:?}" \ -e IPA_SETUP_DNS="${IPA_SETUP_DNS:-false}" \ -e IPA_DNS_FORWARDER="${IPA_DNS_FORWARDER:-}" \ -e IPA_SETUP_KRA="${IPA_SETUP_KRA:-false}" \ -e LUKS_KEY_UPLOAD_PASSWORD="${LUKS_KEY_UPLOAD_PASSWORD:?}" \ -e CMK_URL="http://172.30.0.12:5000" \ -e CMK_ADVERTISED_URL="${CMK_ADVERTISED_URL:-}" \ -e CMK_SITE_ID="${CMK_SITE_ID:-cmk}" \ -e ANSIPA_GIT_SSH_PORT="${ANSIPA_GIT_SSH_PORT:-2222}" \ -e ANSIPA_GIT_ADMIN_PUBKEY="${ANSIPA_GIT_ADMIN_PUBKEY:-}" \ -e ANSIPA_GIT_SIGNING_PUBKEY="${ANSIPA_GIT_SIGNING_PUBKEY:-}" \ -p 389:389 \ -p 636:636 \ -p 88:88 \ -p 88:88/udp \ -p 464:464 \ -p 464:464/udp \ -p 443:443 \ -p 445:445 \ -p 139:139 \ -p 137:137/udp \ -p 138:138/udp \ -p "${ANSIPA_GIT_SSH_PORT:-2222}:${ANSIPA_GIT_SSH_PORT:-2222}" \ freeipa-server:local echo "FreeIPA container started. Watch first-boot progress:" echo " docker exec freeipa journalctl -f -u ipa-first-boot.service" echo " (first-boot takes ~10 min)" } case "$MODE" in down) stop_freeipa docker compose down echo "Stack stopped." ;; all) start_freeipa docker compose up -d postgres ensure_nextcloud_db docker compose up -d keycloak checkmk nextcloud-redis nextcloud nginx echo "Full stack started (freeipa + postgres + keycloak + checkmk + nextcloud + nginx)." echo echo "Next, once the containers report healthy:" echo " ./keycloak-configure.sh # Keycloak ← FreeIPA LDAP, and OIDC clients" echo " ./nextcloud-configure.sh # Nextcloud ← IPA LDAP + Keycloak OIDC" echo " ./checkmk-ldap-configure.sh # CheckMK ← FreeIPA LDAP" ;; "") start_freeipa ;; *) echo "Usage: $0 [all|down]" exit 1 ;; esac