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
# (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=<git-sha> 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-<name> <count>
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"

View file

@ -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
}
}
}