Deploy per Terraform/SSH
Some checks failed
MT5 Stack Deploy / deploy (push) Failing after 4s

This commit is contained in:
bast 2026-11-07 06:31:49 +01:00
parent d505268b30
commit 3f649fca21
10 changed files with 496 additions and 173 deletions

View file

@ -1,23 +1,12 @@
name: MT5 Helm GitOps Deploy name: MT5 Stack Deploy
on: on:
push: push:
branches: [ "main" ] branches: [ "main" ]
tags: [ "*" ]
workflow_dispatch: workflow_dispatch:
env:
# Gleiche Registry wie im boday_tf-Projekt, zwei Namen:
# PUSH_REGISTRY: wie der Runner sie erreicht
# PULL_REGISTRY: wie k3s/containerd sie erreicht (registries.yaml, CA unter certs.d)
PUSH_REGISTRY: localhost:3005
PULL_REGISTRY: registry.local
# Host-Header für Traefik-Ingress des Web-UI (leer = kein Ingress, kein Health-Check per HTTP)
INGRESS_HOST: mt5.local
jobs: jobs:
build-and-deploy: deploy:
if: false
runs-on: ubuntu-latest runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest container: catthehacker/ubuntu:act-latest
steps: steps:
@ -26,87 +15,21 @@ jobs:
# HINWEIS: Terraform-Binary kommt wie bei boday_tf über das globale Runner-Volume (:ro) # HINWEIS: Terraform-Binary kommt wie bei boday_tf über das globale Runner-Volume (:ro)
- name: Trust Local Nginx Certificate Inside Container # Nur versionierte Dateien deployen (kein .git, keine Secrets, keine Laufzeitdaten)
- name: Build deploy bundle
run: | run: |
mkdir -p /usr/local/share/ca-certificates/ rm -rf .deploy-bundle && mkdir .deploy-bundle
cp nginx_ssl/nginx.crt /usr/local/share/ca-certificates/nginx-sandbox.crt 2>/dev/null || echo "Nutze Workspace-Zertifikat" git archive HEAD | tar -x -C .deploy-bundle
update-ca-certificates if find .deploy-bundle -name bots.env -o -name .env -o -name '*.session' | grep -q .; then
echo "ABBRUCH: bots.env / .env / *.session ist im Repo versioniert"; exit 1
fi
du -sh .deploy-bundle
# Docker verlangt Image-Namen in Kleinbuchstaben – "MetaTrader5-Docker-Image" würde sonst scheitern
- name: Compute Image Name
run: |
IMAGE_PATH="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
echo "IMAGE=${PUSH_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV"
echo "PULL_IMAGE=${PULL_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV"
echo "Push: ${PUSH_REGISTRY}/${IMAGE_PATH}"
echo "Pull: ${PULL_REGISTRY}/${IMAGE_PATH}"
- name: Log in to Local Registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${PUSH_REGISTRY} -u ${{ github.actor }} --password-stdin
# Ein Build, zwei Tags (das Image ist ~4 GB – doppelt bauen kostet unnötig Zeit)
- name: Build MT5 Image
run: |
docker build --platform linux/amd64 \
-t ${IMAGE}:${{ github.sha }} \
-t ${IMAGE}:latest \
.
- name: Push MT5 Image
run: |
docker push ${IMAGE}:${{ github.sha }}
docker push ${IMAGE}:latest
- name: Prepare Kubernetes Access
run: |
echo "${{ secrets.KUBECONFIG_DATA }}" > ./kind_kubeconfig
chmod 600 ./kind_kubeconfig
# Erster Rollout kann bis 15 Min dauern (Wine + MT5 Installation nach /config)
# init mit Retry, weil der DNS-Resolver sporadisch "server misbehaving" liefert
- name: Terraform Deploy - name: Terraform Deploy
env: env:
# Web-UI-Login (KasmVNC) – wie bisher TF_VAR_ssh_private_key: ${{ secrets.DEPLOY_SSH_KEY }}
TF_VAR_vnc_user: ${{ secrets.MT5_VNC_USER || 'trader' }} TF_VAR_ssh_host: ${{ vars.DEPLOY_HOST || 'BITMT503' }}
TF_VAR_vnc_password: ${{ secrets.MT5_VNC_PASSWORD }}
# Broker-Login für MT5 – landet als login.ini im Pod und wird beim Start geladen
TF_VAR_mt5_login: ${{ secrets.MT5USER }}
TF_VAR_mt5_password: ${{ secrets.MT5PASSWORD }}
TF_VAR_mt5_server: ${{ secrets.MT5_SERVER }}
run: | run: |
terraform --version terraform --version
for i in 1 2 3; do for i in 1 2 3; do terraform init && break; echo "init fehlgeschlagen, Versuch $i"; sleep 10; done
terraform init && break terraform apply -auto-approve
if [ "$i" = "3" ]; then echo "terraform init nach 3 Versuchen fehlgeschlagen"; exit 1; fi
echo "init fehlgeschlagen, Versuch $i – neuer Anlauf in 10s"
sleep 10
done
terraform apply -auto-approve \
-var="image_repository=${PULL_IMAGE}" \
-var="app_version=${{ github.sha }}" \
-var="ingress_host=${INGRESS_HOST}"
# KasmVNC antwortet ohne Login mit 401 – daher mit Basic Auth prüfen und auf den Erststart warten
- name: Verify App Health
if: env.INGRESS_HOST != ''
env:
VNC_USER: ${{ secrets.MT5_VNC_USER || 'trader' }}
VNC_PASS: ${{ secrets.MT5_VNC_PASSWORD }}
run: |
for i in $(seq 1 40); do
CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 \
-u "${VNC_USER}:${VNC_PASS}" -H "Host: ${INGRESS_HOST}" \
http://local-k3s-platform:80/ || true)
echo "Versuch ${i}: HTTP ${CODE}"
if [ "${CODE}" = "200" ]; then
echo "Erfolg: MetaTrader 5 Web-UI läuft via Helm im GitOps-Standard!"
exit 0
fi
sleep 15
done
echo "MT5 Web-UI nach 10 Minuten nicht erreichbar"
exit 1
- name: Cleanup Kubeconfig
if: always()
run: rm -f ./kind_kubeconfig

