Skip to main content

Deployment Verification

After a fresh deploy or an update, run these commands on the server to confirm every service is healthy and all seed data was provisioned correctly.

Prerequisites

All commands assume you are logged in to the server via SSH and working from the deployment directory:

cd /opt/ctms-deployment

The base Docker Compose alias used throughout:

DC="docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.production"

1. Service Health Checks

Verify all 10 platform services respond with the expected HTTP status code.

echo "=== Service Health ==="
for EP in \
"Zynexa|http://127.0.0.1:3000" \
"API-Gateway|http://127.0.0.1:9080/__health" \
"Sublink|http://127.0.0.1:3001" \
"Cube|http://127.0.0.1:4000/readyz" \
"MCP|http://127.0.0.1:8006/health" \
"ODM|http://127.0.0.1:8001/health" \
"OpenObserve|http://127.0.0.1:5080/healthz" \
"Grafana|http://127.0.0.1:3100/api/health" \
"Elementary|http://127.0.0.1:3200/health" \
"Frappe|http://127.0.0.1:8080"
do
NAME=$(echo "$EP" | cut -d"|" -f1)
URL=$(echo "$EP" | cut -d"|" -f2)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$URL" 2>/dev/null)
if [[ "$STATUS" =~ ^(200|301|302|307)$ ]]; then
echo " ✅ $NAME: $STATUS"
else
echo " ❌ $NAME: $STATUS"
fi
done

Expected Results

ServiceEndpointExpected
Zynexa:3000307 (redirect to login)
API-Gateway:9080/__health200
Sublink:3001200
Cube:4000/readyz200
MCP:8006/health200
ODM:8001/health200
OpenObserve:5080/healthz200
Grafana:3100/api/health200
Elementary:3200/health200
Frappe:8080200

2. Frappe API Token

Extract the Frappe API token from the environment file. This token is used for all subsequent Frappe API calls.

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "Token: ${TOKEN}"
caution

If the token is empty, see the Frappe Token Refresh recipe to regenerate it.


3. Frappe Seed Data Verification

Verify that all master/reference data was seeded correctly across the 5 ctms-init stages.

Item Groups

Two Item Groups — Laboratory and Drug — must exist before any Items can be created (added in bundle v2.30).

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "--- Item Groups ---"
curl -s -H "Authorization: token $TOKEN" \
"http://127.0.0.1:8080/api/resource/Item%20Group?limit_page_length=0" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for item in data['data']:
print(f' {item[\"name\"]}')"

Expected: At minimum All Item Groups, Laboratory, Drug.

Sites

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "--- Sites ---"
curl -s -H "Authorization: token $TOKEN" \
"http://127.0.0.1:8080/api/resource/Site?limit_page_length=0" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f' Count: {len(data[\"data\"])}')"

Expected: 2 sites.

Study Events

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "--- Study Events ---"
curl -s -H "Authorization: token $TOKEN" \
"http://127.0.0.1:8080/api/resource/Study%20Event?limit_page_length=0" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f' Count: {len(data[\"data\"])}')"

Expected: 17 study events.

Items (Laboratory)

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "--- Items ---"
curl -s -H "Authorization: token $TOKEN" \
"http://127.0.0.1:8080/api/resource/Item?limit_page_length=0" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f' Count: {len(data[\"data\"])}')"

Expected: 4 items.

Drug Master

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "--- Drug Master ---"
curl -s -H "Authorization: token $TOKEN" \
"http://127.0.0.1:8080/api/resource/zynomi_drug_master?limit_page_length=0" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
print(f' Count: {len(data[\"data\"])}')"

Expected: 52 drugs.

Healthcare Practitioner

TOKEN=$(grep "^FRAPPE_API_TOKEN=" /opt/ctms-deployment/.env.production | cut -d= -f2)
echo "--- Healthcare Practitioner ---"
curl -s -H "Authorization: token $TOKEN" \
"http://127.0.0.1:8080/api/resource/Healthcare%20Practitioner?limit_page_length=0" \
| python3 -c "
import json, sys
data = json.load(sys.stdin)
for p in data['data']:
print(f' {p[\"name\"]}')"

Expected: At least HLC-PRAC-2026-00001.


4. Deploy Log Analysis

If you ran the deploy via zynctl.sh, check the log file for critical milestones.

Step 6b — Setup Container Wait

grep -A2 "Waiting for Frappe setup" /tmp/deploy-*.log

Expected: ✅ Frappe setup container completed successfully.

Step 7 — Token Extraction

grep "FRAPPE_API_TOKEN" /tmp/deploy-*.log | head -3

Expected: A line showing the token was found and written to .env.production.

Step 8 — ctms-init Stages

grep -E "Stage [1-5]|✅|❌|PASS|FAIL" /tmp/deploy-*.log

Expected: All 5 stages should show or PASS.


5. Docker Container Status

Quick overview of all running containers and their health.

echo "=== Running Containers ==="
docker ps --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | sort

