Add db_migrate init service to auto-run SQL schema on deploy

- New run_migrations.py: executes 02_*.sql and 03_*.sql in order
- New db_migrate service: runs once before all other services start
- All services now depend on db_migrate (service_completed_successfully)
- Tolerates re-deploy: catches errors from already-existing objects

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
David Kiania 2026-04-08 17:02:09 +03:00
parent b59616c7aa
commit 4a31de30b1
2 changed files with 88 additions and 11 deletions

View file

@ -14,6 +14,17 @@ services:
timeout: 5s timeout: 5s
retries: 5 retries: 5
db_migrate:
build:
context: .
dockerfile: Dockerfile
command: python run_migrations.py
depends_on:
timescale_db:
condition: service_healthy
env_file: .env
restart: "no"
ingest_movement: ingest_movement:
build: build:
context: . context: .
@ -21,9 +32,9 @@ services:
command: python ingest_movement_rev.py command: python ingest_movement_rev.py
restart: always restart: always
depends_on: depends_on:
timescale_db: db_migrate:
condition: service_healthy condition: service_completed_successfully
env_file: .env # Coolify will inject variables here env_file: .env
ingest_events: ingest_events:
build: build:
@ -32,8 +43,8 @@ services:
command: python ingest_events_rev.py command: python ingest_events_rev.py
restart: always restart: always
depends_on: depends_on:
timescale_db: db_migrate:
condition: service_healthy condition: service_completed_successfully
env_file: .env env_file: .env
webhook_receiver: webhook_receiver:
@ -43,8 +54,8 @@ services:
command: uvicorn webhook_receiver_rev:app --host 0.0.0.0 --port 8000 --workers 2 command: uvicorn webhook_receiver_rev:app --host 0.0.0.0 --port 8000 --workers 2
restart: always restart: always
depends_on: depends_on:
timescale_db: db_migrate:
condition: service_healthy condition: service_completed_successfully
env_file: .env env_file: .env
# No host port binding — Coolify's Traefik proxy routes traffic internally. # No host port binding — Coolify's Traefik proxy routes traffic internally.
# Set the webhook domain in Coolify UI pointing to this service on port 8000. # Set the webhook domain in Coolify UI pointing to this service on port 8000.
@ -58,8 +69,8 @@ services:
image: grafana/grafana:11.0.0 image: grafana/grafana:11.0.0
restart: always restart: always
depends_on: depends_on:
timescale_db: db_migrate:
condition: service_healthy condition: service_completed_successfully
environment: environment:
- GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD} - GF_SECURITY_ADMIN_PASSWORD=${GRAFANA_ADMIN_PASSWORD}
volumes: volumes:

66
run_migrations.py Normal file
View file

@ -0,0 +1,66 @@
"""
run_migrations.py Idempotent SQL migration runner for Docker init service.
Executes each .sql migration file in order using psycopg2.
Tolerates re-run errors (e.g. "policy already exists") so deploys are safe.
"""
import os
import sys
import psycopg2
DATABASE_URL = os.environ["DATABASE_URL"]
MIGRATIONS = [
"02_tracksolid_full_schema_rev.sql",
"03_webhook_schema_migration.sql",
]
def run_file(conn, path, filename):
"""Execute a SQL file. Returns True on success, False on error."""
with open(path) as f:
sql = f.read()
try:
with conn.cursor() as cur:
cur.execute(sql)
print(f" OK: {filename}")
return True
except psycopg2.Error as e:
msg = (e.pgerror or str(e)).strip().split("\n")[0]
print(f" WARN: {filename}: {msg}")
# Connection is now in error state — must reset
conn.close()
return False
def main():
print("=== Database Migration Runner ===")
conn = psycopg2.connect(DATABASE_URL)
conn.autocommit = True
warnings = 0
for sql_file in MIGRATIONS:
path = os.path.join("/app", sql_file)
if not os.path.exists(path):
print(f" SKIP: {sql_file} (not found)")
continue
print(f"Running {sql_file}...")
ok = run_file(conn, path, sql_file)
if not ok:
warnings += 1
# Reconnect for the next file
conn = psycopg2.connect(DATABASE_URL)
conn.autocommit = True
conn.close()
if warnings:
print(f"Completed with {warnings} warning(s) (expected on re-deploy).")
else:
print("All migrations applied cleanly.")
sys.exit(0)
if __name__ == "__main__":
main()