112
.github/workflows/deploy.yml.k3s vendored Normal file
View file

@ -0,0 +1,112 @@
name: MT5 Helm GitOps Deploy
on:
push:
branches: [ "main" ]
tags: [ "*" ]
workflow_dispatch:
env:
# Gleiche Registry wie im boday_tf-Projekt, zwei Namen:
# PUSH_REGISTRY: wie der Runner sie erreicht
# PULL_REGISTRY: wie k3s/containerd sie erreicht (registries.yaml, CA unter certs.d)
PUSH_REGISTRY: localhost:3005
PULL_REGISTRY: registry.local
# Host-Header für Traefik-Ingress des Web-UI (leer = kein Ingress, kein Health-Check per HTTP)
INGRESS_HOST: mt5.local
jobs:
build-and-deploy:
if: false
runs-on: ubuntu-latest
container: catthehacker/ubuntu:act-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
# HINWEIS: Terraform-Binary kommt wie bei boday_tf über das globale Runner-Volume (:ro)
- name: Trust Local Nginx Certificate Inside Container
run: |
mkdir -p /usr/local/share/ca-certificates/
cp nginx_ssl/nginx.crt /usr/local/share/ca-certificates/nginx-sandbox.crt 2>/dev/null || echo "Nutze Workspace-Zertifikat"
update-ca-certificates
# Docker verlangt Image-Namen in Kleinbuchstaben – "MetaTrader5-Docker-Image" würde sonst scheitern
- name: Compute Image Name
run: |
IMAGE_PATH="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
echo "IMAGE=${PUSH_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV"
echo "PULL_IMAGE=${PULL_REGISTRY}/${IMAGE_PATH}" >> "$GITHUB_ENV"
echo "Push: ${PUSH_REGISTRY}/${IMAGE_PATH}"
echo "Pull: ${PULL_REGISTRY}/${IMAGE_PATH}"
- name: Log in to Local Registry
run: echo "${{ secrets.REGISTRY_TOKEN }}" | docker login ${PUSH_REGISTRY} -u ${{ github.actor }} --password-stdin
# Ein Build, zwei Tags (das Image ist ~4 GB – doppelt bauen kostet unnötig Zeit)
- name: Build MT5 Image
run: |
docker build --platform linux/amd64 \
-t ${IMAGE}:${{ github.sha }} \
-t ${IMAGE}:latest \
.
- name: Push MT5 Image
run: |
docker push ${IMAGE}:${{ github.sha }}
docker push ${IMAGE}:latest
- name: Prepare Kubernetes Access
run: |
echo "${{ secrets.KUBECONFIG_DATA }}" > ./kind_kubeconfig
chmod 600 ./kind_kubeconfig
# Erster Rollout kann bis 15 Min dauern (Wine + MT5 Installation nach /config)
# init mit Retry, weil der DNS-Resolver sporadisch "server misbehaving" liefert
- name: Terraform Deploy
env:
# Web-UI-Login (KasmVNC) – wie bisher
TF_VAR_vnc_user: ${{ secrets.MT5_VNC_USER || 'trader' }}
TF_VAR_vnc_password: ${{ secrets.MT5_VNC_PASSWORD }}
# Broker-Login für MT5 – landet als login.ini im Pod und wird beim Start geladen
TF_VAR_mt5_login: ${{ secrets.MT5USER }}
TF_VAR_mt5_password: ${{ secrets.MT5PASSWORD }}
TF_VAR_mt5_server: ${{ secrets.MT5_SERVER }}
run: |
terraform --version
for i in 1 2 3; do
terraform init && break
if [ "$i" = "3" ]; then echo "terraform init nach 3 Versuchen fehlgeschlagen"; exit 1; fi
echo "init fehlgeschlagen, Versuch $i – neuer Anlauf in 10s"
sleep 10
done
terraform apply -auto-approve \
-var="image_repository=${PULL_IMAGE}" \
-var="app_version=${{ github.sha }}" \
-var="ingress_host=${INGRESS_HOST}"
# KasmVNC antwortet ohne Login mit 401 – daher mit Basic Auth prüfen und auf den Erststart warten
- name: Verify App Health
if: env.INGRESS_HOST != ''
env:
VNC_USER: ${{ secrets.MT5_VNC_USER || 'trader' }}
VNC_PASS: ${{ secrets.MT5_VNC_PASSWORD }}
run: |
for i in $(seq 1 40); do
CODE=$(curl -s -o /dev/null -w '%{http_code}' --connect-timeout 5 \
-u "${VNC_USER}:${VNC_PASS}" -H "Host: ${INGRESS_HOST}" \
http://local-k3s-platform:80/ || true)
echo "Versuch ${i}: HTTP ${CODE}"
if [ "${CODE}" = "200" ]; then
echo "Erfolg: MetaTrader 5 Web-UI läuft via Helm im GitOps-Standard!"
exit 0
fi
sleep 15
done
echo "MT5 Web-UI nach 10 Minuten nicht erreichbar"
exit 1
- name: Cleanup Kubeconfig
if: always()
run: rm -f ./kind_kubeconfig

17
.gitignore vendored
View file

@ -1,3 +1,20 @@
trader-data trader-data
config config
.env .env
# --- Deploy: Secrets und Laufzeitdaten bleiben nur auf dem Server ---
bots.env
.env
*.session
config/
config.*/
trader-data/
# Terraform
.terraform/
.terraform.lock.hcl
*.tfstate
*.tfstate.*
.deploy-bundle/
__pycache__/
*.bak
*.bak-*

39
DEPLOY.md Normal file
View file

@ -0,0 +1,39 @@
# Deploy dieses Stacks
Diese App deployt sich selbst – unabhängig von anderen Apps.
| Datei | Zweck |
|---|---|
| `deploy.conf` | Kenndaten: Dienstnamen, MCP-Port, Diagnosedatei |
| `main.tf` | Terraform: Sperre → Dateien per SSH kopieren → `scripts/deploy-local.sh` |
| `.github/workflows/deploy.yml` | Forgejo: Bundle mit `git archive` bauen, `terraform apply` |
| `scripts/deploy-local.sh` | Deploy auf dem Server: Sperre, `docker compose up -d --build`, warten |
| `scripts/check-stack.sh` | Statusprüfung: Container, Bridge, Konto, Kurs, Positionen, Trader, Telegram |
## Deployen
| Weg | Befehl |
|---|---|
| Push auf `main` | Forgejo-Workflow → Terraform |
| Forgejo manuell | Actions → *MT5 Stack Deploy* → Run workflow |
| Direkt auf dem Server | `cd <stack-ordner> && scripts/deploy-local.sh` |
**Sperre:** Ein Deploy bricht ab, solange der Stack offene Positionen hat. Der Trader
hält Positionen nur im Speicher, nach einem Neustart setzt er für sie kein BE mehr.
`scripts/deploy-local.sh --force` übergeht die Sperre bewusst.
**Nur auf dem Server, nie im Repo:** `config/`, `trader-data/`, `.env`, `bots.env`.
Terraform kopiert nur versionierte Dateien (`git archive`), diese bleiben unberührt.
**Direkt auf dem Server geändert?** Dort committen und pushen – sonst dreht der
nächste Deploy die Änderung zurück.
## Status
```bash
scripts/check-stack.sh # ausführlich
scripts/check-stack.sh -q # nur Fehler (cron)
```
## Forgejo einrichten
- Secret `DEPLOY_SSH_KEY`: privater SSH-Key für `bast@BITMT503`
- optional Variable `DEPLOY_HOST`: IP, falls der Runner `BITMT503` nicht auflöst

8
deploy.conf Normal file
View file

@ -0,0 +1,8 @@
# deploy.conf – Kenndaten DIESES Stacks (von scripts/*.sh gelesen)
STACK_KEY=goldex
STACK_NAME="Goldex (ch1, Vantage)"
MCP_PORT=8010 # Host-Port aus docker-compose.yaml (127.0.0.1:8010:8000)
MT5_SVC=mt5-gx # Dienstnamen aus docker-compose.yaml
MCP_SVC=mt5-mcp-gx
TRADER_SVC=live-trader-gx
DIAG_FILE=trader-data/diagnostics_ch1-Goldex.json

131
main.tf
View file

@ -1,105 +1,72 @@
terraform { # Deployt DIESEN Stack per SSH auf den Server – unabhängig von anderen Apps.
required_providers { # Ablauf: Sperre prüfen -> sauberes Bundle (git archive) in den Stack-Ordner
kubernetes = { # kopieren -> scripts/deploy-local.sh (docker compose up -d --build + Check).
source = "hashicorp/kubernetes" # config/, trader-data/, .env und bots.env auf dem Server bleiben unberührt,
version = "~> 2.26" # weil sie nicht im Repo (und damit nicht im Bundle) sind.
}
helm = {
source = "hashicorp/helm"
version = "~> 2.12"
}
}
# Eigener State-Pfad, damit sich MT5 und die Webseite nicht in die Quere kommen terraform {
backend "consul" { backend "consul" {
address = "consul:8500" address = "consul:8500"
path = "terraform/states/mt5_tf" path = "terraform/states/mt5_goldex" # pro Repo eindeutig
} }
} }
# --- Variablen --------------------------------------------------------------- variable "ssh_host" {
variable "app_version" {
type = string type = string
default = "latest" default = "BITMT503"
} }
variable "image_repository" { variable "ssh_user" {
type = string type = string
default = "gmag11/metatrader5_vnc" default = "bast"
} }
variable "vnc_user" { variable "ssh_private_key" {
type = string
default = "trader"
}
# Kommt aus TF_VAR_vnc_password (Secret MT5_VNC_PASSWORD)
variable "vnc_password" {
type = string type = string
sensitive = true sensitive = true
} }
variable "ingress_host" { variable "stack_dir" {
type = string type = string
default = "" default = "/home/bast/build/MetaTrader5-Docker-Goldex"
} }
# --- Provider ---------------------------------------------------------------- # Wird vom Workflow per "git archive HEAD" erzeugt – nur versionierte Dateien
variable "bundle_dir" {
provider "kubernetes" { type = string
config_path = "./kind_kubeconfig" default = ".deploy-bundle"
host = "https://local-k3s-platform:6443"
insecure = true
} }
provider "helm" { resource "terraform_data" "stack" {
kubernetes { # Neu deployen nur, wenn sich eine versionierte Datei geändert hat
config_path = "./kind_kubeconfig" triggers_replace = sha1(join("", [
host = "https://local-k3s-platform:6443" for f in sort(fileset(var.bundle_dir, "**")) : "${f}:${filesha1("${var.bundle_dir}/${f}")}"
insecure = true ]))
}
} connection {
type = "ssh"
# --- MetaTrader 5 Deployment ------------------------------------------------- host = var.ssh_host
user = var.ssh_user
resource "helm_release" "mt5" { private_key = var.ssh_private_key
name = "mt5" }
chart = "./charts/mt5"
namespace = "trading" # 1. Sperre mit dem Script, das schon auf dem Server liegt (beim ersten Mal fehlt es)
create_namespace = true provisioner "remote-exec" {
inline = [
# Erster Start installiert Wine + MT5 nach /config – das dauert ein paar Minuten "cd '${var.stack_dir}' && if [ -x scripts/deploy-local.sh ]; then scripts/deploy-local.sh --check-flat; else echo 'Erster Deploy – keine Sperre'; fi",
timeout = 900 ]
wait = true }
set { # 2. Versionierte Dateien in den Stack-Ordner kopieren
name = "image.repository" provisioner "file" {
value = var.image_repository source = "${var.bundle_dir}/"
} destination = var.stack_dir
}
set {
name = "image.tag" # 3. Bauen, starten, auf gesunden Stack warten
value = var.app_version provisioner "remote-exec" {
} inline = [
"cd '${var.stack_dir}' && chmod +x scripts/*.sh && scripts/deploy-local.sh",
set { ]
name = "auth.user"
value = var.vnc_user
}
set_sensitive {
name = "auth.password"
value = var.vnc_password
}
set {
name = "ingress.enabled"
value = var.ingress_host != "" ? "true" : "false"
}
set {
name = "ingress.host"
value = var.ingress_host
} }
} }