echo ""
echo "=== Container Counts ==="
echo "Running: $(docker ps -q | wc -l) Stopped: $(docker ps -aq --filter status=exited | wc -l) Total: $(docker ps -aq | wc -l)"

6. Vendor Stack Health

Supabase

echo "--- Supabase ---"
curl -s -o /dev/null -w " Auth: %{http_code}\n" --max-time 3 "http://127.0.0.1:9999/health"
curl -s -o /dev/null -w " REST: %{http_code}\n" --max-time 3 "http://127.0.0.1:3456/"
curl -s -o /dev/null -w " Studio: %{http_code}\n" --max-time 3 "http://127.0.0.1:8443/"

Frappe Direct

echo "--- Frappe Backend ---"
docker exec frappe-marley-health-backend-1 bench --site frontend list-apps

7. Full Verification Script

Copy-paste this single script to run all checks at once:

#!/usr/bin/env bash
# ──────────────────────────────────────────────────────────
# CTMS Deployment Verification Script
# Run on the server after deploy or update
# ──────────────────────────────────────────────────────────
set -euo pipefail

cd /opt/ctms-deployment

TOKEN=$(grep "^FRAPPE_API_TOKEN=" .env.production | cut -d= -f2)
FRAPPE="http://127.0.0.1:8080"

echo "╔══════════════════════════════════════════════════════╗"
echo "║ CTMS Deployment Verification ║"
echo "╚══════════════════════════════════════════════════════╝"
echo ""

# ── 1. Service Health ───────────────────────────────────────
echo "━━━ 1. Service Health ━━━"
PASS=0; FAIL=0
for EP in \
"Zynexa|http://127.0.0.1:3000" \
"API-Gateway|http://127.0.0.1:9080/__health" \
"Sublink|http://127.0.0.1:3001" \
"Cube|http://127.0.0.1:4000/readyz" \
"MCP|http://127.0.0.1:8006/health" \
"ODM|http://127.0.0.1:8001/health" \
"OpenObserve|http://127.0.0.1:5080/healthz" \
"Grafana|http://127.0.0.1:3100/api/health" \
"Elementary|http://127.0.0.1:3200/health" \
"Frappe|http://127.0.0.1:8080"
do
NAME=$(echo "$EP" | cut -d"|" -f1)
URL=$(echo "$EP" | cut -d"|" -f2)
STATUS=$(curl -s -o /dev/null -w "%{http_code}" --max-time 5 "$URL" 2>/dev/null)
if [[ "$STATUS" =~ ^(200|301|302|307)$ ]]; then
echo " ✅ $NAME ($STATUS)"
((PASS++))
else
echo " ❌ $NAME ($STATUS)"
((FAIL++))
fi
done
echo " ── $PASS passed, $FAIL failed"
echo ""

# ── 2. Frappe Token ─────────────────────────────────────────
echo "━━━ 2. Frappe API Token ━━━"
if [ -z "$TOKEN" ]; then
echo " ❌ Token not found in .env.production"
else
echo " ✅ Token: ${TOKEN:0:20}…"
fi
echo ""

# ── 3. Frappe Seed Data ─────────────────────────────────────
echo "━━━ 3. Frappe Seed Data ━━━"

check_doctype() {
local label=$1 doctype=$2 expected=$3
local count
count=$(curl -s -H "Authorization: token $TOKEN" \
"$FRAPPE/api/resource/$doctype?limit_page_length=0" \
| python3 -c "import json,sys; print(len(json.load(sys.stdin)['data']))" 2>/dev/null || echo "ERR")
if [ "$count" = "ERR" ]; then
echo "$label: API error"
elif [ "$count" -ge "$expected" ]; then
echo "$label: $count (expected ≥$expected)"
else
echo " ⚠️ $label: $count (expected ≥$expected)"
fi
}

check_doctype "Item Groups" "Item%20Group" 3
check_doctype "Sites" "Site" 2
check_doctype "Study Events" "Study%20Event" 17
check_doctype "Items" "Item" 4
check_doctype "Drug Master" "zynomi_drug_master" 52
check_doctype "Healthcare Practitioner" "Healthcare%20Practitioner" 1

echo ""

# ── 4. Container Status ────────────────────────────────────
echo "━━━ 4. Container Status ━━━"
RUNNING=$(docker ps -q | wc -l | tr -d ' ')
STOPPED=$(docker ps -aq --filter status=exited | wc -l | tr -d ' ')
echo " Running: $RUNNING | Stopped: $STOPPED | Total: $((RUNNING + STOPPED))"
echo ""

