* feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * feat(self-host): safe versioned releases + pull-based image pipeline Builds on the pull-based images compose (docker-compose.images.yaml) to make pulling pre-built images safe for self-hosters: - docker-publish: stable-releases + edge tag policy. Git tag vX.Y.Z publishes X.Y.Z/X.Y/X/latest; a push to main publishes only :edge (latest never moves on a routine merge). - Pre-publish smoke-test gate: each image is booted against ephemeral postgres+redis and healthchecked before any push (docker/smoke-test.sh). - CIO_VERSION pinning in .env.example; run-docker-full-stack.sh --images flag to pull instead of build. - Docs: SELF_HOST pull-from-images + versioning/upgrade/rollback sections, PUBLISHING_IMAGES tag policy + pnpm release flow, self-hosting.mdx quickstart. * feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * ci: run build-only on PRs, defer lint until repo-wide format * docs: point Railway template link at the new template URL * fix: address Greptile review + dashboard CI build env - docker-push.sh: only also-tag :latest for real semver releases, so VERSION=edge/main/dev can't clobber the stable latest pointer (P1). - run-docker-full-stack.sh: stop deriving OBJECT_STORAGE_PUBLIC_ENDPOINT from the dashboard origin (broke presigned URLs for videos/documents buckets); derive only the public media base URL and document the S3-API route. - ci.yml: provide PUBLIC_IS_SELFHOSTED at build time (dashboard reads it from SvelteKit's strict $env/static/public); bump cache actions v3 -> v4. - upload-openapi-spec.yml: bump cache actions v3 -> v4. - smoke-test.sh: dashboard gate now fails on 5xx instead of accepting any response. * ci: scope PR build gate to the api + dashboard closures Building every package surfaced pre-existing rot in apps that aren't part of this PR (storybook's missing @lucide/svelte import, course-app prettier debt). The gate's purpose is to verify the two Docker-image apps; the rest are built on PRs by their own Cloudflare Workers builds. Filter to @cio/api... and @cio/dashboard... so the gate covers exactly what it should. * fix(docker-push): make latest_arg always return 0 Use an `if` instead of `is_release_version && printf` so the helper returns 0 even when it emits no tag. A non-zero return inside the $(latest_arg ...) build argument is fragile under set -e (and would abort with inherit_errexit), which would break the edge/main builds the policy explicitly supports. * fix(docker): copy patches/ into jobs image so pnpm install resolves patchedDependencies The jobs Dockerfile ran `pnpm install` without copying patches/, so pnpm failed hashing package.json#pnpm.patchedDependencies (layerchart patch), breaking the self-host full-stack build. api/dashboard already copy patches/. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(docker): build jobs worker deps in topological order The jobs Dockerfile built workspace packages with a hand-written list that omitted transitive deps (e.g. @cio/question-types, consumed by @cio/utils), so `tsc` failed with TS2307 during the self-host build. Use the recursive `pnpm -r --filter @cio/jobs-worker... build` like the api/dashboard images, which builds the full dependency closure in the correct order. * refactor(docker): move jobs Dockerfile to docker/Dockerfile.jobs and align dep install Move apps/jobs/Dockerfile -> docker/Dockerfile.jobs so all three image recipes live together in docker/, and update every reference (docker-compose.yaml, docker-push.sh, docker-publish.yml, railway.jobs.json, self-hosting.mdx). Also align its dependency install with the api/dashboard images: replace the hand-maintained list of workspace package.json copies (which silently missed transitive deps like @cio/core / @cio/ai-assistant, leaving their node_modules uninstalled and breaking the build with TS2307) with the proven pnpm-fetch pattern: fetch the full lockfile into a cached store, COPY . ., then an offline filtered install of @cio/jobs-worker... This installs the complete dependency closure regardless of how the workspace graph grows. * refactor(docker): move docker-compose.yaml to repo root for Coolify compatibility Coolify's Docker Compose build pack forces Base Directory to the repo root and resolves build contexts from there, so the compose's `context: ..` (needed when the file lived in docker/) climbed above the repo and failed with `lstat /artifacts/docker: no such file or directory`. Move docker/docker-compose.yaml -> ./docker-compose.yaml and change the build contexts from `..` to `.` so the context is the repo root with no upward path. This works identically for run-docker-full-stack.sh, manual `docker compose up`, and Coolify. Dockerfiles stay referenced as docker/Dockerfile.*. Update the references in run-docker-full-stack.sh and the docs accordingly. The images compose (docker-compose.images.yaml) is unaffected (no build context) and stays in docker/. * Add self-hosting documentation for Coolify and Railway - Created new documentation for deploying ClassroomIO on Coolify using a pull-based Docker Compose stack. - Added comprehensive guide for self-hosting ClassroomIO with Docker, covering both pre-built images and building from source. - Introduced a new meta.json file to organize self-hosting documentation pages. - Developed a Railway deployment guide with environment variable configurations for API, dashboard, and jobs worker. - Updated self-hosting blog post to link directly to the new Railway guide. - Removed obsolete Railway configuration files (railway.api.json, railway.dashboard.json, railway.jobs.json). - Enhanced run-docker-full-stack.sh script to improve MinIO credential generation and service readiness checks. - Updated docker-compose files to enforce strong secret requirements for authentication. - Adjusted self-hosting references in the Docker documentation to include new guides. * fix(docker): point references at root-level docker-compose.images.yaml The pull-based compose file moved to the repo root (alongside docker-compose.yaml). Update IMAGES_COMPOSE_FILE in run-docker-full-stack.sh, the file's own header example, and the docs (docker.mdx curl URL, coolify.mdx path + blob link, SELF_HOST.md relative link + curl URL). * fix(self-host): address review round 2 — drop unused AUTH_BEARER_TOKEN, pull-by-default, doc fixes - Remove AUTH_BEARER_TOKEN everywhere (unused in app code; PRIVATE_SERVER_KEY is the real dashboard<->api key). Simplifies run-docker-full-stack.sh secret generation. - run-docker-full-stack.sh now defaults to PULLING pre-built images; add --build for source. - docs: recommend MinIO, 'a way' (not 'recommended way') to self-host, remove Direct API Access section, trim Common Commands, SMTP vars per-line, fix PRIVATE_SERVER_KEY wording. * feat(db): versioned migrations for self-host with auto-baseline - Regenerate a single clean baseline migration; db:setup now runs drizzle-kit migrate (not push). - Add db:generate / db:migrate / db:baseline scripts. - baselineMigrationsIfNeeded(): adopt an existing (push/supabase) schema into drizzle's journal so migrate won't try to recreate existing objects ("type/table already exists"). No-ops on fresh and already-tracked databases; runs automatically in db:setup before migrate. * ci(openapi): drop stale ci.yml reference in turbo-cache comment ci.yml was removed earlier in this review; the openapi workflow's turbo cache still warms from its own prior runs via restore-keys, so this is a comment-only fix. * fix(db): make baseline squash-safe for migrate-tracked databases baselineMigrationsIfNeeded returned early whenever drizzle.__drizzle_migrations had any rows. After a migration squash, an instance tracked under the old journal (pre-squash hashes) would then let `drizzle-kit migrate` re-run the consolidated baseline — whose journal `when` is newer than every old created_at — and crash on `CREATE TYPE/TABLE "..." already exists` at startup. Now, when history exists but the consolidated baseline hash is absent, insert that hash (marking it already applied) so migrate skips it. Only the baseline entry (idx 0) is reconciled, so genuinely pending future migrations still run. Verified end-to-end against Postgres: reproduces the original crash, confirms the fix no-ops migrate on a pre-squash DB, and confirms a later 0001 migration still applies. Fresh-DB and already-adopted paths unchanged. * fix(docker): add explicit id to pnpm-store cache mounts Railway's Metal builder rejects BuildKit cache mounts that omit an explicit id (it does not default id to target like standard Docker/ BuildKit do), failing the build at parse time with: dockerfile invalid: flag '--mount=type=cache,target=/pnpm/store' is missing an id argument Add id=pnpm-store to all six cache mounts across the api, dashboard, and jobs images. Harmless on Docker Desktop / CI / GitHub Actions, and required for the Railway self-host build path. * fix(docker): drop BuildKit cache mounts for Railway compatibility Railway's builder rejects cache-mount ids not prefixed with its per-service cacheKey, which can't be hardcoded in a Dockerfile shared across CI, local Docker, and three Railway services. Remove the --mount=type=cache flag (keep --store-dir /pnpm/store); the store is baked into the fetch layer and reused by the offline install, so behaviour is unchanged. Supersedes 9c4633f2c. * fix(docker): force LF for shell scripts and Dockerfiles via .gitattributes cio-api crash-looped on Railway with 'exec /app/entrypoint.sh: No such file or directory' because the entrypoint shipped with CRLF endings (Windows checkout, core.autocrlf=true) — the '#!/bin/sh\r' shebang is unresolvable in the container. Add .gitattributes pinning *.sh and Dockerfile* to LF so container-executed files are never CRLF regardless of contributor OS. * fix(db): serialize db-setup with an advisory lock to prevent baseline double-insert Concurrent db:setup runs (e.g. multiple API replicas booting together) could both pass the baseline 'already present?' check and insert duplicate rows into drizzle.__drizzle_migrations, which has no unique constraint on hash. Wrap baseline + migrate in a pg_advisory_lock held on a dedicated connection so concurrent startups serialize: the loser blocks, then sees the baseline applied and no-ops. * docs(railway): update template deploy URL to classroomio-1 --------- Co-authored-by: Rotimi Best <[email protected]>
327 lines
11 KiB
Bash
Executable File
327 lines
11 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
|
BUILD_COMPOSE_FILE="${ROOT_DIR}/docker-compose.yaml"
|
|
IMAGES_COMPOSE_FILE="${ROOT_DIR}/docker-compose.images.yaml"
|
|
PROJECT_NAME="classroomio"
|
|
ENV_FILE="${ROOT_DIR}/.env"
|
|
# Default to PULLING pre-built images (fast). Pass --build to build from source instead.
|
|
USE_IMAGES=true
|
|
BUILD_IMAGES=false
|
|
COMPOSE_FILE="${IMAGES_COMPOSE_FILE}"
|
|
COMPOSE_PROFILES="--profile minio"
|
|
|
|
print_usage() {
|
|
cat <<'USAGE'
|
|
Usage: ./run-docker-full-stack.sh [--build] [--no-build] [--no-minio]
|
|
|
|
By default this PULLS the pre-built classroomio/* images from Docker Hub at the tag in
|
|
CIO_VERSION (.env) — no local build, no full repo needed.
|
|
|
|
Options:
|
|
--build Build the images from source (docker-compose.yaml) instead of pulling.
|
|
--no-build With --build, start containers without rebuilding.
|
|
--no-minio Exclude MinIO (object storage). By default MinIO is included.
|
|
--images Deprecated alias — pulling images is now the default.
|
|
-h, --help Show this help message.
|
|
USAGE
|
|
}
|
|
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--build)
|
|
USE_IMAGES=false
|
|
BUILD_IMAGES=true
|
|
COMPOSE_FILE="${BUILD_COMPOSE_FILE}"
|
|
shift
|
|
;;
|
|
--no-build)
|
|
BUILD_IMAGES=false
|
|
shift
|
|
;;
|
|
--no-minio)
|
|
COMPOSE_PROFILES=""
|
|
shift
|
|
;;
|
|
--images)
|
|
# Pulling is the default now; kept for backward compatibility.
|
|
USE_IMAGES=true
|
|
BUILD_IMAGES=false
|
|
COMPOSE_FILE="${IMAGES_COMPOSE_FILE}"
|
|
shift
|
|
;;
|
|
-h|--help)
|
|
print_usage
|
|
exit 0
|
|
;;
|
|
*)
|
|
echo "Unknown option: $1"
|
|
print_usage
|
|
exit 1
|
|
;;
|
|
esac
|
|
done
|
|
|
|
get_env_value() {
|
|
local key="$1"
|
|
local line
|
|
line="$(grep -E "^${key}=" "${ENV_FILE}" | tail -n 1 || true)"
|
|
printf '%s' "${line#*=}"
|
|
}
|
|
|
|
upsert_env_value() {
|
|
local key="$1"
|
|
local value="$2"
|
|
local tmp_file
|
|
|
|
tmp_file="$(mktemp)"
|
|
awk -v key="${key}" -v value="${value}" '
|
|
BEGIN { updated = 0 }
|
|
$0 ~ ("^" key "=") {
|
|
if (updated == 0) {
|
|
print key "=" value;
|
|
updated = 1;
|
|
}
|
|
next;
|
|
}
|
|
{ print }
|
|
END {
|
|
if (updated == 0) {
|
|
print key "=" value;
|
|
}
|
|
}
|
|
' "${ENV_FILE}" > "${tmp_file}"
|
|
|
|
mv "${tmp_file}" "${ENV_FILE}"
|
|
}
|
|
|
|
is_insecure_token_value() {
|
|
local value="$1"
|
|
local normalized
|
|
|
|
normalized="$(printf '%s' "${value}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
|
|
|
if [[ "${normalized}" =~ ^\"(.*)\"$ ]]; then
|
|
normalized="${BASH_REMATCH[1]}"
|
|
elif [[ "${normalized}" =~ ^\'(.*)\'$ ]]; then
|
|
normalized="${BASH_REMATCH[1]}"
|
|
fi
|
|
|
|
case "${normalized}" in
|
|
"" | "replace-with-a-long-random-token" | "local-dev-api-key" | "changeme" | "replace-me")
|
|
return 0
|
|
;;
|
|
replace-with-* | *local-dev* | *change-this* | *your-*)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
generate_secure_token() {
|
|
if command -v openssl >/dev/null 2>&1; then
|
|
openssl rand -hex 32
|
|
return
|
|
fi
|
|
|
|
if command -v node >/dev/null 2>&1; then
|
|
node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"
|
|
return
|
|
fi
|
|
|
|
if command -v python3 >/dev/null 2>&1; then
|
|
python3 -c "import secrets; print(secrets.token_hex(32))"
|
|
return
|
|
fi
|
|
|
|
date +%s%N | shasum | awk '{ print $1 }'
|
|
}
|
|
|
|
ensure_secure_auth_tokens() {
|
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
touch "${ENV_FILE}"
|
|
fi
|
|
|
|
local private_server_key
|
|
private_server_key="$(get_env_value PRIVATE_SERVER_KEY)"
|
|
|
|
# PRIVATE_SERVER_KEY authenticates the dashboard's server-side calls to the API and must be
|
|
# identical in both services. Generate a strong value when it's empty or a known placeholder.
|
|
if is_insecure_token_value "${private_server_key}"; then
|
|
upsert_env_value PRIVATE_SERVER_KEY "$(generate_secure_token)"
|
|
echo "Generated secure PRIVATE_SERVER_KEY in .env"
|
|
fi
|
|
}
|
|
|
|
ensure_secure_betterauth_secret() {
|
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
touch "${ENV_FILE}"
|
|
fi
|
|
|
|
local secret
|
|
secret="$(get_env_value BETTER_AUTH_SECRET)"
|
|
|
|
# Only (re)generate when the value is empty or a known placeholder; never clobber
|
|
# a strong secret the user has set themselves.
|
|
if is_insecure_token_value "${secret}"; then
|
|
upsert_env_value BETTER_AUTH_SECRET "$(generate_secure_token)"
|
|
echo "Generated secure BETTER_AUTH_SECRET in .env"
|
|
fi
|
|
}
|
|
|
|
is_local_origin() {
|
|
local value="$1"
|
|
case "${value}" in
|
|
"" | *localhost* | *127.0.0.1* | *0.0.0.0*)
|
|
return 0
|
|
;;
|
|
*)
|
|
return 1
|
|
;;
|
|
esac
|
|
}
|
|
|
|
# Point browser-facing storage URLs at the public domain when DASHBOARD_ORIGIN is not
|
|
# localhost. MinIO media is served on :9000 — operators must reverse-proxy /media to it.
|
|
# We only override values that are still empty/localhost so explicit S3/R2 config is kept.
|
|
derive_public_storage_urls() {
|
|
local dashboard_origin public_endpoint media_base
|
|
dashboard_origin="$(get_env_value DASHBOARD_ORIGIN)"
|
|
public_endpoint="$(get_env_value OBJECT_STORAGE_PUBLIC_ENDPOINT)"
|
|
media_base="$(get_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL)"
|
|
|
|
if ! is_local_origin "${dashboard_origin}"; then
|
|
local origin="${dashboard_origin%/}"
|
|
# Public media is served from the public-download `media` bucket, so deriving its base
|
|
# URL from the domain is what fixes broken media links in served pages. We deliberately
|
|
# do NOT touch OBJECT_STORAGE_PUBLIC_ENDPOINT (the S3 API endpoint) — pointing it at the
|
|
# dashboard origin would break presigned URLs for the non-public videos/documents buckets.
|
|
if is_local_origin "${media_base}"; then
|
|
upsert_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL "${origin}/media"
|
|
fi
|
|
echo "Derived OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL from DASHBOARD_ORIGIN (${origin}/media)."
|
|
echo " -> Ensure your reverse proxy routes ${origin}/media to MinIO (port 9000)."
|
|
echo " -> For presigned access to the videos/documents buckets behind a domain, expose"
|
|
echo " MinIO's S3 API on its own route and set OBJECT_STORAGE_PUBLIC_ENDPOINT explicitly."
|
|
else
|
|
# Local demo: ensure localhost defaults are present.
|
|
if [[ -z "${public_endpoint}" ]]; then
|
|
upsert_env_value OBJECT_STORAGE_PUBLIC_ENDPOINT "http://localhost:9000"
|
|
fi
|
|
if [[ -z "${media_base}" ]]; then
|
|
upsert_env_value OBJECT_STORAGE_MEDIA_PUBLIC_BASE_URL "http://localhost:9000/media"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
ensure_minio_env() {
|
|
if [[ ! -f "${ENV_FILE}" ]]; then
|
|
touch "${ENV_FILE}"
|
|
fi
|
|
|
|
local minio_password
|
|
minio_password="$(get_env_value MINIO_ROOT_PASSWORD)"
|
|
|
|
# First provision: randomize credentials whenever the MinIO password is still empty or the
|
|
# well-known `minioadmin` placeholder shipped in .env.example. (Previously this gated on
|
|
# OBJECT_STORAGE_ENDPOINT being empty, but .env.example ships it non-empty — so on a normal
|
|
# copy-the-example first run the randomization was always skipped and minioadmin/minioadmin
|
|
# survived.) MinIO uses its root credentials as the S3 access key / secret.
|
|
if [[ -z "${minio_password}" || "${minio_password}" =~ ^[[:space:]]*$ || "${minio_password}" == "minioadmin" ]]; then
|
|
local minio_user
|
|
minio_user="cio-$(generate_secure_token | cut -c1-16)"
|
|
minio_password="$(generate_secure_token)"
|
|
|
|
upsert_env_value MINIO_ROOT_USER "${minio_user}"
|
|
upsert_env_value MINIO_ROOT_PASSWORD "${minio_password}"
|
|
upsert_env_value OBJECT_STORAGE_ENDPOINT "http://minio:9000"
|
|
upsert_env_value OBJECT_STORAGE_ACCESS_KEY_ID "${minio_user}"
|
|
upsert_env_value OBJECT_STORAGE_SECRET_ACCESS_KEY "${minio_password}"
|
|
upsert_env_value OBJECT_STORAGE_FORCE_PATH_STYLE "true"
|
|
echo "Provisioned MinIO with randomized credentials in .env"
|
|
fi
|
|
|
|
derive_public_storage_urls
|
|
}
|
|
|
|
if ! command -v docker >/dev/null 2>&1; then
|
|
echo "Error: docker is not installed or not on PATH."
|
|
exit 1
|
|
fi
|
|
|
|
ensure_secure_auth_tokens
|
|
ensure_secure_betterauth_secret
|
|
|
|
if [[ "${COMPOSE_PROFILES}" == *"minio"* ]]; then
|
|
ensure_minio_env
|
|
else
|
|
# MinIO excluded: the user must supply external object storage, or uploads/media
|
|
# silently break. Fail fast instead. The endpoint must be set AND not still point at the
|
|
# bundled MinIO host (which won't be running with --no-minio).
|
|
external_endpoint="$(get_env_value OBJECT_STORAGE_ENDPOINT)"
|
|
if [[ -z "${external_endpoint}" || "${external_endpoint}" == *"minio:9000"* ]]; then
|
|
echo "Error: --no-minio was passed but no external object storage is configured."
|
|
echo "OBJECT_STORAGE_ENDPOINT is unset or still points at the bundled MinIO (minio:9000)."
|
|
echo "Configure an external S3-compatible store via OBJECT_STORAGE_* in .env,"
|
|
echo "or drop --no-minio to use the bundled MinIO."
|
|
exit 1
|
|
fi
|
|
derive_public_storage_urls
|
|
fi
|
|
|
|
echo "Starting ClassroomIO Docker full stack..."
|
|
if [[ "${COMPOSE_PROFILES}" != "" ]]; then
|
|
echo "Including MinIO (object storage, default)..."
|
|
fi
|
|
if [[ "${USE_IMAGES}" == "true" ]]; then
|
|
echo "Using pre-built images from Docker Hub (CIO_VERSION=$(get_env_value CIO_VERSION))..."
|
|
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} pull
|
|
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} up -d
|
|
elif [[ "${BUILD_IMAGES}" == "true" ]]; then
|
|
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} up --build -d
|
|
else
|
|
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ${COMPOSE_PROFILES} up -d
|
|
fi
|
|
|
|
echo
|
|
echo "Current service status:"
|
|
docker compose --env-file "${ENV_FILE}" -p "${PROJECT_NAME}" -f "${COMPOSE_FILE}" ps
|
|
|
|
# `docker compose up -d` returns once containers are *started*, not *healthy*. On a fresh
|
|
# deployment the API runs DB migrations before it listens, so the endpoints aren't reachable
|
|
# immediately. Poll with a retry instead of a single curl — and never let a not-yet-ready
|
|
# check abort the script (set -euo pipefail is active), so first runs end cleanly.
|
|
check_endpoint() {
|
|
local name="$1" url="$2" curl_flags="$3" attempts="$4"
|
|
local i=0
|
|
while ((i < attempts)); do
|
|
if curl ${curl_flags} --max-time 5 "${url}" >/dev/null 2>&1; then
|
|
echo "${name} is reachable on ${url}"
|
|
return 0
|
|
fi
|
|
i=$((i + 1))
|
|
sleep 5
|
|
done
|
|
echo "Note: ${name} is not reachable yet at ${url} (waited $((attempts * 5))s)."
|
|
echo " It may still be starting — the first run migrates the database. Follow logs with:"
|
|
echo " docker compose --env-file \"${ENV_FILE}\" -p \"${PROJECT_NAME}\" -f \"${COMPOSE_FILE}\" logs -f"
|
|
return 0
|
|
}
|
|
|
|
if command -v curl >/dev/null 2>&1; then
|
|
echo
|
|
echo "Waiting for services to come up (first run can take a minute while the API migrates)..."
|
|
check_endpoint "API" "http://localhost:3081/" "-fsS" 24
|
|
check_endpoint "Dashboard" "http://localhost:3082/" "-fsSI" 12
|
|
else
|
|
echo
|
|
echo "curl not found, skipped endpoint checks."
|
|
fi
|
|
|
|
echo
|
|
echo "Done. Full stack is running."
|