105
main.tf.k3s Normal file
View file

@ -0,0 +1,105 @@
terraform {
required_providers {
kubernetes = {
source = "hashicorp/kubernetes"
version = "~> 2.26"
}
helm = {
source = "hashicorp/helm"
version = "~> 2.12"
}
}
# Eigener State-Pfad, damit sich MT5 und die Webseite nicht in die Quere kommen
backend "consul" {
address = "consul:8500"
path = "terraform/states/mt5_tf"
}
}
# --- Variablen ---------------------------------------------------------------
variable "app_version" {
type = string
default = "latest"
}
variable "image_repository" {
type = string
default = "gmag11/metatrader5_vnc"
}
variable "vnc_user" {
type = string
default = "trader"
}
# Kommt aus TF_VAR_vnc_password (Secret MT5_VNC_PASSWORD)
variable "vnc_password" {
type = string
sensitive = true
}
variable "ingress_host" {
type = string
default = ""
}
# --- Provider ----------------------------------------------------------------
provider "kubernetes" {
config_path = "./kind_kubeconfig"
host = "https://local-k3s-platform:6443"
insecure = true
}
provider "helm" {
kubernetes {
config_path = "./kind_kubeconfig"
host = "https://local-k3s-platform:6443"
insecure = true
}
}
# --- MetaTrader 5 Deployment -------------------------------------------------
resource "helm_release" "mt5" {
name = "mt5"
chart = "./charts/mt5"
namespace = "trading"
create_namespace = true
# Erster Start installiert Wine + MT5 nach /config – das dauert ein paar Minuten
timeout = 900
wait = true
set {
name = "image.repository"
value = var.image_repository
}
set {
name = "image.tag"
value = var.app_version
}
set {
name = "auth.user"
value = var.vnc_user
}
set_sensitive {
name = "auth.password"
value = var.vnc_password
}
set {
name = "ingress.enabled"
value = var.ingress_host != "" ? "true" : "false"
}
set {
name = "ingress.host"
value = var.ingress_host
}
}