# ── 5. Deploy Log (if available) ───────────────────────────
LOGFILE=$(ls -t /tmp/deploy-*.log 2>/dev/null | head -1)
if [ -n "$LOGFILE" ]; then
echo "━━━ 5. Deploy Log ($LOGFILE) ━━━"
if grep -q "Waiting for Frappe setup" "$LOGFILE"; then
echo " ✅ Step 6b: Setup container wait detected"
else
echo " ⚠️ Step 6b: Not found in log (older bundle?)"
fi
if grep -q "FRAPPE_API_TOKEN" "$LOGFILE"; then
echo " ✅ Step 7: Token extraction detected"
else
echo " ⚠️ Step 7: Not found in log"
fi
STAGES_PASS=$(grep -c "✅" "$LOGFILE" 2>/dev/null || echo 0)
echo " ℹ️ Log contains $STAGES_PASS checkmarks"
else
echo "━━━ 5. Deploy Log ━━━"
echo " ℹ️ No deploy log found in /tmp/"
fi
echo ""

# ── Summary ─────────────────────────────────────────────────
echo "╔══════════════════════════════════════════════════════╗"
echo "║ Verification complete ║"
echo "╚══════════════════════════════════════════════════════╝"
Save as a reusable script

You can save the full script to your server for repeated use:

curl -sL https://raw.githubusercontent.com/zynomi/ctms.devops/main/scripts/verify-deployment.sh \
-o /usr/local/bin/ctms-verify && chmod +x /usr/local/bin/ctms-verify

Then simply run ctms-verify after every deploy or update.


8. Manual Provisioning (Docker Compose)

If any provisioning step was skipped during deploy, or you need to re-run them on an existing deployment, use the commands below in this order. All commands use Docker Compose — no direct Frappe API calls or token passing required.

Prerequisites

All services must already be running (docker ps should show Zynexa, Frappe, Supabase, etc. as healthy). These are not destructive — they do not restart or recreate the Zynexa container.

cd /opt/ctms-deployment
DC="docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.production"

Step 1 — Healthcare Practitioner (ctms-init Stage 5)

Creates the default Healthcare Practitioner record in Frappe (e.g. HLC-PRAC-2026-00001). This runs inside the ctms-init container which already has the Frappe API token from .env.production.

# Run only Stage 5 (practitioner)
CTMS_INIT_STAGES=5 $DC --profile init run --rm ctms-init

Expected output: Prints the practitioner ID and instructions to set NEXT_PUBLIC_DEFAULT_PRACTITIONER_ID in .env.production.

OptionCommand
Dry run (preview only)CTMS_INIT_DRY_RUN=true CTMS_INIT_STAGES=5 $DC --profile init run --rm ctms-init
Re-run all 5 stages$DC --profile init run --rm ctms-init
Force-pull latest image first$DC --profile init pull ctms-init (then run above)
Idempotent

If a practitioner with the same name already exists, the command skips creation and prints the existing ID.

Step 2 — Seed Demo Users

Creates 4 demo staff users via the Zynexa signup API. Each user gets a Supabase Auth account + Frappe User record.

$DC --profile init run --rm ctms-user-seed
EmailRolePassword
kiran.v@zynomi.comPlatform AdministratorWelcome@1234
michael.x@zynomi.comStudy CoordinatorWelcome@1234
roshini.s@zynomi.comStudy DesignerWelcome@1234
peter.p@zynomi.comPrincipal InvestigatorWelcome@1234

Expected output: Done: 4 total | ✅ N created | ⏭️ N skipped

Idempotent

Existing users (HTTP 409/422) are counted as skipped — safe to re-run.

Step 3 — Seed Demo Patients

Creates 20 synthetic patient accounts. Each patient gets: Supabase Auth → Frappe User → Frappe Patient record.

$DC --profile init run --rm ctms-patient-seed
OptionCommand
Dry run (preview payloads)DRY_RUN=true $DC --profile init run --rm ctms-patient-seed

Expected output: Done: 20 total | ✅ N created | ⏭️ N skipped | ❌ N failed

All patients use @subject.com emails with password Welcome@1234.

Idempotent

Duplicate patients (HTTP 409/422) are skipped. Safe to re-run after partial failures.

Run All Three in Sequence

cd /opt/ctms-deployment
DC="docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env.production"

# 1. Healthcare Practitioner
CTMS_INIT_STAGES=5 $DC --profile init run --rm ctms-init

# 2. Demo Users (4 staff accounts)
$DC --profile init run --rm ctms-user-seed

# 3. Demo Patients (20 synthetic patients)
$DC --profile init run --rm ctms-patient-seed
Order matters

The practitioner must exist before patients can be assigned to studies. Demo users should be created before patients so coordinators can manage them.

For advanced options (selective stages, environment variables, alternative scripts), see the Platform Provisioning Commands recipe.


Quick Reference

CheckCommandExpected
All services healthySection 110/10 pass
Token presentSection 2Non-empty string
Item Groups existSection 3≥3 (incl. Laboratory, Drug)
Sites seededSection 3≥2
Study Events seededSection 3≥17
Items createdSection 3≥4
Drug Master loadedSection 3≥52
Practitioner existsSection 3≥1
Demo users createdSection 84 staff users
Demo patients createdSection 820 patients
Deploy log cleanSection 4All stages ✅

See Also