nomad job: bind-mount compose .env (simpler than nomadVar templates);
Some checks failed
ci / Lint & typecheck (push) Successful in 29s
ci / Unit tests (push) Successful in 1m4s
deploy / build-and-deploy (push) Failing after 46s

enable docker volumes
This commit is contained in:
febux 2026-09-14 02:53:56 +03:00
parent 1ae14536e6
commit a264b20c1c
2 changed files with 159 additions and 223 deletions

View file

@ -5,23 +5,19 @@
# docker0 host gateway 172.17.0.1 and the host-published ports # docker0 host gateway 172.17.0.1 and the host-published ports
# (15432/17379/5672/9000). The nginx edge cascade is unchanged. # (15432/17379/5672/9000). The nginx edge cascade is unchanged.
# #
# Secrets flow (nothing sensitive lives in this file): # Env: the compose .env file (~/DealDocumentScreening/.env on the VPS) is
# - app secrets + connection strings Nomad Variables at # bind-mounted into every container at /secrets/.env and sourced before the
# nomad/jobs/contract-check (see deploy/nomad/README.md §5), rendered # app starts. A sed one-liner rewrites compose hostnames (postgres, redis,
# agent-side into env by `template` blocks; # rabbitmq, minio) to 172.17.0.1 + published ports so the connections work
# - image tag + APP_VERSION use ${IMAGE_TAG}, rendered by `envsubst` at # from outside the compose network.
# submit time:
# IMAGE_TAG=<git-sha> envsubst < deploy/nomad/contract-check.nomad.hcl | nomad job run -
# - registry auth: host-level `docker login` on the VPS (README §5).
# #
# First deploy: verify with `nomad job validate` + `nomad job plan`, then # Image tag uses ${IMAGE_TAG}, rendered by envsubst at submit time.
# cutover workers one by one (README / docs/DEPLOY.md §15). # Registry auth: host-level `docker login` on the VPS (README §5).
job "contract-check" { job "contract-check" {
datacenters = ["vps"] datacenters = ["vps"]
type = "service" type = "service"
# Rolling defaults for every group
update { update {
max_parallel = 1 max_parallel = 1
min_healthy_time = "15s" min_healthy_time = "15s"
@ -34,9 +30,6 @@ job "contract-check" {
group "api" { group "api" {
count = 1 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 { update {
max_parallel = 1 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" { task "migrate" {
lifecycle { lifecycle {
hook = "prestart" hook = "prestart"
@ -83,21 +76,20 @@ job "contract-check" {
driver = "docker" driver = "docker"
config { 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}"
args = ["alembic", "upgrade", "head"] command = "sh"
args = [
# registry auth: host-level `docker login` on the VPS (README §5) "-c",
} "set -a; . /secrets/.env; set +a; exec alembic upgrade head",
]
template { mounts = [
destination = "secrets/env" {
env = true type = "bind"
change_mode = "restart" target = "/secrets/.env"
data = <<-EOT source = "/root/DealDocumentScreening/.env"
{{ with nomadVar "nomad/jobs/contract-check" }} readonly = true
DATABASE_URL="{{ .database_url }}" },
{{ end }} ]
EOT
} }
resources { resources {
@ -110,44 +102,29 @@ job "contract-check" {
driver = "docker" driver = "docker"
config { 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"] ports = ["http"]
mounts = [
# registry auth: host-level `docker login` on the VPS (README §5) {
} type = "bind"
target = "/secrets/.env"
env { source = "/root/DealDocumentScreening/.env"
ENV = "prod" readonly = true
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
} }
kill_timeout = "30s" kill_timeout = "30s"
@ -160,16 +137,12 @@ job "contract-check" {
} }
# WORKERS # 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-<name> <count>
group "worker-extract" { group "worker-extract" {
count = 1 count = 1
update { update {
canary = 1 canary = 1
auto_promote = true auto_promote = true
} }
@ -184,40 +157,32 @@ job "contract-check" {
driver = "docker" driver = "docker"
config { config {
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-extract:${IMAGE_TAG}" image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-extract:${IMAGE_TAG}"
command = "sh"
# registry auth: host-level `docker login` on the VPS (README §5) args = [
} "-c",
<<-SH
env { set -a; . /secrets/.env; set +a
ENV = "prod" export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
LOG_LEVEL = "INFO" export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
LOG_FORMAT = "json" export S3_ENDPOINT_URL="http://172.17.0.1:9000"
APP_VERSION = "${IMAGE_TAG}" export APP_VERSION="${IMAGE_TAG}"
} exec python -m src.contract_check.worker_extract
SH
template { ]
destination = "secrets/env" mounts = [
env = true {
change_mode = "restart" type = "bind"
data = <<-EOT target = "/secrets/.env"
{{ with nomadVar "nomad/jobs/contract-check" }} source = "/root/DealDocumentScreening/.env"
DATABASE_URL="{{ .database_url }}" readonly = true
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
} }
kill_timeout = "60s" kill_timeout = "60s"
resources { resources {
# CPU-bound OCR (tesseract); the heaviest task of the pipeline.
cpu = 500 cpu = 500
memory = 640 memory = 640
} }
@ -228,7 +193,7 @@ job "contract-check" {
count = 1 count = 1
update { update {
canary = 1 canary = 1
auto_promote = true auto_promote = true
} }
@ -243,40 +208,32 @@ job "contract-check" {
driver = "docker" driver = "docker"
config { config {
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-analyze:${IMAGE_TAG}" image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-analyze:${IMAGE_TAG}"
command = "sh"
# registry auth: host-level `docker login` on the VPS (README §5) args = [
} "-c",
<<-SH
env { set -a; . /secrets/.env; set +a
ENV = "prod" export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
LOG_LEVEL = "INFO" export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
LOG_FORMAT = "json" export S3_ENDPOINT_URL="http://172.17.0.1:9000"
APP_VERSION = "${IMAGE_TAG}" export APP_VERSION="${IMAGE_TAG}"
} exec python -m src.contract_check.worker_analyze
SH
template { ]
destination = "secrets/env" mounts = [
env = true {
change_mode = "restart" type = "bind"
data = <<-EOT target = "/secrets/.env"
{{ with nomadVar "nomad/jobs/contract-check" }} source = "/root/DealDocumentScreening/.env"
DATABASE_URL="{{ .database_url }}" readonly = true
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
} }
kill_timeout = "120s" kill_timeout = "120s"
resources { resources {
# LLM calls (I/O bound, long in-flight requests on shutdown).
cpu = 250 cpu = 250
memory = 384 memory = 384
} }
@ -287,7 +244,7 @@ job "contract-check" {
count = 1 count = 1
update { update {
canary = 1 canary = 1
auto_promote = true auto_promote = true
} }
@ -302,34 +259,27 @@ job "contract-check" {
driver = "docker" driver = "docker"
config { config {
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-prescreen:${IMAGE_TAG}" image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-prescreen:${IMAGE_TAG}"
command = "sh"
# registry auth: host-level `docker login` on the VPS (README §5) args = [
} "-c",
<<-SH
env { set -a; . /secrets/.env; set +a
ENV = "prod" export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
LOG_LEVEL = "INFO" export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
LOG_FORMAT = "json" export S3_ENDPOINT_URL="http://172.17.0.1:9000"
APP_VERSION = "${IMAGE_TAG}" export APP_VERSION="${IMAGE_TAG}"
} exec python -m src.contract_check.worker_prescreen
SH
template { ]
destination = "secrets/env" mounts = [
env = true {
change_mode = "restart" type = "bind"
data = <<-EOT target = "/secrets/.env"
{{ with nomadVar "nomad/jobs/contract-check" }} source = "/root/DealDocumentScreening/.env"
DATABASE_URL="{{ .database_url }}" readonly = true
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
} }
kill_timeout = "60s" kill_timeout = "60s"
@ -345,7 +295,7 @@ job "contract-check" {
count = 1 count = 1
update { update {
canary = 1 canary = 1
auto_promote = true auto_promote = true
} }
@ -360,33 +310,30 @@ job "contract-check" {
driver = "docker" driver = "docker"
config { config {
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-billing:${IMAGE_TAG}" image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-billing:${IMAGE_TAG}"
command = "sh"
# registry auth: host-level `docker login` on the VPS (README §5) args = [
} "-c",
<<-SH
env { set -a; . /secrets/.env; set +a
ENV = "prod" export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
LOG_LEVEL = "INFO" export APP_VERSION="${IMAGE_TAG}"
LOG_FORMAT = "json" exec python -m src.contract_check.worker_billing
APP_VERSION = "${IMAGE_TAG}" SH
} ]
mounts = [
template { {
destination = "secrets/env" type = "bind"
env = true target = "/secrets/.env"
change_mode = "restart" source = "/root/DealDocumentScreening/.env"
data = <<-EOT readonly = true
{{ with nomadVar "nomad/jobs/contract-check" }} },
DATABASE_URL="{{ .database_url }}" ]
{{ end }}
EOT
} }
kill_timeout = "60s" kill_timeout = "60s"
resources { resources {
# Background DB-only worker (dunning, renewals); lightest of the set.
cpu = 150 cpu = 150
memory = 256 memory = 256
} }
@ -397,7 +344,7 @@ job "contract-check" {
count = 1 count = 1
update { update {
canary = 1 canary = 1
auto_promote = true auto_promote = true
} }
@ -412,31 +359,26 @@ job "contract-check" {
driver = "docker" driver = "docker"
config { config {
image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-notify:${IMAGE_TAG}" image = "p2gnl.mu-dungeon.xyz/admin-git/contract-check-worker-notify:${IMAGE_TAG}"
command = "sh"
# registry auth: host-level `docker login` on the VPS (README §5) args = [
} "-c",
<<-SH
env { set -a; . /secrets/.env; set +a
ENV = "prod" export DATABASE_URL="$${DATABASE_URL/@postgres:/@172.17.0.1:15432:}"
LOG_LEVEL = "INFO" export RABBITMQ_URL="$${RABBITMQ_URL/@rabbitmq:/@172.17.0.1:5672:}"
LOG_FORMAT = "json" export APP_VERSION="${IMAGE_TAG}"
APP_VERSION = "${IMAGE_TAG}" exec python -m src.contract_check.worker_notify
} SH
]
template { mounts = [
destination = "secrets/env" {
env = true type = "bind"
change_mode = "restart" target = "/secrets/.env"
data = <<-EOT source = "/root/DealDocumentScreening/.env"
{{ with nomadVar "nomad/jobs/contract-check" }} readonly = true
DATABASE_URL="{{ .database_url }}" },
RABBITMQ_URL="{{ .rabbitmq_url }}" ]
SMTP_HOST="{{ .smtp_host }}"
SMTP_USERNAME="{{ .smtp_username }}"
SMTP_PASSWORD="{{ .smtp_password }}"
{{ end }}
EOT
} }
kill_timeout = "60s" kill_timeout = "60s"

View file

@ -58,17 +58,11 @@ client {
} }
} }
# Docker driver is intentionally left at defaults: # Docker driver: host bind mounts enabled so tasks can source /root's .env.
# - host bind mounts are DISABLED (docker.volumes.enabled=false by default) plugin "docker" {
# tasks are 12-factor (env-only config), no volumes required; config {
# - Nomad garbage-collects unused images periodically good for the volumes {
# 50 GB NVMe. enabled = true
# Enable only if a task ever needs a bind mount: }
# }
# plugin "docker" { }
# config {
# volumes {
# enabled = true
# }
# }
# }