100
scripts/check-stack.sh Executable file
View file

@ -0,0 +1,100 @@
#!/bin/bash
# check-stack.sh – Statusprüfung DIESES Stacks (MT5, MCP, Trader, Telegram)
# Läuft im Stack-Ordner auf dem Server; Werte kommen aus deploy.conf.
#
# scripts/check-stack.sh ausführlich
# scripts/check-stack.sh -q nur Fehler/Warnungen (cron/Deploy)
# Exit-Code: 0 = ok, 1 = mindestens ein Fehler (Warnungen zählen nicht)
cd "$(dirname "$0")/.." || exit 1
source ./deploy.conf
QUIET=0; [ "${1:-}" = "-q" ] && QUIET=1
FAIL=0
SYMBOL="${CHECK_SYMBOL:-XAUUSD}"
BASE="http://127.0.0.1:$MCP_PORT"
if [ -t 1 ]; then G=$'\e[32m'; R=$'\e[31m'; Y=$'\e[33m'; B=$'\e[1m'; N=$'\e[0m'; else G=; R=; Y=; B=; N=; fi
pfx() { [ $QUIET -eq 1 ] && echo "[$STACK_KEY] "; }
ok() { [ $QUIET -eq 1 ] || echo " ${G}OK${N} $*"; }
warn() { echo " ${Y}WARN${N} $(pfx)$*"; }
fail() { echo " ${R}FAIL${N} $(pfx)$*"; FAIL=1; }
jget() { python3 -c "import json,sys
try: v=json.load(sys.stdin)
except Exception: print('${2:-}'); sys.exit()
for k in '$1'.split('.'):
v = v.get(k) if isinstance(v, dict) else None
print('${2:-}' if v is None else v)"; }
cid() { docker compose ps -q "$1" 2>/dev/null | head -1; }
[ $QUIET -eq 1 ] || echo "${B}== $STACK_NAME ==${N}"
# 1. Container
for svc in "$MT5_SVC" "$MCP_SVC" "$TRADER_SVC"; do
id=$(cid "$svc")
if [ -z "$id" ]; then fail "Dienst $svc: kein Container"; continue; fi
st=$(docker inspect -f '{{.State.Status}}{{if .State.Health}}/{{.State.Health.Status}}{{end}}' "$id")
case "$st" in running/healthy|running) ok "Dienst $svc: $st" ;; *) fail "Dienst $svc: $st" ;; esac
done
# 2. Bridge + Konto
H=$(curl -s -m 10 "$BASE/health")
[ "$(echo "$H" | jget connected)" = "True" ] && ok "MCP $BASE: Bridge verbunden" \
|| fail "MCP $BASE: keine Verbindung zu MT5 (${H:-keine Antwort})"
A=$(curl -s -m 15 "$BASE/account")
if [ -n "$(echo "$A" | jget login)" ]; then
ok "Konto $(echo "$A" | jget login) @ $(echo "$A" | jget server): Balance $(echo "$A" | jget balance)" \
"Equity $(echo "$A" | jget equity) $(echo "$A" | jget currency)"
else
fail "Konto: $(echo "$A" | jget detail "${A:-keine Antwort}")"
fi
# 3. Kurs
T=$(curl -s -m 15 "$BASE/tick/$SYMBOL")
if [ -n "$(echo "$T" | jget bid)" ]; then
ok "$SYMBOL → $(echo "$T" | jget symbol): bid $(echo "$T" | jget bid) ask $(echo "$T" | jget ask)"
else
fail "Kurs $SYMBOL: $(echo "$T" | jget detail "${T:-keine Antwort}")"
fi
# 4. Positionen
P=$(curl -s -m 15 "$BASE/positions" | jget count)
if [ -z "$P" ]; then fail "Positionen: keine Antwort"
elif [ "$P" -gt 0 ]; then warn "Offene Positionen: $P"
else ok "Offene Positionen: 0"; fi
# 5. Trader-Lebenszeichen (Docker-Zeitstempel = UTC, unabhängig von Zeitzonen)
TID=$(cid "$TRADER_SVC")
if [ -n "$TID" ]; then
LAST=$(docker logs --timestamps --since 10m "$TID" 2>&1 | grep -E "\[(LIVE|DRY)\] Balance" | tail -1)
if [ -n "$LAST" ]; then
AGE=$(( $(date +%s) - $(date -d "$(echo "$LAST" | awk '{print $1}')" +%s 2>/dev/null || echo 0) ))
MODE=$(echo "$LAST" | grep -oE "LIVE|DRY" | head -1)
[ "$AGE" -le 120 ] && ok "Trader lebt ($MODE), letztes Lebenszeichen vor ${AGE}s" \
|| fail "Trader: letztes Lebenszeichen vor ${AGE}s"
else
fail "Trader: keine Balance-Zeile in den letzten 10 min"
fi
# 6. Telegram
START=$(docker inspect -f '{{.State.StartedAt}}' "$TID")
CHAN=$(docker logs --since "$START" "$TID" 2>&1 | grep -oE "Active channels: [^|]*" | tail -1)
[ -n "$CHAN" ] && ok "Telegram: $CHAN" || fail "Telegram: keine 'Active channels'-Meldung seit Start"
ERRS=$(docker logs --since 60m "$TID" 2>&1 \
| grep -ciE "AUTH_KEY|EOFError|SessionPasswordNeeded|PhoneNumber|Traceback|disconnected|server closed")
[ "$ERRS" -gt 0 ] && warn "Telegram/Trader: $ERRS Fehlerzeile(n) in der letzten Stunde"
fi
if [ -f "$DIAG_FILE" ]; then
DAGE=$(( $(date +%s) - $(stat -c %Y "$DIAG_FILE") ))
[ "$DAGE" -le 900 ] && ok "Diagnose: vor ${DAGE}s geschrieben, $(jget total_messages < "$DIAG_FILE") Nachricht(en)" \
|| fail "Diagnose: seit ${DAGE}s nicht geschrieben ($DIAG_FILE)"
else
warn "Diagnose: $DIAG_FILE fehlt"
fi
[ $QUIET -eq 1 ] || { [ $FAIL -eq 0 ] && echo "${G}${B}Alles in Ordnung.${N}" || echo "${R}${B}Fehler – siehe oben.${N}"; }
exit $FAIL

