mirror of
https://github.com/taylanbakircioglu/haproxy-openmanager.git
synced 2026-09-11 21:38:55 +00:00
bb774141d4
Correcting a wrong ACME challenge backend was impossible without a shell, and
even with one the correction did not reach the nodes.
The mint gate only fired when `acme_enabled` flipped. `acme_backend_url` was
written to the DB and minted nothing, so Apply answered "No pending changes to
apply" and the nodes kept the old address forever. It is now decided by
comparing the rendered `server _acme_mgmt` line against the active version —
the one line that answers "would the nodes talk to a different address?".
Comparing whole configs would flag every unrelated pending edit.
The field had no UI at all. Added to the cluster form with validation that
mirrors the backend rules, and keyed on `model_fields_set` so clearing it
reverts to the global setting — with a plain `is not None` test an empty box is
indistinguishable from "not submitted", so a value could never be removed.
Validation is asymmetric on purpose (utils/acme_backend_url):
- at the write boundary, reject what cannot express a reachable target —
including the two silent traps: a scheme-less value became `localhost`, and
an out-of-range port raised inside the generator and destroyed the config
- at render time, never reject. The shipped defaults are themselves loopback,
so refusing to render would make every acme_enabled cluster unappliable,
including for changes unrelated to ACME. Problems are logged and surfaced.
The port-less default stays 8080 rather than moving to HTTP's 80: the bundled
compose publishes nginx on 8080, so installs relying on it work today and the
first sign of breaking them would be the unattended renewal loop months later.
The omission is warned about instead.
RFC1918 is allowed and is usually the right answer here, and no DNS resolution
is performed — both deliberate departures from utils/ssrf_guard, whose policy
is the opposite of what this address needs. What the management host can
resolve says nothing about what the HAProxy node can reach.
Diagnostics stop reporting success on a dead path:
- check_port80 uses GET instead of HEAD and classifies the body. A proxy that
has lost its /.well-known/acme-challenge/ location serves its SPA with HTTP
200, which `status in (200, 404)` accepted as healthy. Warnings also surface
when other domains pass, which previously hid the most diagnostic outcome.
- check_routing filters `mode`, joins `acme_enabled` and reads the APPLIED
config instead of counting database rows, and reports a loopback target.
- every new condition is `warn`, never `fail`: the site wizard blocks submit on
any fail, so a new failing condition would lock every install on upgrade day.
Also: normalise `frontends.mode` once per frontend. It is nullable, and the
raw value was interpolated into `mode {}`, emitting a literal `mode None` that
HAProxy rejects — taking down the whole cluster config. The ACME gate and the
backend-mode check now read the same normalised value.
And stop hardcoding PUBLIC_URL / MANAGEMENT_BASE_URL in docker-compose, which
silently ignored the operator's .env and made the wrong default load-bearing.
177 lines
5.5 KiB
YAML
177 lines
5.5 KiB
YAML
services:
|
|
# PostgreSQL Database
|
|
postgres:
|
|
image: postgres:15-alpine
|
|
container_name: haproxy-openmanager-db
|
|
environment:
|
|
POSTGRES_DB: haproxy_openmanager
|
|
POSTGRES_USER: haproxy_user
|
|
POSTGRES_PASSWORD: haproxy_pass
|
|
PGDATA: /var/lib/postgresql/data/pgdata
|
|
volumes:
|
|
- postgres_data:/var/lib/postgresql/data
|
|
ports:
|
|
- "5432:5432"
|
|
networks:
|
|
- haproxy-network
|
|
healthcheck:
|
|
test: ["CMD-SHELL", "pg_isready -U haproxy_user -d haproxy_openmanager"]
|
|
interval: 10s
|
|
timeout: 5s
|
|
retries: 5
|
|
|
|
# Redis Cache
|
|
redis:
|
|
image: redis:8.8.0-alpine
|
|
container_name: haproxy-openmanager-redis
|
|
command: redis-server --maxmemory 2gb --maxmemory-policy volatile-lru --save ""
|
|
ports:
|
|
- "6379:6379"
|
|
networks:
|
|
- haproxy-network
|
|
healthcheck:
|
|
test: ["CMD", "redis-cli", "ping"]
|
|
interval: 10s
|
|
timeout: 3s
|
|
retries: 5
|
|
|
|
# Backend API - pulls from Docker Hub by default
|
|
# To build locally instead, uncomment the build section and run: docker compose build backend
|
|
backend:
|
|
image: taylanbakircioglu/haproxy-openmanager-backend:latest
|
|
# build:
|
|
# context: ./backend
|
|
# dockerfile: Dockerfile
|
|
container_name: haproxy-openmanager-backend
|
|
environment:
|
|
- DATABASE_URL=postgresql://haproxy_user:haproxy_pass@postgres:5432/haproxy_openmanager
|
|
- REDIS_URL=redis://redis:6379
|
|
- SECRET_KEY=your-secret-key-change-this-in-production
|
|
- DEBUG=False
|
|
- LOG_LEVEL=INFO
|
|
# Interpolated, not hardcoded: these were literals, so a value set in the
|
|
# host environment or .env was silently ignored and every install kept the
|
|
# localhost default. That default is also what the ACME challenge backend
|
|
# falls back to, and HAProxy resolves it ON THE HAPROXY NODE — so on any
|
|
# deployment where HAProxy is not this machine, it points at the wrong box.
|
|
- PUBLIC_URL=${PUBLIC_URL:-http://localhost:8080}
|
|
- MANAGEMENT_BASE_URL=${MANAGEMENT_BASE_URL:-http://localhost:8080}
|
|
# Empty when unset on the host: the image CMD then falls back to
|
|
# WEB_CONCURRENCY (uvicorn's native env) and finally to 1.
|
|
- UVICORN_WORKERS=${UVICORN_WORKERS:-}
|
|
volumes:
|
|
- haproxy_configs:/etc/haproxy
|
|
# NOT published to the host: the API is reachable only through the nginx
|
|
# service (host :8080). Host port 8000 is therefore NOT this API — on a box
|
|
# running Portainer it is Portainer's edge tunnel, which answers 404 and looks
|
|
# deceptively like a working challenge endpoint.
|
|
expose:
|
|
- "8000"
|
|
depends_on:
|
|
postgres:
|
|
condition: service_healthy
|
|
redis:
|
|
condition: service_healthy
|
|
networks:
|
|
- haproxy-network
|
|
healthcheck:
|
|
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8000/api/health')"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
start_period: 40s
|
|
|
|
# Frontend React App - pulls from Docker Hub by default
|
|
# To build locally instead, uncomment the build section and run: docker compose build frontend
|
|
#
|
|
# NOTE: REACT_APP_* env vars are BUILD-time only for Create-React-App. The
|
|
# runtime container (serve -s build) does NOT consume them. The frontend
|
|
# uses same-origin (window.location) for /api/* and is routed by the nginx
|
|
# service below to the backend container. No env vars are required here.
|
|
frontend:
|
|
image: taylanbakircioglu/haproxy-openmanager-frontend:latest
|
|
# build:
|
|
# context: ./frontend
|
|
# dockerfile: Dockerfile
|
|
container_name: haproxy-openmanager-frontend
|
|
expose:
|
|
- "3000"
|
|
depends_on:
|
|
- backend
|
|
networks:
|
|
- haproxy-network
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
|
|
# Nginx Reverse Proxy
|
|
nginx:
|
|
# Pinned to a patched release for the nginx "poolslip" advisory
|
|
# (mainline <=1.31.0 affected; fixed in mainline 1.31.1+ / stable 1.30.2+).
|
|
image: nginx:1.31.1-alpine
|
|
container_name: haproxy-openmanager-nginx
|
|
volumes:
|
|
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
ports:
|
|
- "8080:8080"
|
|
depends_on:
|
|
- frontend
|
|
- backend
|
|
networks:
|
|
- haproxy-network
|
|
healthcheck:
|
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/health"]
|
|
interval: 30s
|
|
timeout: 10s
|
|
retries: 3
|
|
|
|
# HAProxy Instance (for testing)
|
|
haproxy:
|
|
image: haproxy:2.8-alpine
|
|
container_name: haproxy-instance
|
|
volumes:
|
|
- ./haproxy/haproxy-simple.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
|
ports:
|
|
- "80:80"
|
|
- "8404:8404"
|
|
networks:
|
|
- haproxy-network
|
|
|
|
# Remote HAProxy Instance 1 (Production)
|
|
haproxy-remote1:
|
|
image: haproxy:2.8-alpine
|
|
container_name: haproxy-remote1
|
|
volumes:
|
|
- ./haproxy/haproxy-simple.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
|
ports:
|
|
- "8001:80"
|
|
- "8405:8404"
|
|
networks:
|
|
- haproxy-network
|
|
environment:
|
|
- HAPROXY_ENV=production
|
|
|
|
# Remote HAProxy Instance 2 (Staging)
|
|
haproxy-remote2:
|
|
image: haproxy:2.8-alpine
|
|
container_name: haproxy-remote2
|
|
volumes:
|
|
- ./haproxy/haproxy-simple.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
|
|
ports:
|
|
- "8002:80"
|
|
- "8406:8404"
|
|
networks:
|
|
- haproxy-network
|
|
environment:
|
|
- HAPROXY_ENV=staging
|
|
|
|
volumes:
|
|
postgres_data:
|
|
haproxy_configs:
|
|
|
|
networks:
|
|
haproxy-network:
|
|
driver: bridge
|