Deploy a service to the staging environment on srv-staging (35.195.54.220) using Coolify. Covers pre-deploy checks, Coolify API interaction, health verification, and post-deploy validation.
Applies to all ODS Platform services that run as long-lived HTTP servers. Does not apply to CLI tools (e.g., migration scripts) or library crates (e.g., ods-common). For non-HTTP services, see SOP-007 (External Blockers) for escalation.
~/dev/ops/coolify/{service}.json
with valid app_uuidsource ~/.env.adlc provides
$COOLIFY_API_URL and $COOLIFY_API_TOKEN/health for Rust
services, /api/health for Node)source ~/.env.adlc
SERVICE="{service}"
UUID=$(python3 -c "import json; print(json.load(open('$HOME/dev/ops/coolify/$SERVICE.json'))['coolify']['app_uuid'])")
echo "Deploying $SERVICE (UUID: $UUID)"curl -sf "$COOLIFY_API_URL/api/v1/applications/$UUID" \
-H "Authorization: Bearer $COOLIFY_API_TOKEN" | python3 -m json.tool | head -20If 404: the app needs to be created in Coolify UI first. Post to Slack DM:
:eyes: HUMAN REVIEW -- {service} needs first-time Coolify setup. Create app in Coolify UI, save UUID to ~/dev/ops/coolify/{service}.json
coolifyCritical lesson (2026-03-21): containers must be on the
coolify network to reach ods-postgres and redpanda.
curl -sf "$COOLIFY_API_URL/api/v1/applications/$UUID" \
-H "Authorization: Bearer $COOLIFY_API_TOKEN" | python3 -c "
import sys, json
d = json.load(sys.stdin)
net = d.get('docker_network', d.get('settings', {}).get('docker_network', 'unknown'))
print(f'Docker network: {net}')
if net != 'coolify':
print('WARNING: Network must be coolify!')
"curl -sf -X POST "$COOLIFY_API_URL/api/v1/applications/$UUID/restart" \
-H "Authorization: Bearer $COOLIFY_API_TOKEN" \
-H "Content-Type: application/json"Note: Use /restart not /deploy or
/redeploy – this Coolify version only supports
/restart (lesson from 2026-03-22).
For Rust services, use a 6-minute window (36 polls x 10s) due to long compile times (lesson from 2026-03-23):
FQDN=$(python3 -c "import json; print(json.load(open('$HOME/dev/ops/coolify/$SERVICE.json'))['coolify']['fqdn'])")
HEALTH_PATH="/health" # Use /api/health for Node services
MAX_POLLS=36 # 6 minutes for Rust, 18 for Node
for i in $(seq 1 $MAX_POLLS); do
code=$(curl -sf -o /dev/null -w "%{http_code}" "${FQDN}${HEALTH_PATH}" 2>/dev/null || echo "000")
echo "Poll $i/$MAX_POLLS: HTTP $code"
[ "$code" = "200" ] && echo "HEALTHY" && break
sleep 10
done# Full health check
curl -sf "${FQDN}${HEALTH_PATH}" | python3 -m json.tool
# Check response headers
curl -sI "${FQDN}${HEALTH_PATH}" | head -10CLI="$HOME/dev/ops/adlc-v2/scripts/cli"
bash $CLI/write-status.sh $SERVICE deploy DEPLOYED "Deployed to $FQDN"
bash $CLI/write-pipeline-state.sh {project} $SERVICE STAGING_DEPLOYED "$FQDN"source ~/.env.adlc
curl -sf -X POST "https://slack.com/api/chat.postMessage" \
-H "Authorization: Bearer $SLACK_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d "$(python3 -c "import json; print(json.dumps({'channel':'C0AN0N8AUGZ','text':':white_check_mark: $SERVICE deployed to staging -- $FQDN'}))")"curl -sf ${FQDN}/health succeedscat ~/dev/ops/outputs/${SERVICE}-deploy.statusgrep $SERVICE ~/.claude/agent-memory/pipeline/state.mdIf deployment fails or service is unhealthy:
curl -sf "$COOLIFY_API_URL/api/v1/applications/$UUID/logs" \
-H "Authorization: Bearer $COOLIFY_API_TOKEN" | tail -50cd ~/dev/projects/$SERVICE
git log --oneline -5 # Find last known good commit
# Revert on staging branch, push, then restart via Coolify~/dev/ops/coolify/{service}.json$COOLIFY_API_URL (internal:
http://10.132.0.2:8000, NOT the public hostname)coolify
(2026-03-21)/restart not /deploy
(2026-03-22)$COOLIFY_API_URL from .env.adlc (2026-03-23)