52
scripts/deploy-local.sh Executable file
View file

@ -0,0 +1,52 @@
#!/bin/bash
# deploy-local.sh – Deploy DIESES Stacks, ausgeführt im Stack-Ordner auf dem Server.
# Wird von Terraform aufgerufen, funktioniert aber genauso von Hand:
#
# scripts/deploy-local.sh Sperre prüfen, bauen/starten, auf gesund warten
# scripts/deploy-local.sh --check-flat nur die Sperre prüfen (Terraform vor dem Kopieren)
# scripts/deploy-local.sh --force Sperre ignorieren
#
# Sperre: Der Trader hält offene Positionen nur im Speicher. Ein Neustart mitten
# im Trade = für diese Position kein BE und kein CANCEL mehr.
cd "$(dirname "$0")/.." || exit 1
source ./deploy.conf
check_flat() {
local n
n=$(curl -s -m 10 "http://127.0.0.1:$MCP_PORT/positions" \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["count"])' 2>/dev/null || echo "?")
if [ "$n" = "0" ]; then
echo "[$STACK_KEY] Keine offenen Positionen – Deploy erlaubt."
return 0
fi
# Stack (noch) nicht gestartet -> nichts, was verloren gehen kann
if [ "$n" = "?" ] && [ -z "$(docker compose ps -q "$TRADER_SVC" 2>/dev/null)" ]; then
echo "[$STACK_KEY] Trader läuft nicht – Deploy erlaubt."
return 0
fi
echo "[$STACK_KEY] ABBRUCH: $n offene Position(en) bzw. MCP nicht erreichbar."
echo " Warten bis flat, oder bewusst: scripts/deploy-local.sh --force"
return 1
}
case "${1:-}" in
--check-flat) check_flat; exit $? ;;
--force) echo "[$STACK_KEY] --force: Sperre übersprungen" ;;
*) check_flat || exit 1 ;;
esac
chmod +x scripts/*.sh 2>/dev/null
echo "[$STACK_KEY] docker compose up -d --build"
docker compose up -d --build || exit 1
echo "[$STACK_KEY] Warte auf gesunden Stack (max. 10 min)..."
for i in $(seq 1 20); do
if scripts/check-stack.sh -q >/dev/null 2>&1; then
scripts/check-stack.sh
exit 0
fi
sleep 30
done
scripts/check-stack.sh -q
exit 1