From a264b20c1c64cbad1be6082ec2ea87fe536a3e8d Mon Sep 17 00:00:00 2001 From: febux Date: Mon, 14 Sep 2026 02:53:56 +0300 Subject: [PATCH] nomad job: bind-mount compose .env (simpler than nomadVar templates); enable docker volumes --- deploy/nomad/contract-check.nomad.hcl | 360 +++++++++++--------------- deploy/nomad/nomad.hcl | 22 +- 2 files changed, 159 insertions(+), 223 deletions(-) diff --git a/deploy/nomad/contract-check.nomad.hcl b/deploy/nomad/contract-check.nomad.hcl index 24b7972..f6a313b 100644 --- a/deploy/nomad/contract-check.nomad.hcl +++ b/deploy/nomad/contract-check.nomad.hcl @@ -5,23 +5,19 @@ # docker0 host gateway 172.17.0.1 and the host-published ports # (15432/17379/5672/9000). The nginx edge cascade is unchanged. # -# Secrets flow (nothing sensitive lives in this file): -# - app secrets + connection strings → Nomad Variables at -# nomad/jobs/contract-check (see deploy/nomad/README.md §5), rendered -# agent-side into env by `template` blocks; -# - image tag + APP_VERSION use ${IMAGE_TAG}, rendered by `envsubst` at -# submit time: -# IMAGE_TAG= envsubst < deploy/nomad/contract-check.nomad.hcl | nomad job run - -# - registry auth: host-level `docker login` on the VPS (README §5). +# Env: the compose .env file (~/DealDocumentScreening/.env on the VPS) is +# bind-mounted into every container at /secrets/.env and sourced before the +# app starts. A sed one-liner rewrites compose hostnames (postgres, redis, +# rabbitmq, minio) to 172.17.0.1 + published ports so the connections work +# from outside the compose network. # -# First deploy: verify with `nomad job validate` + `nomad job plan`, then -# cutover workers one by one (README / docs/DEPLOY.md §15). +# Image tag uses ${IMAGE_TAG}, rendered by envsubst at submit time. +# Registry auth: host-level `docker login` on the VPS (README §5). job "contract-check" { datacenters = ["vps"] type = "service" - # ── Rolling defaults for every group ──────────────────────────────────────── update { max_parallel = 1 min_healthy_time = "15s" @@ -34,9 +30,6 @@ job "contract-check" { group "api" { count = 1 - # No canary here: the static host port (18000) cannot be bound twice on a - # single node. Rolling = brief seconds-level gap per deploy; switch to - # dynamic ports + Traefik if zero-downtime becomes a requirement. update { max_parallel = 1 } @@ -74,7 +67,7 @@ job "contract-check" { } } - # One-shot migrations before the API starts (replaces `make migrate`). + # One-shot migrations before the API starts. task "migrate" { lifecycle { hook = "prestart" @@ -83,21 +76,20 @@ job "contract-check" { driver = "docker" config { - image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}" - args = ["alembic", "upgrade", "head"] - - # registry auth: host-level `docker login` on the VPS (README §5) - } - - template { - destination = "secrets/env" - env = true - change_mode = "restart" - data = <<-EOT - {{ with nomadVar "nomad/jobs/contract-check" }} - DATABASE_URL="{{ .database_url }}" - {{ end }} - EOT + image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}" + command = "sh" + args = [ + "-c", + "set -a; . /secrets/.env; set +a; exec alembic upgrade head", + ] + mounts = [ + { + type = "bind" + target = "/secrets/.env" + source = "/root/DealDocumentScreening/.env" + readonly = true + }, + ] } resources { @@ -110,44 +102,29 @@ job "contract-check" { driver = "docker" config { - image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}" + image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-api:${IMAGE_TAG}" + command = "sh" + args = [ + "-c", + <<-SH + set -a; . /secrets/.env; set +a + export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}" + export REDIS_URL="$${REDIS_URL/@redis:/@172.17.0.1:17379:}" + export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}" + export S3_ENDPOINT_URL="http://172.17.0.1:9000" + export APP_VERSION="${IMAGE_TAG}" + exec python -m src.contract_check.api + SH + ] ports = ["http"] - - # registry auth: host-level `docker login` on the VPS (README §5) - } - - env { - ENV = "prod" - LOG_LEVEL = "INFO" - LOG_FORMAT = "json" - APP_VERSION = "${IMAGE_TAG}" - } - - # All connection strings + app secrets, agent-rendered from Nomad - # Variables. Missing keys render empty — define them all once (README §5). - template { - destination = "secrets/env" - env = true - change_mode = "restart" - data = <<-EOT - {{ with nomadVar "nomad/jobs/contract-check" }} - DATABASE_URL="{{ .database_url }}" - REDIS_URL="{{ .redis_url }}" - RABBITMQ_URL="{{ .rabbitmq_url }}" - S3_ENDPOINT_URL="{{ .s3_endpoint_url }}" - S3_ACCESS_KEY="{{ .s3_access_key }}" - S3_SECRET_KEY="{{ .s3_secret_key }}" - S3_BUCKET="{{ .s3_bucket }}" - JWT_SECRET="{{ .jwt_secret }}" - TELEGRAM_BOT_TOKEN="{{ .telegram_bot_token }}" - OLLAMA_API_KEY="{{ .ollama_api_key }}" - YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}" - SMTP_HOST="{{ .smtp_host }}" - SMTP_USERNAME="{{ .smtp_username }}" - SMTP_PASSWORD="{{ .smtp_password }}" - METRICS_BEARER_TOKEN="{{ .metrics_bearer_token }}" - {{ end }} - EOT + mounts = [ + { + type = "bind" + target = "/secrets/.env" + source = "/root/DealDocumentScreening/.env" + readonly = true + }, + ] } kill_timeout = "30s" @@ -160,16 +137,12 @@ job "contract-check" { } # ══ WORKERS ═══════════════════════════════════════════════════════════════ - # Workers bind no ports, so each group gets canary + auto_promote: - # new version starts alongside the old one, must pass min_healthy_time, - # then old allocations stop. auto_revert rolls back on failure. - # Scale with: nomad job scale contract-check worker- group "worker-extract" { count = 1 update { - canary = 1 + canary = 1 auto_promote = true } @@ -184,40 +157,32 @@ job "contract-check" { driver = "docker" config { - image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-extract:${IMAGE_TAG}" - - # registry auth: host-level `docker login` on the VPS (README §5) - } - - env { - ENV = "prod" - LOG_LEVEL = "INFO" - LOG_FORMAT = "json" - APP_VERSION = "${IMAGE_TAG}" - } - - template { - destination = "secrets/env" - env = true - change_mode = "restart" - data = <<-EOT - {{ with nomadVar "nomad/jobs/contract-check" }} - DATABASE_URL="{{ .database_url }}" - RABBITMQ_URL="{{ .rabbitmq_url }}" - S3_ENDPOINT_URL="{{ .s3_endpoint_url }}" - S3_ACCESS_KEY="{{ .s3_access_key }}" - S3_SECRET_KEY="{{ .s3_secret_key }}" - S3_BUCKET="{{ .s3_bucket }}" - OLLAMA_API_KEY="{{ .ollama_api_key }}" - YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}" - {{ end }} - EOT + image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-extract:${IMAGE_TAG}" + command = "sh" + args = [ + "-c", + <<-SH + set -a; . /secrets/.env; set +a + export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}" + export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}" + export S3_ENDPOINT_URL="http://172.17.0.1:9000" + export APP_VERSION="${IMAGE_TAG}" + exec python -m src.contract_check.worker_extract + SH + ] + mounts = [ + { + type = "bind" + target = "/secrets/.env" + source = "/root/DealDocumentScreening/.env" + readonly = true + }, + ] } kill_timeout = "60s" resources { - # CPU-bound OCR (tesseract); the heaviest task of the pipeline. cpu = 500 memory = 640 } @@ -228,7 +193,7 @@ job "contract-check" { count = 1 update { - canary = 1 + canary = 1 auto_promote = true } @@ -243,40 +208,32 @@ job "contract-check" { driver = "docker" config { - image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-analyze:${IMAGE_TAG}" - - # registry auth: host-level `docker login` on the VPS (README §5) - } - - env { - ENV = "prod" - LOG_LEVEL = "INFO" - LOG_FORMAT = "json" - APP_VERSION = "${IMAGE_TAG}" - } - - template { - destination = "secrets/env" - env = true - change_mode = "restart" - data = <<-EOT - {{ with nomadVar "nomad/jobs/contract-check" }} - DATABASE_URL="{{ .database_url }}" - RABBITMQ_URL="{{ .rabbitmq_url }}" - S3_ENDPOINT_URL="{{ .s3_endpoint_url }}" - S3_ACCESS_KEY="{{ .s3_access_key }}" - S3_SECRET_KEY="{{ .s3_secret_key }}" - S3_BUCKET="{{ .s3_bucket }}" - OLLAMA_API_KEY="{{ .ollama_api_key }}" - YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}" - {{ end }} - EOT + image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-analyze:${IMAGE_TAG}" + command = "sh" + args = [ + "-c", + <<-SH + set -a; . /secrets/.env; set +a + export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}" + export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}" + export S3_ENDPOINT_URL="http://172.17.0.1:9000" + export APP_VERSION="${IMAGE_TAG}" + exec python -m src.contract_check.worker_analyze + SH + ] + mounts = [ + { + type = "bind" + target = "/secrets/.env" + source = "/root/DealDocumentScreening/.env" + readonly = true + }, + ] } kill_timeout = "120s" resources { - # LLM calls (I/O bound, long in-flight requests on shutdown). cpu = 250 memory = 384 } @@ -287,7 +244,7 @@ job "contract-check" { count = 1 update { - canary = 1 + canary = 1 auto_promote = true } @@ -302,34 +259,27 @@ job "contract-check" { driver = "docker" config { - image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-prescreen:${IMAGE_TAG}" - - # registry auth: host-level `docker login` on the VPS (README §5) - } - - env { - ENV = "prod" - LOG_LEVEL = "INFO" - LOG_FORMAT = "json" - APP_VERSION = "${IMAGE_TAG}" - } - - template { - destination = "secrets/env" - env = true - change_mode = "restart" - data = <<-EOT - {{ with nomadVar "nomad/jobs/contract-check" }} - DATABASE_URL="{{ .database_url }}" - RABBITMQ_URL="{{ .rabbitmq_url }}" - S3_ENDPOINT_URL="{{ .s3_endpoint_url }}" - S3_ACCESS_KEY="{{ .s3_access_key }}" - S3_SECRET_KEY="{{ .s3_secret_key }}" - S3_BUCKET="{{ .s3_bucket }}" - OLLAMA_API_KEY="{{ .ollama_api_key }}" - YANDEXGPT_API_KEY="{{ .yandexgpt_api_key }}" - {{ end }} - EOT + image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-prescreen:${IMAGE_TAG}" + command = "sh" + args = [ + "-c", + <<-SH + set -a; . /secrets/.env; set +a + export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}" + export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}" + export S3_ENDPOINT_URL="http://172.17.0.1:9000" + export APP_VERSION="${IMAGE_TAG}" + exec python -m src.contract_check.worker_prescreen + SH + ] + mounts = [ + { + type = "bind" + target = "/secrets/.env" + source = "/root/DealDocumentScreening/.env" + readonly = true + }, + ] } kill_timeout = "60s" @@ -345,7 +295,7 @@ job "contract-check" { count = 1 update { - canary = 1 + canary = 1 auto_promote = true } @@ -360,33 +310,30 @@ job "contract-check" { driver = "docker" config { - image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-billing:${IMAGE_TAG}" - - # registry auth: host-level `docker login` on the VPS (README §5) - } - - env { - ENV = "prod" - LOG_LEVEL = "INFO" - LOG_FORMAT = "json" - APP_VERSION = "${IMAGE_TAG}" - } - - template { - destination = "secrets/env" - env = true - change_mode = "restart" - data = <<-EOT - {{ with nomadVar "nomad/jobs/contract-check" }} - DATABASE_URL="{{ .database_url }}" - {{ end }} - EOT + image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-billing:${IMAGE_TAG}" + command = "sh" + args = [ + "-c", + <<-SH + set -a; . /secrets/.env; set +a + export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}" + export APP_VERSION="${IMAGE_TAG}" + exec python -m src.contract_check.worker_billing + SH + ] + mounts = [ + { + type = "bind" + target = "/secrets/.env" + source = "/root/DealDocumentScreening/.env" + readonly = true + }, + ] } kill_timeout = "60s" resources { - # Background DB-only worker (dunning, renewals); lightest of the set. cpu = 150 memory = 256 } @@ -397,7 +344,7 @@ job "contract-check" { count = 1 update { - canary = 1 + canary = 1 auto_promote = true } @@ -412,31 +359,26 @@ job "contract-check" { driver = "docker" config { - image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-notify:${IMAGE_TAG}" - - # registry auth: host-level `docker login` on the VPS (README §5) - } - - env { - ENV = "prod" - LOG_LEVEL = "INFO" - LOG_FORMAT = "json" - APP_VERSION = "${IMAGE_TAG}" - } - - template { - destination = "secrets/env" - env = true - change_mode = "restart" - data = <<-EOT - {{ with nomadVar "nomad/jobs/contract-check" }} - DATABASE_URL="{{ .database_url }}" - RABBITMQ_URL="{{ .rabbitmq_url }}" - SMTP_HOST="{{ .smtp_host }}" - SMTP_USERNAME="{{ .smtp_username }}" - SMTP_PASSWORD="{{ .smtp_password }}" - {{ end }} - EOT + image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-notify:${IMAGE_TAG}" + command = "sh" + args = [ + "-c", + <<-SH + set -a; . /secrets/.env; set +a + export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}" + export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}" + export APP_VERSION="${IMAGE_TAG}" + exec python -m src.contract_check.worker_notify + SH + ] + mounts = [ + { + type = "bind" + target = "/secrets/.env" + source = "/root/DealDocumentScreening/.env" + readonly = true + }, + ] } kill_timeout = "60s" diff --git a/deploy/nomad/nomad.hcl b/deploy/nomad/nomad.hcl index adae33b..48a06af 100644 --- a/deploy/nomad/nomad.hcl +++ b/deploy/nomad/nomad.hcl @@ -58,17 +58,11 @@ client { } } -# Docker driver is intentionally left at defaults: -# - host bind mounts are DISABLED (docker.volumes.enabled=false by default) -# — tasks are 12-factor (env-only config), no volumes required; -# - Nomad garbage-collects unused images periodically — good for the -# 50 GB NVMe. -# Enable only if a task ever needs a bind mount: -# -# plugin "docker" { -# config { -# volumes { -# enabled = true -# } -# } -# } +# Docker driver: host bind mounts enabled so tasks can source /root's .env. +plugin "docker" { + config { + volumes { + enabled = true + } + } +}