DEPLOY.md 7.9 KB

Deployment — EKS Support Relay

Target

Host acmycar, 18.168.156.244 (the Chatwoot host)
SSH ssh -i <maciek_priv> ubuntu@18.168.156.244
Path /home/ubuntu/eks_relay (a real git checkout of the Gogs repo)
Domain https://eks-relay.easyklima.com
Reverse proxy Traefik traefik-traefik-1, docker network web
DNS Cloudflare zone easyklima.com, proxied A record

The relay is co-located with Chatwoot rather than with Flowise: the Chatwoot webhook is the latency-sensitive hop (Chatwoot waits for the 202), while the Flowise → /tools/* calls happen inside an already long-running LLM turn.

Why easyklima.com and not easyklima.pl

eksupport.easyklima.com and botek.easyklima.com already live in the easyklima.com Cloudflare zone and terminate on the same Traefik. Putting the relay in the same zone keeps one certificate resolver, one WAF policy surface and one set of DNS conventions. easyklima.pl points at the shop's origin and would need separate Cloudflare and Traefik work for no benefit.

Note: the zone has a wildcard *.easyklima.com CNAME pointing at the shop origin. Without an explicit A record, eks-relay.easyklima.com resolves to the shop, not to Chatwoot. The explicit record below is mandatory, not optional.

1. DNS (Cloudflare)

Create a proxied A record matching the eksupport pattern:

Type Name Content Proxy
A eks-relay 18.168.156.244 Proxied (orange cloud)
# CLOUDFLARE_TOKEN comes from the local secrets file — never inline it.
ZONE=3ff752c975f4bfc6286f0f357f306c2f
curl -s -X POST "https://api.cloudflare.com/client/v4/zones/$ZONE/dns_records" \
  -H "Authorization: Bearer $CLOUDFLARE_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"type":"A","name":"eks-relay","content":"18.168.156.244","proxied":true,"ttl":1}'

Verify:

dig +short eks-relay.easyklima.com    # Cloudflare anycast IPs

Cloudflare WAF — two rules are required

Cloudflare's managed challenge blocks server-to-server calls in both directions. Two narrow skip rules are in place; neither is a zone-wide bypass, and both endpoints keep their own bearer authentication.

1. Inbound — zone easyklima.com (Chatwoot and Flowise reaching the relay). Without it every request to the relay gets a 403 challenge page:

(http.host eq "eks-relay.easyklima.com" and
 (starts_with(http.request.uri.path, "/webhooks/") or
  starts_with(http.request.uri.path, "/tools/") or
  http.request.uri.path eq "/health" or
  http.request.uri.path eq "/ready"))

2. Outbound — zone easyklima.pl (the relay reaching the shop's REST APIs). Without it every WooCommerce and eksrelay/v1 call gets a 403 challenge, so every shop-backed tool fails:

(ip.src eq 18.168.156.244 and starts_with(http.request.uri.path, "/wp-json/"))

Both use action: skip over phases http_ratelimit, http_request_firewall_managed, http_request_sbfm and products uaBlock, bic, hot, securityLevel, rateLimit, waf — matching the convention of the pre-existing rules in these zones.

If the relay ever moves to another host, rule 2's source IP must move with it.

2. Server checkout

ssh -i <key> ubuntu@18.168.156.244
git clone https://gogs.tenteg.es/aiac/eks_relay.git /home/ubuntu/eks_relay
cd /home/ubuntu/eks_relay
git config user.name  "Maciek"
git config user.email "maciek@aiac.local"

It is a normal checkout, so git pull and git push both work on the server (push verified by tagging the deployed commit deployed-acmycar-20260820).

Anonymous read works, so clone and pull need no credentials. push does; supply them through a temporary GIT_ASKPASS helper and delete it afterwards — never store credentials on the server or put them in the remote URL:

umask 077
cat > /tmp/.ap.sh <<'EOF'
#!/bin/sh
case "$1" in
  *Username*) printf "%s" "$GIT_USER" ;;
  *Password*) printf "%s" "$GIT_PASS" ;;
esac
EOF
chmod 700 /tmp/.ap.sh
GIT_USER=... GIT_PASS=... GIT_ASKPASS=/tmp/.ap.sh GIT_TERMINAL_PROMPT=0 \
  git push origin master
rm -f /tmp/.ap.sh

3. Environment

cp .env.example .env
chmod 600 .env
# fill in the real values (migrated from the PHP relay's .env.php)

Required before first start: CHATWOOT_API_TOKEN, FLOWISE_PREDICT_URL, FLOWISE_API_KEY, WOOCOMMERCE_CONSUMER_KEY, WOOCOMMERCE_CONSUMER_SECRET, RELAY_SHARED_SECRET, ADMIN_TOKEN.

RELAY_SHARED_SECRET must stay identical to the value configured in Flowise as $vars.relay_shared_secret, otherwise every tool call returns 401.

4. Start

cd /home/ubuntu/eks_relay
sudo docker compose config          # validate
sudo docker compose up -d --build
sudo docker compose ps
sudo docker compose logs -f relay

The web network already exists (Traefik owns it); the compose file joins it as external.

5. Verify

curl -s https://eks-relay.easyklima.com/health | jq
curl -s https://eks-relay.easyklima.com/ready  | jq

Then run SMOKE_TEST.md.

6. Updating

cd /home/ubuntu/eks_relay
git pull
sudo docker compose restart relay

No rebuild: the repo is bind-mounted and the entrypoint reinstalls dependencies only when package-lock.json changed, regenerates the Prisma client only when prisma/schema.prisma changed, and always applies pending migrations.

Rebuild (--build) only when the Dockerfile or its base image changes.

restart does not re-read .env. Docker Compose bakes env_file values into the container at create time, so after editing .env you need sudo docker compose up -d (which recreates the container), not restart. For code-only changes restart is the faster path.

Live-reload while debugging

Set RELAY_MODE=dev in .env and run sudo docker compose up -d. The container then runs tsx watch, so saving a file reloads the process in place — no restart and no rebuild. Set it back to prod and up -d again when done; prod mode compiles once and runs dist/, which is what should serve real traffic.

Read-only ops panel

Daily operational visibility should use the built-in read-only panel:

https://eks-relay.easyklima.com/ops

The HTML shell contains no data and no token. It asks for ADMIN_TOKEN, stores it only in browser sessionStorage, and then calls /admin/* with Authorization: Bearer ....

The panel shows:

  • /health and /ready,
  • recent audit events,
  • queue/jobs including dead jobs,
  • processed messages/idempotency records,
  • tickets with last event and skipped-message counters.

/admin/* remains token-protected; without a bearer token it returns 401.

Prisma Studio

SQLite at /home/ubuntu/eks_relay/data/eks_relay.db, bind-mounted into the container at /app/data. It is gitignored.

Back it up before a schema change:

cp data/eks_relay.db data/eks_relay.db.$(date +%F-%H%M)

Browse it via Prisma Studio over an SSH tunnel — see the README. Studio is never routed through Traefik.

Chatwoot webhook

Point the Chatwoot bot/webhook at:

https://eks-relay.easyklima.com/webhooks/chatwoot

Keep the old PHP URL noted until the Node relay has passed a real e-mail test; switching back is a one-field change in the Chatwoot panel.

Known follow-ups

  • wp-plugins/eksrelay_api.php is unauthenticated. Its routes use permission_callback => __return_true. The relay already sends Authorization: Bearer <WP_STORE_API_SECRET> when that variable is set; enforcing it in the plugin is a separate, coordinated change on the shop's WordPress.
  • npm audit reports a high-severity advisory in deepmerge-ts, reached through @prisma/configprisma. That is the Prisma CLI dependency chain (build/migration time), not the runtime @prisma/client used to serve requests. The only offered fix downgrades Prisma to 6.12; the advisory is therefore accepted and tracked rather than force-fixed.