Files
2026-02-13 18:47:57 -08:00

324 lines
9.5 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd "${script_dir}/.." && pwd)"
default_qortal_repo_url="https://github.com/Qortal/qortal.git"
default_external_auth_repo_url="https://gitea.qortal.link/crowetic/Qortal-External-Auth.git"
mode="nossl"
with_external_auth="auto"
with_qortal="1"
env_file="${repo_root}/.env.devprod"
accept_defaults="0"
skip_occ="0"
usage() {
cat <<USAGE
Usage: ./scripts/install-production-docker.sh [options]
Options:
--mode ssl|nossl Compose mode (default: nossl)
--ssl Alias for --mode ssl
--nossl Alias for --mode nossl
--with-external-auth Ensure external-auth profile is enabled
--without-external-auth Disable external-auth profile
--with-qortal Include qortal_node service (default)
--without-qortal Exclude qortal_node service
--env-file <path> Env file to use (default: .env.devprod)
--accept-defaults If env file is auto-created from template, continue without stopping
--skip-occ Skip post-start Nextcloud app enable/repair commands
-h, --help Show this help
USAGE
}
while [[ $# -gt 0 ]]; do
case "$1" in
--mode)
mode="${2:-}"
shift 2
;;
--mode=*)
mode="${1#*=}"
shift
;;
--ssl)
mode="ssl"
shift
;;
--nossl)
mode="nossl"
shift
;;
--with-external-auth)
with_external_auth="1"
shift
;;
--without-external-auth)
with_external_auth="0"
shift
;;
--with-qortal)
with_qortal="1"
shift
;;
--without-qortal)
with_qortal="0"
shift
;;
--env-file)
env_file="${2:-}"
shift 2
;;
--env-file=*)
env_file="${1#*=}"
shift
;;
--accept-defaults)
accept_defaults="1"
shift
;;
--skip-occ)
skip_occ="1"
shift
;;
-h|--help)
usage
exit 0
;;
*)
echo "Unknown option: $1"
usage
exit 1
;;
esac
done
if [[ "${mode}" != "ssl" && "${mode}" != "nossl" ]]; then
echo "Invalid mode: ${mode}"
usage
exit 1
fi
read_env_value() {
local key="$1"
local default_value="${2:-}"
local value
if command -v rg >/dev/null 2>&1; then
value="$(rg -m1 "^${key}=" "${env_file}" | cut -d= -f2- || true)"
else
value="$(grep -m1 -E "^${key}=" "${env_file}" | cut -d= -f2- || true)"
fi
if [[ -z "${value}" ]]; then
echo "${default_value}"
return
fi
if [[ "${value}" =~ ^\".*\"$ ]]; then
value="${value:1:${#value}-2}"
elif [[ "${value}" =~ ^\'.*\'$ ]]; then
value="${value:1:${#value}-2}"
fi
echo "${value}"
}
resolve_context_path() {
local raw_path="$1"
if [[ "${raw_path}" == /* ]]; then
echo "${raw_path}"
else
echo "${repo_root}/${raw_path}"
fi
}
ensure_context_repo() {
local label="$1"
local target_path="$2"
local repo_url="$3"
if [[ -d "${target_path}/.git" ]]; then
echo "Using existing ${label} repo at ${target_path}"
return 0
fi
if [[ -e "${target_path}" && ! -d "${target_path}/.git" ]]; then
echo "${label} context exists but is not a git repo: ${target_path}"
echo "Set a valid path in ${env_file} or remove this path."
exit 1
fi
echo "Cloning ${label} repo into ${target_path}"
mkdir -p "$(dirname "${target_path}")"
if ! git clone --depth 1 "${repo_url}" "${target_path}"; then
echo "Failed to clone ${label} from ${repo_url}"
echo "Set the context manually in ${env_file} and rerun."
exit 1
fi
}
if ! command -v docker >/dev/null 2>&1; then
echo "docker is required"
exit 1
fi
if ! docker compose version >/dev/null 2>&1; then
echo "docker compose plugin is required"
exit 1
fi
env_template="${repo_root}/.env.devprod.example"
if [[ ! -f "${env_template}" ]]; then
echo "Missing env template: ${env_template}"
exit 1
fi
if [[ "${env_file}" != /* ]]; then
env_file="${repo_root}/${env_file}"
fi
created_env="0"
if [[ ! -f "${env_file}" ]]; then
cp "${env_template}" "${env_file}"
created_env="1"
echo "Created ${env_file} from template."
fi
if [[ "${created_env}" == "1" && "${accept_defaults}" != "1" ]]; then
cat <<MSG
Edit ${env_file} with production values, then rerun.
Use --accept-defaults only for initial smoke testing.
MSG
exit 1
fi
qortal_context_raw="$(read_env_value "QORTAL_NODE_CONTEXT" "../qortal")"
qortal_context_path="$(resolve_context_path "${qortal_context_raw}")"
qortal_repo_url="${QORTAL_GIT_URL:-${default_qortal_repo_url}}"
ensure_context_repo "Qortal Core" "${qortal_context_path}" "${qortal_repo_url}"
compose_file="${repo_root}/docker-compose.devprod.nossl.yml"
if [[ "${mode}" == "ssl" ]]; then
compose_file="${repo_root}/docker-compose.devprod.yml"
fi
mkdir -p "${repo_root}/qortal/data"
if [[ -x "${repo_root}/scripts/ensure-qortal-settings.sh" ]]; then
"${repo_root}/scripts/ensure-qortal-settings.sh"
fi
if [[ -x "${repo_root}/scripts/ensure-qortal-start-args.sh" ]]; then
"${repo_root}/scripts/ensure-qortal-start-args.sh" "${env_file}"
fi
if [[ -x "${repo_root}/scripts/select-qortal-p2p-port.sh" ]]; then
"${repo_root}/scripts/select-qortal-p2p-port.sh" "${env_file}"
fi
if [[ -f "${repo_root}/scripts/ensure-broker-internal-token.sh" ]]; then
bash "${repo_root}/scripts/ensure-broker-internal-token.sh" "${env_file}"
fi
broker_internal_api_token="$(read_env_value "BROKER_INTERNAL_API_TOKEN" "")"
if [[ -z "${broker_internal_api_token}" ]]; then
echo "BROKER_INTERNAL_API_TOKEN is missing in ${env_file}"
echo "Run: bash scripts/ensure-broker-internal-token.sh ${env_file}"
exit 1
fi
export BROKER_INTERNAL_API_TOKEN="${broker_internal_api_token}"
broker_cors_allowed_origins="$(read_env_value "BROKER_CORS_ALLOWED_ORIGINS" "")"
if [[ -n "${broker_cors_allowed_origins}" ]]; then
export BROKER_CORS_ALLOWED_ORIGINS="${broker_cors_allowed_origins}"
fi
echo "Broker auth env loaded from ${env_file}: token_set=yes cors_origins=${broker_cors_allowed_origins:-<empty>}"
current_profiles=""
if command -v rg >/dev/null 2>&1; then
profiles_line="$(rg -m1 '^COMPOSE_PROFILES=' "${env_file}" || true)"
else
profiles_line="$(grep -m1 '^COMPOSE_PROFILES=' "${env_file}" || true)"
fi
if [[ -n "${profiles_line}" ]]; then
current_profiles="${profiles_line#COMPOSE_PROFILES=}"
fi
if [[ "${with_external_auth}" == "1" ]]; then
if [[ -z "${current_profiles}" ]]; then
current_profiles="external-auth"
elif [[ ",${current_profiles}," != *",external-auth,"* ]]; then
current_profiles="${current_profiles},external-auth"
fi
elif [[ "${with_external_auth}" == "0" ]]; then
filtered=()
IFS=',' read -r -a profile_parts <<< "${current_profiles}"
for p in "${profile_parts[@]}"; do
p_trimmed="$(echo "${p}" | tr -d '[:space:]')"
[[ -z "${p_trimmed}" ]] && continue
[[ "${p_trimmed}" == "external-auth" ]] && continue
filtered+=("${p_trimmed}")
done
current_profiles="$(IFS=','; echo "${filtered[*]}")"
fi
if [[ -n "${current_profiles}" ]]; then
export COMPOSE_PROFILES="${current_profiles}"
else
unset COMPOSE_PROFILES || true
fi
if [[ ",${COMPOSE_PROFILES:-}," == *",external-auth," ]]; then
external_auth_context_raw="$(read_env_value "EXTERNAL_AUTH_CONTEXT" "../Qortal-External-Auth")"
external_auth_context_path="$(resolve_context_path "${external_auth_context_raw}")"
external_auth_repo_url="${EXTERNAL_AUTH_GIT_URL:-${default_external_auth_repo_url}}"
ensure_context_repo "Qortal External Auth" "${external_auth_context_path}" "${external_auth_repo_url}"
fi
if [[ "${with_qortal}" == "0" && ",${COMPOSE_PROFILES:-}," == *",external-auth,"* ]]; then
echo "Cannot run --without-qortal while external-auth profile is enabled."
exit 1
fi
echo "Using compose file: ${compose_file}"
echo "Using env file: ${env_file}"
echo "Using profiles: ${COMPOSE_PROFILES:-<none>}"
if [[ "${with_qortal}" == "1" ]]; then
docker compose -f "${compose_file}" --env-file "${env_file}" up -d --build
else
mapfile -t services < <(docker compose -f "${compose_file}" --env-file "${env_file}" config --services)
target_services=()
for svc in "${services[@]}"; do
if [[ "${svc}" == "qortal_node" ]]; then
continue
fi
target_services+=("${svc}")
done
if [[ "${#target_services[@]}" -eq 0 ]]; then
echo "No services selected to start."
exit 1
fi
docker compose -f "${compose_file}" --env-file "${env_file}" up -d --build "${target_services[@]}"
fi
if [[ "${skip_occ}" != "1" ]]; then
set +e
docker compose -f "${compose_file}" --env-file "${env_file}" exec -T app php occ app:enable qortal_integration >/dev/null 2>&1
docker compose -f "${compose_file}" --env-file "${env_file}" exec -T app php occ maintenance:repair >/dev/null 2>&1
occ_status=$?
set -e
if [[ "${occ_status}" -ne 0 ]]; then
echo "Warning: Nextcloud OCC post-setup commands did not complete (app may still be initializing)."
fi
fi
cat <<DONE
Installation bootstrap complete.
Next steps:
1. Verify running services:
docker compose -f ${compose_file} --env-file ${env_file} ps
2. If using external-auth profile and node API key is required, verify env contains:
QORTAL_AUTH_NODE_API_KEY=...
QORTAL_AUTH_NODE_API_KEY_MODE=paths
QORTAL_AUTH_NODE_API_KEY_PATHS=/
3. Broker internal API security:
BROKER_INTERNAL_API_TOKEN is generated/required in ${env_file}
and must be set in Nextcloud admin as "Broker Internal API Token"
if your Nextcloud app container does not receive QORTAL_BROKER_INTERNAL_API_TOKEN env.
4. Open Nextcloud Admin > Qortal Integration and run "Test Broker Connection".
DONE