From a746fe4cabb8b00e672a91b654f177c95861bc4a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 13:36:34 +0000 Subject: [PATCH 1/8] Add Docker setup script for automated deployment configuration Replaces manual editing of docker-compose.yaml with a setup script that generates secure random passwords, creates a .env file, generates SSL certificates, and creates required directories. Docker Compose files now use ${VAR} substitution from .env with safe defaults. New files: - docker-setup.sh: Interactive/auto setup script - .env.example: Documented template for environment configuration https://claude.ai/code/session_01KdeKniaCeDDV1UvnkTLQcb --- .env.example | 35 +++++ .gitignore | 1 + docker-compose-build.yaml | 31 ++-- docker-compose.yaml | 31 ++-- docker-setup.sh | 292 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 354 insertions(+), 36 deletions(-) create mode 100644 .env.example create mode 100755 docker-setup.sh diff --git a/.env.example b/.env.example new file mode 100644 index 000000000..36324f9d7 --- /dev/null +++ b/.env.example @@ -0,0 +1,35 @@ +# ============================================================================ +# Dungeon Master's Vault — Docker Environment Configuration +# +# Copy this file to .env and update the values: +# cp .env.example .env +# +# Or run the setup script to generate .env with secure random values: +# ./docker-setup.sh +# ============================================================================ + +# --- Application --- +PORT=8890 + +# --- Datomic Database --- +# ADMIN_PASSWORD secures the Datomic admin interface +# DATOMIC_PASSWORD is used by the application to connect to Datomic +# The password in DATOMIC_URL must match DATOMIC_PASSWORD +ADMIN_PASSWORD=change-me-admin +DATOMIC_PASSWORD=change-me-datomic +DATOMIC_URL=datomic:free://datomic:4334/orcpub?password=change-me-datomic + +# --- Security --- +# Secret used to sign JWT tokens (20+ characters recommended) +SIGNATURE=change-me-to-something-unique-and-long + +# --- Email (SMTP) --- +# Leave EMAIL_SERVER_URL empty to disable email functionality +EMAIL_SERVER_URL= +EMAIL_ACCESS_KEY= +EMAIL_SECRET_KEY= +EMAIL_SERVER_PORT=587 +EMAIL_FROM_ADDRESS= +EMAIL_ERRORS_TO= +EMAIL_SSL=FALSE +EMAIL_TLS=FALSE diff --git a/.gitignore b/.gitignore index 3e3c55535..a49906f50 100644 --- a/.gitignore +++ b/.gitignore @@ -22,6 +22,7 @@ pom.xml orcpub.iml profiles.clj env.sh +.env .repl .nrepl-port *~ diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml index 296e97768..611a34958 100644 --- a/docker-compose-build.yaml +++ b/docker-compose-build.yaml @@ -6,21 +6,17 @@ services: context: . dockerfile: docker/orcpub/Dockerfile environment: - PORT: 8890 - EMAIL_SERVER_URL: '' - EMAIL_ACCESS_KEY: '' - EMAIL_SECRET_KEY: '' - EMAIL_SERVER_PORT: 587 - # Email address to send from, will default to 'no-reply@orcpub.com' - EMAIL_FROM_ADDRESS: '' - # Email address to send errors to - EMAIL_ERRORS_TO: '' - EMAIL_SSL: 'TRUE' - EMAIL_TLS: 'FALSE' - # Datomic connection string - Make sure the matches the DATOMIC_PASSWORD below - DATOMIC_URL: datomic:free://datomic:4334/orcpub?password= - # The secret used to hash your password in the browser, 20+ characters recommended - SIGNATURE: '' + PORT: ${PORT:-8890} + EMAIL_SERVER_URL: ${EMAIL_SERVER_URL:-} + EMAIL_ACCESS_KEY: ${EMAIL_ACCESS_KEY:-} + EMAIL_SECRET_KEY: ${EMAIL_SECRET_KEY:-} + EMAIL_SERVER_PORT: ${EMAIL_SERVER_PORT:-587} + EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-} + EMAIL_ERRORS_TO: ${EMAIL_ERRORS_TO:-} + EMAIL_SSL: ${EMAIL_SSL:-FALSE} + EMAIL_TLS: ${EMAIL_TLS:-FALSE} + DATOMIC_URL: ${DATOMIC_URL:-datomic:free://datomic:4334/orcpub?password=change-me} + SIGNATURE: ${SIGNATURE:-change-me-to-something-unique} depends_on: - datomic restart: always @@ -29,9 +25,8 @@ services: context: . dockerfile: docker/datomic/Dockerfile environment: - ADMIN_PASSWORD: - # Must match the in the DATOMIC_URL above. - DATOMIC_PASSWORD: + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me-admin} + DATOMIC_PASSWORD: ${DATOMIC_PASSWORD:-change-me} volumes: - ./data:/data - ./logs:/logs diff --git a/docker-compose.yaml b/docker-compose.yaml index 5ebaa5048..23e242306 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -4,30 +4,25 @@ services: orcpub: image: orcpub/orcpub:latest environment: - PORT: 8890 - EMAIL_SERVER_URL: '' - EMAIL_ACCESS_KEY: '' - EMAIL_SECRET_KEY: '' - EMAIL_SERVER_PORT: 587 - # Email address to send from, will default to 'no-reply@orcpub.com' - EMAIL_FROM_ADDRESS: '' - # Email address to send errors to - EMAIL_ERRORS_TO: '' - EMAIL_SSL: 'FALSE' - EMAIL_TLS: 'FALSE' - # Datomic connection string - Make sure the matches the DATOMIC_PASSWORD below - DATOMIC_URL: datomic:free://datomic:4334/orcpub?password= - # The secret used to hash your password in the browser, 20+ characters recommended - SIGNATURE: '' + PORT: ${PORT:-8890} + EMAIL_SERVER_URL: ${EMAIL_SERVER_URL:-} + EMAIL_ACCESS_KEY: ${EMAIL_ACCESS_KEY:-} + EMAIL_SECRET_KEY: ${EMAIL_SECRET_KEY:-} + EMAIL_SERVER_PORT: ${EMAIL_SERVER_PORT:-587} + EMAIL_FROM_ADDRESS: ${EMAIL_FROM_ADDRESS:-} + EMAIL_ERRORS_TO: ${EMAIL_ERRORS_TO:-} + EMAIL_SSL: ${EMAIL_SSL:-FALSE} + EMAIL_TLS: ${EMAIL_TLS:-FALSE} + DATOMIC_URL: ${DATOMIC_URL:-datomic:free://datomic:4334/orcpub?password=change-me} + SIGNATURE: ${SIGNATURE:-change-me-to-something-unique} depends_on: - datomic restart: always datomic: image: orcpub/datomic:latest environment: - ADMIN_PASSWORD: - # Must match the in the DATOMIC_URL above. - DATOMIC_PASSWORD: + ADMIN_PASSWORD: ${ADMIN_PASSWORD:-change-me-admin} + DATOMIC_PASSWORD: ${DATOMIC_PASSWORD:-change-me} volumes: - ./data:/data - ./logs:/logs diff --git a/docker-setup.sh b/docker-setup.sh new file mode 100755 index 000000000..9caf9b238 --- /dev/null +++ b/docker-setup.sh @@ -0,0 +1,292 @@ +#!/usr/bin/env bash +# +# OrcPub / Dungeon Master's Vault — Docker Setup Script +# +# Prepares everything needed to run the application via Docker Compose: +# 1. Generates secure random passwords and a signing secret +# 2. Creates a .env file (or uses an existing one) +# 3. Generates self-signed SSL certificates (if missing) +# 4. Creates required directories (data, logs, deploy/homebrew) +# +# Usage: +# ./docker-setup.sh # Interactive mode — prompts for optional values +# ./docker-setup.sh --auto # Non-interactive — accepts all defaults +# ./docker-setup.sh --help # Show usage +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="${SCRIPT_DIR}/.env" +ENV_EXAMPLE="${SCRIPT_DIR}/.env.example" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +color_green='\033[0;32m' +color_yellow='\033[1;33m' +color_red='\033[0;31m' +color_cyan='\033[0;36m' +color_reset='\033[0m' + +info() { printf "${color_green}[INFO]${color_reset} %s\n" "$*"; } +warn() { printf "${color_yellow}[WARN]${color_reset} %s\n" "$*"; } +error() { printf "${color_red}[ERROR]${color_reset} %s\n" "$*" >&2; } +header() { printf "\n${color_cyan}=== %s ===${color_reset}\n\n" "$*"; } + +generate_password() { + # Generate a URL-safe random password (no special chars that break URLs/YAML) + local length="${1:-24}" + if command -v openssl &>/dev/null; then + openssl rand -base64 "$length" | tr -d '/+=' | head -c "$length" + elif [ -r /dev/urandom ]; then + tr -dc 'A-Za-z0-9' < /dev/urandom | head -c "$length" + else + error "Cannot generate random password: no openssl or /dev/urandom available" + exit 1 + fi +} + +prompt_value() { + local prompt_text="$1" + local default_value="$2" + local result + + if [ "${AUTO_MODE:-false}" = "true" ]; then + echo "$default_value" + return + fi + + if [ -n "$default_value" ]; then + read -rp "${prompt_text} [${default_value}]: " result + echo "${result:-$default_value}" + else + read -rp "${prompt_text}: " result + echo "$result" + fi +} + +usage() { + cat <<'USAGE' +Usage: ./docker-setup.sh [OPTIONS] + +Options: + --auto Non-interactive mode; accept all defaults + --force Overwrite existing .env file + --help Show this help message + +Examples: + ./docker-setup.sh # Interactive setup + ./docker-setup.sh --auto # Quick setup with generated defaults + ./docker-setup.sh --auto --force # Regenerate everything from scratch +USAGE +} + +# --------------------------------------------------------------------------- +# Parse arguments +# --------------------------------------------------------------------------- + +AUTO_MODE=false +FORCE_MODE=false + +for arg in "$@"; do + case "$arg" in + --auto) AUTO_MODE=true ;; + --force) FORCE_MODE=true ;; + --help) usage; exit 0 ;; + *) + error "Unknown option: $arg" + usage + exit 1 + ;; + esac +done + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +header "Dungeon Master's Vault — Docker Setup" + +# ---- Step 1: .env file --------------------------------------------------- + +if [ -f "$ENV_FILE" ] && [ "$FORCE_MODE" = "false" ]; then + info "Existing .env file found. Skipping generation (use --force to overwrite)." +else + info "Generating secure credentials..." + + ADMIN_PASSWORD="$(generate_password 24)" + DATOMIC_PASSWORD="$(generate_password 24)" + SIGNATURE="$(generate_password 32)" + + header "Configuration" + + PORT=$(prompt_value "Application port" "8890") + EMAIL_SERVER_URL=$(prompt_value "SMTP server URL (leave empty to skip email)" "") + EMAIL_ACCESS_KEY="" + EMAIL_SECRET_KEY="" + EMAIL_SERVER_PORT="587" + EMAIL_FROM_ADDRESS="" + EMAIL_ERRORS_TO="" + EMAIL_SSL="FALSE" + EMAIL_TLS="FALSE" + + if [ -n "$EMAIL_SERVER_URL" ]; then + EMAIL_ACCESS_KEY=$(prompt_value "SMTP username" "") + EMAIL_SECRET_KEY=$(prompt_value "SMTP password" "") + EMAIL_SERVER_PORT=$(prompt_value "SMTP port" "587") + EMAIL_FROM_ADDRESS=$(prompt_value "From email address" "no-reply@orcpub.com") + EMAIL_ERRORS_TO=$(prompt_value "Error notification email" "") + EMAIL_SSL=$(prompt_value "Use SSL? (TRUE/FALSE)" "FALSE") + EMAIL_TLS=$(prompt_value "Use TLS? (TRUE/FALSE)" "FALSE") + fi + + info "Writing .env file..." + + cat > "$ENV_FILE" </dev/null; then + info "Generating self-signed SSL certificate..." + openssl req \ + -subj "/C=US/ST=State/L=City/O=OrcPub/OU=Dev/CN=localhost" \ + -x509 \ + -nodes \ + -days 365 \ + -newkey rsa:2048 \ + -keyout "$KEY_FILE" \ + -out "$CERT_FILE" \ + 2>/dev/null + info "SSL certificate generated (valid for 365 days)." + else + warn "openssl not found — cannot generate SSL certificates." + warn "Install openssl and run: ./deploy/snakeoil.sh" + fi +fi + +# ---- Step 4: Validation -------------------------------------------------- + +header "Validation" + +ERRORS=0 + +check_file() { + local label="$1" path="$2" + if [ -f "$path" ]; then + info " ${label}: OK" + else + warn " ${label}: MISSING (${path})" + ERRORS=$((ERRORS + 1)) + fi +} + +check_dir() { + local label="$1" path="$2" + if [ -d "$path" ]; then + info " ${label}: OK" + else + warn " ${label}: MISSING (${path})" + ERRORS=$((ERRORS + 1)) + fi +} + +check_file ".env" "$ENV_FILE" +check_file "docker-compose.yaml" "${SCRIPT_DIR}/docker-compose.yaml" +check_file "nginx.conf" "${SCRIPT_DIR}/deploy/nginx.conf" +check_file "SSL certificate" "$CERT_FILE" +check_file "SSL key" "$KEY_FILE" +check_dir "data/" "${SCRIPT_DIR}/data" +check_dir "logs/" "${SCRIPT_DIR}/logs" +check_dir "deploy/homebrew/" "${SCRIPT_DIR}/deploy/homebrew" + +echo "" + +if [ "$ERRORS" -gt 0 ]; then + warn "Setup completed with ${ERRORS} warning(s). Review the items above." +else + info "All checks passed!" +fi + +# ---- Step 5: Next steps --------------------------------------------------- + +header "Next Steps" + +cat <<'NEXT' +1. Review your .env file and adjust values if needed. + +2. Launch the application: + docker-compose up -d + +3. Access the site at: + https://localhost + +4. To import homebrew content, place your .orcbrew file at: + deploy/homebrew/homebrew.orcbrew + +5. To build from source instead of pulling images: + docker-compose -f docker-compose-build.yaml build + docker-compose -f docker-compose-build.yaml up -d + +For more details, see README.md. +NEXT From 28cede51bd2760041dafa564c738f3922c8c40f8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 13:47:13 +0000 Subject: [PATCH 2/8] Add Docker user management script for injecting users into Datomic Solves the problem of managing users when running in Docker, where connecting an external REPL to the containerized Datomic has always failed. Uses docker exec + clojure.main against the uberjar classpath to run Datomic peer operations inside the container. New files: - docker-user.sh: Shell wrapper that finds the orcpub container, waits for Datomic readiness, and runs commands - docker/scripts/manage-user.clj: Clojure script supporting create (auto-verified), verify, check, and list operations Also updates docker-setup.sh to prompt for passwords interactively instead of silently generating them. https://claude.ai/code/session_01KdeKniaCeDDV1UvnkTLQcb --- docker-setup.sh | 29 +++-- docker-user.sh | 198 +++++++++++++++++++++++++++++++++ docker/scripts/manage-user.clj | 154 +++++++++++++++++++++++++ 3 files changed, 373 insertions(+), 8 deletions(-) create mode 100755 docker-user.sh create mode 100644 docker/scripts/manage-user.clj diff --git a/docker-setup.sh b/docker-setup.sh index 9caf9b238..e03c527de 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -114,13 +114,18 @@ header "Dungeon Master's Vault — Docker Setup" if [ -f "$ENV_FILE" ] && [ "$FORCE_MODE" = "false" ]; then info "Existing .env file found. Skipping generation (use --force to overwrite)." else - info "Generating secure credentials..." + header "Database Passwords" - ADMIN_PASSWORD="$(generate_password 24)" - DATOMIC_PASSWORD="$(generate_password 24)" - SIGNATURE="$(generate_password 32)" + # Generate defaults but let user override + DEFAULT_ADMIN_PW="$(generate_password 24)" + DEFAULT_DATOMIC_PW="$(generate_password 24)" + DEFAULT_SIGNATURE="$(generate_password 32)" - header "Configuration" + ADMIN_PASSWORD=$(prompt_value "Datomic admin password" "$DEFAULT_ADMIN_PW") + DATOMIC_PASSWORD=$(prompt_value "Datomic application password" "$DEFAULT_DATOMIC_PW") + SIGNATURE=$(prompt_value "JWT signing secret (20+ chars)" "$DEFAULT_SIGNATURE") + + header "Application" PORT=$(prompt_value "Application port" "8890") EMAIL_SERVER_URL=$(prompt_value "SMTP server URL (leave empty to skip email)" "") @@ -278,13 +283,21 @@ cat <<'NEXT' 2. Launch the application: docker-compose up -d -3. Access the site at: +3. Create your first user (once containers are running): + ./docker-user.sh create + +4. Access the site at: https://localhost -4. To import homebrew content, place your .orcbrew file at: +5. Manage users later with: + ./docker-user.sh list # List all users + ./docker-user.sh check # Check a user's status + ./docker-user.sh verify # Verify an unverified user + +6. To import homebrew content, place your .orcbrew file at: deploy/homebrew/homebrew.orcbrew -5. To build from source instead of pulling images: +7. To build from source instead of pulling images: docker-compose -f docker-compose-build.yaml build docker-compose -f docker-compose-build.yaml up -d diff --git a/docker-user.sh b/docker-user.sh new file mode 100755 index 000000000..f1b4ca68e --- /dev/null +++ b/docker-user.sh @@ -0,0 +1,198 @@ +#!/usr/bin/env bash +# +# OrcPub Docker User Management +# +# Injects and verifies users in the Datomic database running inside Docker. +# Works by executing Clojure code inside the orcpub container, using the +# uberjar classpath (which already has datomic.api and buddy.hashers). +# +# Usage: +# ./docker-user.sh create +# ./docker-user.sh verify +# ./docker-user.sh check +# ./docker-user.sh list +# +# The script auto-detects the orcpub container name from docker-compose. +# + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +MANAGE_SCRIPT="${SCRIPT_DIR}/docker/scripts/manage-user.clj" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +color_green='\033[0;32m' +color_red='\033[0;31m' +color_yellow='\033[1;33m' +color_reset='\033[0m' + +info() { printf "${color_green}[OK]${color_reset} %s\n" "$*"; } +error() { printf "${color_red}[ERROR]${color_reset} %s\n" "$*" >&2; } +warn() { printf "${color_yellow}[WARN]${color_reset} %s\n" "$*"; } + +usage() { + cat <<'USAGE' +OrcPub Docker User Management + +Usage: + ./docker-user.sh create + Create a new user (auto-verified, skips email) + + ./docker-user.sh verify + Verify an existing unverified user + + ./docker-user.sh check + Check if a user exists and show their status + + ./docker-user.sh list + List all users in the database + +Options: + --container Override container name detection + --help Show this help + +Examples: + ./docker-user.sh create admin admin@example.com MySecurePass123 + ./docker-user.sh check admin + ./docker-user.sh list +USAGE +} + +# --------------------------------------------------------------------------- +# Find the orcpub container +# --------------------------------------------------------------------------- + +find_container() { + local container="" + + # Try docker-compose/docker compose service name first + if command -v docker-compose &>/dev/null; then + container=$(docker-compose ps -q orcpub 2>/dev/null || true) + fi + if [ -z "$container" ] && docker compose version &>/dev/null 2>&1; then + container=$(docker compose ps -q orcpub 2>/dev/null || true) + fi + + # Fallback: search by image name + if [ -z "$container" ]; then + container=$(docker ps -q --filter "ancestor=orcpub/orcpub:latest" 2>/dev/null | head -1 || true) + fi + + # Fallback: search by container name pattern + if [ -z "$container" ]; then + container=$(docker ps -q --filter "name=orcpub" 2>/dev/null | head -1 || true) + fi + + echo "$container" +} + +# --------------------------------------------------------------------------- +# Wait for container and Datomic to be ready +# --------------------------------------------------------------------------- + +wait_for_ready() { + local container="$1" + local max_wait=30 + local waited=0 + + # Check container is running + if ! docker inspect --format='{{.State.Running}}' "$container" 2>/dev/null | grep -q true; then + error "Container $container is not running." + error "Start it first: docker-compose up -d" + exit 1 + fi + + # Wait for the app to have connected to Datomic (the uberjar starts the + # Component system which connects on boot). We test by attempting a + # trivial Datomic query via clojure.main. + printf "Waiting for Datomic connection" + while [ $waited -lt $max_wait ]; do + if docker exec "$container" java -cp /orcpub.jar clojure.main -e \ + '(require (quote [datomic.api :as d])) (d/connect (System/getenv "DATOMIC_URL")) (println "ready")' \ + 2>/dev/null | grep -q "ready"; then + echo "" + return 0 + fi + printf "." + sleep 2 + waited=$((waited + 2)) + done + + echo "" + error "Timed out waiting for Datomic (${max_wait}s). Is the datomic container running?" + exit 1 +} + +# --------------------------------------------------------------------------- +# Run the management script inside the container +# --------------------------------------------------------------------------- + +run_in_container() { + local container="$1" + shift + + # Copy the management script into the container + docker cp "$MANAGE_SCRIPT" "${container}:/tmp/manage-user.clj" + + # Run it with the uberjar classpath + docker exec "$container" \ + java -cp /orcpub.jar clojure.main /tmp/manage-user.clj "$@" +} + +# --------------------------------------------------------------------------- +# Parse args and dispatch +# --------------------------------------------------------------------------- + +CONTAINER_OVERRIDE="" + +# Extract --container flag if present +ARGS=() +while [[ $# -gt 0 ]]; do + case "$1" in + --container) + CONTAINER_OVERRIDE="$2" + shift 2 + ;; + --help|-h) + usage + exit 0 + ;; + *) + ARGS+=("$1") + shift + ;; + esac +done + +set -- "${ARGS[@]+"${ARGS[@]}"}" + +if [ $# -eq 0 ]; then + usage + exit 1 +fi + +# Verify manage-user.clj exists +if [ ! -f "$MANAGE_SCRIPT" ]; then + error "Management script not found at: $MANAGE_SCRIPT" + exit 1 +fi + +# Find or use specified container +if [ -n "$CONTAINER_OVERRIDE" ]; then + CONTAINER="$CONTAINER_OVERRIDE" +else + CONTAINER=$(find_container) +fi + +if [ -z "$CONTAINER" ]; then + error "Cannot find the orcpub container." + error "Make sure the containers are running: docker-compose up -d" + exit 1 +fi + +# Wait for Datomic to be reachable, then run the command +wait_for_ready "$CONTAINER" +run_in_container "$CONTAINER" "$@" diff --git a/docker/scripts/manage-user.clj b/docker/scripts/manage-user.clj new file mode 100644 index 000000000..1f3a1f589 --- /dev/null +++ b/docker/scripts/manage-user.clj @@ -0,0 +1,154 @@ +;; OrcPub Docker User Management Script +;; +;; Runs inside the orcpub container using the uberjar classpath: +;; java -cp /orcpub.jar clojure.main /scripts/manage-user.clj [args...] +;; +;; Commands: +;; create — Create and auto-verify a user +;; verify — Verify an existing unverified user +;; check — Check if a user exists and their status +;; list — List all users (username + email + verified) + +(ns manage-user + (:require [datomic.api :as d] + [buddy.hashers :as hashers] + [clojure.string :as s])) + +(def datomic-url + (or (System/getenv "DATOMIC_URL") + "datomic:free://datomic:4334/orcpub?password=datomic")) + +(defn get-conn [] + (try + (d/connect datomic-url) + (catch Exception e + (binding [*out* *err*] + (println "ERROR: Cannot connect to Datomic at" datomic-url) + (println " Is the transactor running? Cause:" (.getMessage e))) + (System/exit 1)))) + +(defn find-user [db username-or-email] + (d/q '[:find (pull ?e [:orcpub.user/username + :orcpub.user/email + :orcpub.user/verified? + :orcpub.user/created + :db/id]) . + :in $ ?needle + :where + (or [?e :orcpub.user/username ?needle] + [?e :orcpub.user/email ?needle])] + db + username-or-email)) + +(defn create-user! [conn username email password] + (let [db (d/db conn) + email (s/lower-case (s/trim email)) + username (s/trim username)] + ;; Check for duplicates + (when (d/q '[:find ?e . :in $ ?email + :where [?e :orcpub.user/email ?email]] db email) + (binding [*out* *err*] + (println "ERROR: Email already registered:" email)) + (System/exit 1)) + (when (d/q '[:find ?e . :in $ ?username + :where [?e :orcpub.user/username ?username]] db username) + (binding [*out* *err*] + (println "ERROR: Username already taken:" username)) + (System/exit 1)) + ;; Create user — already verified, no email step needed + @(d/transact conn + [{:orcpub.user/email email + :orcpub.user/username username + :orcpub.user/password (hashers/encrypt password) + :orcpub.user/verified? true + :orcpub.user/send-updates? false + :orcpub.user/created (java.util.Date.)}]) + (println "OK: User created and verified —" username "<" email ">"))) + +(defn verify-user! [conn username-or-email] + (let [db (d/db conn) + user (find-user db username-or-email)] + (if-not user + (do (binding [*out* *err*] + (println "ERROR: User not found:" username-or-email)) + (System/exit 1)) + (if (:orcpub.user/verified? user) + (println "OK: User already verified —" (:orcpub.user/username user)) + (do + @(d/transact conn + [[:db/add (:db/id user) :orcpub.user/verified? true]]) + (println "OK: User verified —" (:orcpub.user/username user))))))) + +(defn check-user [db username-or-email] + (if-let [user (find-user db username-or-email)] + (do + (println "Found user:") + (println " Username:" (:orcpub.user/username user)) + (println " Email: " (:orcpub.user/email user)) + (println " Verified:" (:orcpub.user/verified? user)) + (println " Created: " (:orcpub.user/created user))) + (do + (println "User not found:" username-or-email) + (System/exit 1)))) + +(defn list-users [db] + (let [users (d/q '[:find [(pull ?e [:orcpub.user/username + :orcpub.user/email + :orcpub.user/verified?]) ...] + :where [?e :orcpub.user/username]] + db)] + (if (empty? users) + (println "No users found.") + (do + (println (format "%-20s %-30s %s" "USERNAME" "EMAIL" "VERIFIED")) + (println (apply str (repeat 65 "-"))) + (doseq [u (sort-by :orcpub.user/username users)] + (println (format "%-20s %-30s %s" + (:orcpub.user/username u) + (:orcpub.user/email u) + (:orcpub.user/verified? u)))))))) + +;; --- CLI dispatch --- + +(let [args *command-line-args* + cmd (first args)] + (case cmd + "create" (let [[_ username email password] args] + (when-not (and username email password) + (binding [*out* *err*] + (println "Usage: manage-user.clj create ")) + (System/exit 1)) + (let [conn (get-conn)] + (create-user! conn username email password))) + + "verify" (let [[_ username-or-email] args] + (when-not username-or-email + (binding [*out* *err*] + (println "Usage: manage-user.clj verify ")) + (System/exit 1)) + (let [conn (get-conn)] + (verify-user! conn username-or-email))) + + "check" (let [[_ username-or-email] args] + (when-not username-or-email + (binding [*out* *err*] + (println "Usage: manage-user.clj check ")) + (System/exit 1)) + (let [conn (get-conn) + db (d/db conn)] + (check-user db username-or-email))) + + "list" (let [conn (get-conn) + db (d/db conn)] + (list-users db)) + + (do + (println "OrcPub User Management") + (println "") + (println "Commands:") + (println " create Create and auto-verify a user") + (println " verify Verify an existing user") + (println " check Check if a user exists") + (println " list List all users") + (when-not cmd + (System/exit 1))))) From f5df45897941f68b200e1ab8b80764a863ee1bc4 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 13:57:08 +0000 Subject: [PATCH 3/8] Add Docker integration test CI workflow Runs on PRs that touch Docker/deploy files. Tests the full flow: docker-setup.sh --auto, docker compose up, then exercises all docker-user.sh commands (create, check, list, verify, duplicate rejection, nonexistent user). Also lints shell scripts with shellcheck. https://claude.ai/code/session_01KdeKniaCeDDV1UvnkTLQcb --- .github/workflows/docker-integration.yml | 151 +++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 .github/workflows/docker-integration.yml diff --git a/.github/workflows/docker-integration.yml b/.github/workflows/docker-integration.yml new file mode 100644 index 000000000..67d0475b1 --- /dev/null +++ b/.github/workflows/docker-integration.yml @@ -0,0 +1,151 @@ +name: Docker Integration Test + +on: + pull_request: + branches: [develop] + paths: + - 'docker/**' + - 'docker-compose*.yaml' + - 'docker-setup.sh' + - 'docker-user.sh' + - 'deploy/**' + - '.github/workflows/docker-integration.yml' + workflow_dispatch: + +jobs: + docker-test: + name: Docker Setup & User Management + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Lint shell scripts + run: | + sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck + shellcheck docker-setup.sh docker-user.sh + + - name: Run docker-setup.sh --auto + run: | + ./docker-setup.sh --auto + echo "--- Generated .env (secrets redacted) ---" + sed 's/=.*/=***/' .env + + - name: Validate .env password consistency + run: | + PW=$(grep '^DATOMIC_PASSWORD=' .env | cut -d= -f2) + URL_PW=$(grep '^DATOMIC_URL=' .env | sed 's/.*password=//') + if [ "$PW" != "$URL_PW" ]; then + echo "FAIL: DATOMIC_PASSWORD and DATOMIC_URL password mismatch" + exit 1 + fi + echo "OK: Passwords match" + + - name: Pull and start containers + run: | + docker compose up -d + docker compose ps + + - name: Wait for Datomic transactor + run: | + echo "Waiting for Datomic transactor to accept connections..." + for i in $(seq 1 30); do + if docker compose logs datomic 2>&1 | grep -q "System started"; then + echo "Datomic transactor is ready (after ~${i}s)" + break + fi + if [ "$i" -eq 30 ]; then + echo "FAIL: Datomic did not start within 30s" + docker compose logs datomic + exit 1 + fi + sleep 1 + done + + - name: Wait for orcpub app + run: | + echo "Waiting for orcpub app to be ready..." + for i in $(seq 1 60); do + CONTAINER=$(docker compose ps -q orcpub) + if docker exec "$CONTAINER" java -cp /orcpub.jar clojure.main -e \ + '(require (quote [datomic.api :as d])) (d/connect (System/getenv "DATOMIC_URL")) (println "ready")' \ + 2>/dev/null | grep -q "ready"; then + echo "orcpub app connected to Datomic (after ~${i}s)" + break + fi + if [ "$i" -eq 60 ]; then + echo "FAIL: orcpub could not connect to Datomic within 60s" + docker compose logs + exit 1 + fi + sleep 1 + done + + - name: Test — create user + run: | + ./docker-user.sh create testadmin admin@test.local SecurePass123 + echo "Exit code: $?" + + - name: Test — check user exists + run: | + OUTPUT=$(./docker-user.sh check testadmin) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "testadmin" + echo "$OUTPUT" | grep -q "admin@test.local" + echo "$OUTPUT" | grep -q "true" # verified + + - name: Test — list includes user + run: | + OUTPUT=$(./docker-user.sh list) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "testadmin" + + - name: Test — duplicate user fails + run: | + if ./docker-user.sh create testadmin admin@test.local SecurePass123 2>&1; then + echo "FAIL: Should have rejected duplicate user" + exit 1 + fi + echo "OK: Duplicate user correctly rejected" + + - name: Test — create second user + run: ./docker-user.sh create player2 player2@test.local AnotherPass456 + + - name: Test — list shows both users + run: | + OUTPUT=$(./docker-user.sh list) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "testadmin" + echo "$OUTPUT" | grep -q "player2" + + - name: Test — verify already-verified user is idempotent + run: | + OUTPUT=$(./docker-user.sh verify testadmin) + echo "$OUTPUT" + echo "$OUTPUT" | grep -q "already verified" + + - name: Test — check nonexistent user fails + run: | + if ./docker-user.sh check nobody@nowhere.com 2>&1; then + echo "FAIL: Should have reported user not found" + exit 1 + fi + echo "OK: Nonexistent user correctly not found" + + - name: Collect logs on failure + if: failure() + run: | + echo "=== docker compose ps ===" + docker compose ps + echo "=== datomic logs ===" + docker compose logs datomic + echo "=== orcpub logs ===" + docker compose logs orcpub + echo "=== web logs ===" + docker compose logs web + + - name: Cleanup + if: always() + run: docker compose down -v From 5398c7e8f9e7c66d40e95e2f6d8b973e05f4106b Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 13:58:55 +0000 Subject: [PATCH 4/8] Add HTTP login test to Docker integration CI Tests that a user created via docker-user.sh can actually log in through the app's POST /login endpoint and receive a JWT token. Also verifies that wrong passwords are rejected with HTTP 401. https://claude.ai/code/session_01KdeKniaCeDDV1UvnkTLQcb --- .github/workflows/docker-integration.yml | 52 ++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/.github/workflows/docker-integration.yml b/.github/workflows/docker-integration.yml index 67d0475b1..1c22ef89f 100644 --- a/.github/workflows/docker-integration.yml +++ b/.github/workflows/docker-integration.yml @@ -134,6 +134,58 @@ jobs: fi echo "OK: Nonexistent user correctly not found" + - name: Test — created user can log in via HTTP + run: | + # Wait for the HTTP server to be listening + for i in $(seq 1 30); do + if curl -sf http://localhost:8890/ >/dev/null 2>&1 || \ + curl -sf -o /dev/null -w "%{http_code}" http://localhost:8890/ 2>&1 | grep -qE "^[234]"; then + echo "HTTP server is up (after ~${i}s)" + break + fi + if [ "$i" -eq 30 ]; then + echo "WARN: HTTP server not reachable, skipping login test" + exit 0 + fi + sleep 1 + done + + # POST /login with the user we created + RESPONSE=$(curl -sf -X POST http://localhost:8890/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testadmin","password":"SecurePass123"}' \ + -w "\n%{http_code}" 2>&1) || true + + HTTP_CODE=$(echo "$RESPONSE" | tail -1) + BODY=$(echo "$RESPONSE" | sed '$d') + + echo "HTTP $HTTP_CODE" + echo "$BODY" + + if [ "$HTTP_CODE" = "200" ]; then + echo "OK: Login succeeded" + echo "$BODY" | grep -q "token" + echo "OK: Response contains JWT token" + else + echo "FAIL: Expected HTTP 200, got $HTTP_CODE" + exit 1 + fi + + - name: Test — wrong password is rejected + run: | + HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ + -X POST http://localhost:8890/login \ + -H "Content-Type: application/json" \ + -d '{"username":"testadmin","password":"WrongPassword"}' 2>&1) || true + + echo "HTTP $HTTP_CODE" + if [ "$HTTP_CODE" = "401" ]; then + echo "OK: Wrong password correctly rejected" + else + echo "FAIL: Expected HTTP 401, got $HTTP_CODE" + exit 1 + fi + - name: Collect logs on failure if: failure() run: | From cda2b8c79281aa5d96479045f20b77b9b2d2db2d Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 14:18:58 +0000 Subject: [PATCH 5/8] Fix shellcheck warnings in docker-setup.sh - Remove unused ENV_EXAMPLE variable (SC2034) - Quote expansions inside ${dir#...} parameter substitution (SC2295) https://claude.ai/code/session_01KdeKniaCeDDV1UvnkTLQcb --- docker-setup.sh | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docker-setup.sh b/docker-setup.sh index e03c527de..b854a5030 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -18,7 +18,6 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ENV_FILE="${SCRIPT_DIR}/.env" -ENV_EXAMPLE="${SCRIPT_DIR}/.env.example" # --------------------------------------------------------------------------- # Helpers @@ -196,9 +195,9 @@ header "Directories" for dir in "${SCRIPT_DIR}/data" "${SCRIPT_DIR}/logs" "${SCRIPT_DIR}/deploy/homebrew"; do if [ ! -d "$dir" ]; then mkdir -p "$dir" - info "Created directory: ${dir#${SCRIPT_DIR}/}" + info "Created directory: ${dir#"${SCRIPT_DIR}"/}" else - info "Directory exists: ${dir#${SCRIPT_DIR}/}" + info "Directory exists: ${dir#"${SCRIPT_DIR}"/}" fi done From c313f0e58c004dca2553e419bcee5adc27310e9c Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 14:32:12 +0000 Subject: [PATCH 6/8] Use Docker native healthchecks instead of JVM polling - Add healthcheck to datomic service (nc -z localhost 4334) - Add healthcheck to orcpub service (wget --spider localhost:8890) - Use depends_on condition: service_healthy for proper startup ordering - Replace JVM-per-iteration readiness loop in docker-user.sh with docker inspect health status polling (falls back to wget if no healthcheck is defined) - Replace CI wait steps with health status polling instead of spawning a new JVM every second - Fix CI HTTP tests to use nginx (port 443) since orcpub:8890 is not exposed to the host - Remove obsolete `version: '3'` from both compose files https://claude.ai/code/session_01KdeKniaCeDDV1UvnkTLQcb --- .github/workflows/docker-integration.yml | 67 +++++++++++------------- docker-compose-build.yaml | 19 +++++-- docker-compose.yaml | 19 +++++-- docker-user.sh | 45 ++++++++++++---- 4 files changed, 99 insertions(+), 51 deletions(-) diff --git a/.github/workflows/docker-integration.yml b/.github/workflows/docker-integration.yml index 1c22ef89f..786acff43 100644 --- a/.github/workflows/docker-integration.yml +++ b/.github/workflows/docker-integration.yml @@ -48,41 +48,50 @@ jobs: docker compose up -d docker compose ps - - name: Wait for Datomic transactor + - name: Wait for healthy containers run: | - echo "Waiting for Datomic transactor to accept connections..." - for i in $(seq 1 30); do - if docker compose logs datomic 2>&1 | grep -q "System started"; then - echo "Datomic transactor is ready (after ~${i}s)" + echo "Waiting for datomic healthcheck..." + for i in $(seq 1 60); do + STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$(docker compose ps -q datomic)" 2>/dev/null || echo "waiting") + if [ "$STATUS" = "healthy" ]; then + echo "Datomic is healthy (after ~$((i * 2))s)" break fi - if [ "$i" -eq 30 ]; then - echo "FAIL: Datomic did not start within 30s" + if [ "$STATUS" = "unhealthy" ]; then + echo "FAIL: Datomic reported unhealthy" + docker compose logs datomic + exit 1 + fi + if [ "$i" -eq 60 ]; then + echo "FAIL: Datomic did not become healthy within 120s" docker compose logs datomic exit 1 fi - sleep 1 + sleep 2 done - - name: Wait for orcpub app - run: | - echo "Waiting for orcpub app to be ready..." + echo "Waiting for orcpub healthcheck..." for i in $(seq 1 60); do - CONTAINER=$(docker compose ps -q orcpub) - if docker exec "$CONTAINER" java -cp /orcpub.jar clojure.main -e \ - '(require (quote [datomic.api :as d])) (d/connect (System/getenv "DATOMIC_URL")) (println "ready")' \ - 2>/dev/null | grep -q "ready"; then - echo "orcpub app connected to Datomic (after ~${i}s)" + STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$(docker compose ps -q orcpub)" 2>/dev/null || echo "waiting") + if [ "$STATUS" = "healthy" ]; then + echo "orcpub is healthy (after ~$((i * 2))s)" break fi + if [ "$STATUS" = "unhealthy" ]; then + echo "FAIL: orcpub reported unhealthy" + docker compose logs + exit 1 + fi if [ "$i" -eq 60 ]; then - echo "FAIL: orcpub could not connect to Datomic within 60s" + echo "FAIL: orcpub did not become healthy within 120s" docker compose logs exit 1 fi - sleep 1 + sleep 2 done + docker compose ps + - name: Test — create user run: | ./docker-user.sh create testadmin admin@test.local SecurePass123 @@ -136,22 +145,8 @@ jobs: - name: Test — created user can log in via HTTP run: | - # Wait for the HTTP server to be listening - for i in $(seq 1 30); do - if curl -sf http://localhost:8890/ >/dev/null 2>&1 || \ - curl -sf -o /dev/null -w "%{http_code}" http://localhost:8890/ 2>&1 | grep -qE "^[234]"; then - echo "HTTP server is up (after ~${i}s)" - break - fi - if [ "$i" -eq 30 ]; then - echo "WARN: HTTP server not reachable, skipping login test" - exit 0 - fi - sleep 1 - done - - # POST /login with the user we created - RESPONSE=$(curl -sf -X POST http://localhost:8890/login \ + # Use nginx (port 443) since orcpub:8890 is not exposed to host + RESPONSE=$(curl -sk -X POST https://localhost/login \ -H "Content-Type: application/json" \ -d '{"username":"testadmin","password":"SecurePass123"}' \ -w "\n%{http_code}" 2>&1) || true @@ -173,8 +168,8 @@ jobs: - name: Test — wrong password is rejected run: | - HTTP_CODE=$(curl -sf -o /dev/null -w "%{http_code}" \ - -X POST http://localhost:8890/login \ + HTTP_CODE=$(curl -sk -o /dev/null -w "%{http_code}" \ + -X POST https://localhost/login \ -H "Content-Type: application/json" \ -d '{"username":"testadmin","password":"WrongPassword"}' 2>&1) || true diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml index 611a34958..0ca89be1d 100644 --- a/docker-compose-build.yaml +++ b/docker-compose-build.yaml @@ -1,5 +1,4 @@ --- -version: '3' services: orcpub: build: @@ -18,7 +17,14 @@ services: DATOMIC_URL: ${DATOMIC_URL:-datomic:free://datomic:4334/orcpub?password=change-me} SIGNATURE: ${SIGNATURE:-change-me-to-something-unique} depends_on: - - datomic + datomic: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8890/"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 15s restart: always datomic: build: @@ -30,6 +36,12 @@ services: volumes: - ./data:/data - ./logs:/logs + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 4334"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 10s restart: always web: image: nginx:alpine @@ -41,5 +53,6 @@ services: - ./deploy/snakeoil.crt:/etc/nginx/snakeoil.crt - ./deploy/snakeoil.key:/etc/nginx/snakeoil.key depends_on: - - orcpub + orcpub: + condition: service_healthy restart: always diff --git a/docker-compose.yaml b/docker-compose.yaml index 23e242306..7b341864f 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -1,5 +1,4 @@ --- -version: '3' services: orcpub: image: orcpub/orcpub:latest @@ -16,7 +15,14 @@ services: DATOMIC_URL: ${DATOMIC_URL:-datomic:free://datomic:4334/orcpub?password=change-me} SIGNATURE: ${SIGNATURE:-change-me-to-something-unique} depends_on: - - datomic + datomic: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8890/"] + interval: 5s + timeout: 3s + retries: 20 + start_period: 15s restart: always datomic: image: orcpub/datomic:latest @@ -26,6 +32,12 @@ services: volumes: - ./data:/data - ./logs:/logs + healthcheck: + test: ["CMD-SHELL", "nc -z localhost 4334"] + interval: 5s + timeout: 3s + retries: 10 + start_period: 10s restart: always web: image: nginx:alpine @@ -38,5 +50,6 @@ services: - ./deploy/snakeoil.key:/etc/nginx/snakeoil.key - ./deploy/homebrew/:/usr/share/nginx/html/homebrew/ depends_on: - - orcpub + orcpub: + condition: service_healthy restart: always diff --git a/docker-user.sh b/docker-user.sh index f1b4ca68e..f357b3d67 100755 --- a/docker-user.sh +++ b/docker-user.sh @@ -95,7 +95,7 @@ find_container() { wait_for_ready() { local container="$1" - local max_wait=30 + local max_wait=120 local waited=0 # Check container is running @@ -105,14 +105,41 @@ wait_for_ready() { exit 1 fi - # Wait for the app to have connected to Datomic (the uberjar starts the - # Component system which connects on boot). We test by attempting a - # trivial Datomic query via clojure.main. - printf "Waiting for Datomic connection" + # Wait for Docker's native healthcheck (defined in docker-compose.yaml) + # to report the container as healthy. This avoids spawning a JVM per check. + local health + health=$(docker inspect --format='{{if .State.Health}}yes{{end}}' "$container" 2>/dev/null || true) + + if [ "$health" = "yes" ]; then + printf "Waiting for container health check" + while [ $waited -lt $max_wait ]; do + local status + status=$(docker inspect --format='{{.State.Health.Status}}' "$container" 2>/dev/null || true) + if [ "$status" = "healthy" ]; then + echo "" + info "Container is healthy" + return 0 + fi + if [ "$status" = "unhealthy" ]; then + echo "" + error "Container reported unhealthy" + exit 1 + fi + printf "." + sleep 2 + waited=$((waited + 2)) + done + echo "" + error "Timed out waiting for healthy status (${max_wait}s)." + exit 1 + fi + + # Fallback: no healthcheck defined — check HTTP readiness directly + warn "No Docker healthcheck found; polling HTTP on container port..." + printf "Waiting for app" while [ $waited -lt $max_wait ]; do - if docker exec "$container" java -cp /orcpub.jar clojure.main -e \ - '(require (quote [datomic.api :as d])) (d/connect (System/getenv "DATOMIC_URL")) (println "ready")' \ - 2>/dev/null | grep -q "ready"; then + if docker exec "$container" wget --no-verbose --tries=1 --spider \ + "http://localhost:${PORT:-8890}/" 2>/dev/null; then echo "" return 0 fi @@ -122,7 +149,7 @@ wait_for_ready() { done echo "" - error "Timed out waiting for Datomic (${max_wait}s). Is the datomic container running?" + error "Timed out waiting for app (${max_wait}s). Is the datomic container running?" exit 1 } From c5d3f9d1a88ca041cbd6f658c2e92e97f4b09032 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 19:03:32 +0000 Subject: [PATCH 7/8] Fix Docker CI: replace nc healthcheck with bash /dev/tcp, fix shellcheck SC2059 The datomic healthcheck used `nc -z localhost 4334` but netcat is not available in the openjdk:8u242-jre base image, causing the container to always report unhealthy. Replace with bash's built-in /dev/tcp which is guaranteed available. Also increase start_period to 30s and retries to 20 to give the JVM transactor more time on CI runners. Fix shellcheck SC2059 warnings in docker-setup.sh and docker-user.sh by moving color variables out of printf format strings into %s arguments. https://claude.ai/code/session_016YeAFxbw5tP5VPa95KGLWY --- docker-compose-build.yaml | 6 +++--- docker-compose.yaml | 6 +++--- docker-setup.sh | 8 ++++---- docker-user.sh | 6 +++--- 4 files changed, 13 insertions(+), 13 deletions(-) diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml index 0ca89be1d..7748eb73c 100644 --- a/docker-compose-build.yaml +++ b/docker-compose-build.yaml @@ -37,11 +37,11 @@ services: - ./data:/data - ./logs:/logs healthcheck: - test: ["CMD-SHELL", "nc -z localhost 4334"] + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/4334'"] interval: 5s timeout: 3s - retries: 10 - start_period: 10s + retries: 20 + start_period: 30s restart: always web: image: nginx:alpine diff --git a/docker-compose.yaml b/docker-compose.yaml index 7b341864f..6a12614f3 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,11 +33,11 @@ services: - ./data:/data - ./logs:/logs healthcheck: - test: ["CMD-SHELL", "nc -z localhost 4334"] + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/4334'"] interval: 5s timeout: 3s - retries: 10 - start_period: 10s + retries: 20 + start_period: 30s restart: always web: image: nginx:alpine diff --git a/docker-setup.sh b/docker-setup.sh index b854a5030..05c5d42a9 100755 --- a/docker-setup.sh +++ b/docker-setup.sh @@ -29,10 +29,10 @@ color_red='\033[0;31m' color_cyan='\033[0;36m' color_reset='\033[0m' -info() { printf "${color_green}[INFO]${color_reset} %s\n" "$*"; } -warn() { printf "${color_yellow}[WARN]${color_reset} %s\n" "$*"; } -error() { printf "${color_red}[ERROR]${color_reset} %s\n" "$*" >&2; } -header() { printf "\n${color_cyan}=== %s ===${color_reset}\n\n" "$*"; } +info() { printf '%s[INFO]%s %s\n' "$color_green" "$color_reset" "$*"; } +warn() { printf '%s[WARN]%s %s\n' "$color_yellow" "$color_reset" "$*"; } +error() { printf '%s[ERROR]%s %s\n' "$color_red" "$color_reset" "$*" >&2; } +header() { printf '\n%s=== %s ===%s\n\n' "$color_cyan" "$*" "$color_reset"; } generate_password() { # Generate a URL-safe random password (no special chars that break URLs/YAML) diff --git a/docker-user.sh b/docker-user.sh index f357b3d67..14575b867 100755 --- a/docker-user.sh +++ b/docker-user.sh @@ -29,9 +29,9 @@ color_red='\033[0;31m' color_yellow='\033[1;33m' color_reset='\033[0m' -info() { printf "${color_green}[OK]${color_reset} %s\n" "$*"; } -error() { printf "${color_red}[ERROR]${color_reset} %s\n" "$*" >&2; } -warn() { printf "${color_yellow}[WARN]${color_reset} %s\n" "$*"; } +info() { printf '%s[OK]%s %s\n' "$color_green" "$color_reset" "$*"; } +error() { printf '%s[ERROR]%s %s\n' "$color_red" "$color_reset" "$*" >&2; } +warn() { printf '%s[WARN]%s %s\n' "$color_yellow" "$color_reset" "$*"; } usage() { cat <<'USAGE' From a6fb857208694ed47d2178d350d2dfa8c98395cf Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 18 Feb 2026 20:11:08 +0000 Subject: [PATCH 8/8] Fix datomic healthcheck: use /proc/net/tcp instead of bash /dev/tcp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The previous healthcheck (bash -c 'echo > /dev/tcp/localhost/4334') fails on the openjdk:8u242-jre base image because CMD-SHELL runs via /bin/sh (dash) and /dev/tcp is a bash-only feature that may not be available in slim Debian images. Replace with grep on /proc/net/tcp which checks the kernel TCP listen table directly — requires only grep (always present) and works on any Linux container. Port 4334 decimal = 10EE hex, so we grep for ':10EE ' in /proc/net/tcp with a fallback to /proc/net/tcp6 for IPv6 listeners. Also restructure the CI workflow to start datomic independently first (docker compose up -d --no-deps datomic) so the depends_on chain doesn't block. This ensures we get container logs and health state on failure instead of an opaque "dependency failed to start" error. https://claude.ai/code/session_01KkQBjzJHkceYz36K79jWri --- .github/workflows/docker-integration.yml | 60 +++++++++++++++++------- docker-compose-build.yaml | 6 +-- docker-compose.yaml | 6 +-- 3 files changed, 48 insertions(+), 24 deletions(-) diff --git a/.github/workflows/docker-integration.yml b/.github/workflows/docker-integration.yml index 786acff43..86041a5e9 100644 --- a/.github/workflows/docker-integration.yml +++ b/.github/workflows/docker-integration.yml @@ -43,53 +43,77 @@ jobs: fi echo "OK: Passwords match" - - name: Pull and start containers + - name: Start datomic (no deps) run: | - docker compose up -d - docker compose ps + docker compose pull + docker compose up -d --no-deps datomic + echo "Datomic container started, waiting for health..." - - name: Wait for healthy containers + - name: Wait for datomic healthy run: | - echo "Waiting for datomic healthcheck..." - for i in $(seq 1 60); do - STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$(docker compose ps -q datomic)" 2>/dev/null || echo "waiting") + for i in $(seq 1 90); do + CID=$(docker compose ps -q datomic 2>/dev/null) || true + if [ -z "$CID" ]; then + echo " [$i/90] datomic container not found yet" + sleep 2 + continue + fi + RUNNING=$(docker inspect --format='{{.State.Running}}' "$CID" 2>/dev/null || echo "false") + STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting") + echo " [$i/90] running=$RUNNING health=$STATUS" if [ "$STATUS" = "healthy" ]; then echo "Datomic is healthy (after ~$((i * 2))s)" break fi - if [ "$STATUS" = "unhealthy" ]; then - echo "FAIL: Datomic reported unhealthy" + if [ "$RUNNING" = "false" ]; then + echo "WARN: datomic container stopped — dumping logs" docker compose logs datomic - exit 1 + echo "Container will restart (restart: always), continuing to wait..." fi - if [ "$i" -eq 60 ]; then - echo "FAIL: Datomic did not become healthy within 120s" + if [ "$i" -eq 90 ]; then + echo "FAIL: Datomic did not become healthy within 180s" + echo "=== container state ===" + docker inspect --format='{{json .State}}' "$CID" | python3 -m json.tool || true + echo "=== datomic logs ===" docker compose logs datomic exit 1 fi sleep 2 done - echo "Waiting for orcpub healthcheck..." - for i in $(seq 1 60); do - STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$(docker compose ps -q orcpub)" 2>/dev/null || echo "waiting") + - name: Start orcpub and web + run: | + docker compose up -d + docker compose ps + + - name: Wait for orcpub healthy + run: | + for i in $(seq 1 90); do + CID=$(docker compose ps -q orcpub 2>/dev/null) || true + if [ -z "$CID" ]; then + echo " [$i/90] orcpub container not found yet" + sleep 2 + continue + fi + STATUS=$(docker inspect --format='{{.State.Health.Status}}' "$CID" 2>/dev/null || echo "starting") + echo " [$i/90] orcpub health=$STATUS" if [ "$STATUS" = "healthy" ]; then echo "orcpub is healthy (after ~$((i * 2))s)" break fi if [ "$STATUS" = "unhealthy" ]; then echo "FAIL: orcpub reported unhealthy" + echo "=== all logs ===" docker compose logs exit 1 fi - if [ "$i" -eq 60 ]; then - echo "FAIL: orcpub did not become healthy within 120s" + if [ "$i" -eq 90 ]; then + echo "FAIL: orcpub did not become healthy within 180s" docker compose logs exit 1 fi sleep 2 done - docker compose ps - name: Test — create user diff --git a/docker-compose-build.yaml b/docker-compose-build.yaml index 7748eb73c..015b6daa8 100644 --- a/docker-compose-build.yaml +++ b/docker-compose-build.yaml @@ -37,11 +37,11 @@ services: - ./data:/data - ./logs:/logs healthcheck: - test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/4334'"] + test: ["CMD-SHELL", "grep -q ':10EE ' /proc/net/tcp || grep -q ':10EE ' /proc/net/tcp6"] interval: 5s timeout: 3s - retries: 20 - start_period: 30s + retries: 30 + start_period: 40s restart: always web: image: nginx:alpine diff --git a/docker-compose.yaml b/docker-compose.yaml index 6a12614f3..08e508977 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -33,11 +33,11 @@ services: - ./data:/data - ./logs:/logs healthcheck: - test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/4334'"] + test: ["CMD-SHELL", "grep -q ':10EE ' /proc/net/tcp || grep -q ':10EE ' /proc/net/tcp6"] interval: 5s timeout: 3s - retries: 20 - start_period: 30s + retries: 30 + start_period: 40s restart: always web: image: nginx:alpine