Panel to govern Nginx Proxy Manager domains, reconciling public and private DNS.
969
Web panel to govern the domains of Nginx Proxy Manager (NPM), distinguishing public and private domains and keeping them in sync with the system that resolves each type:
The app reads the domains from NPM, shows them in a table with their sync state and, visually, whether they are active in Cloudflare (public) or in the Mikrotik (private), and offers reconciliation actions when there are divergences.
Design and detailed decisions:
AGENTS.md (symlink:CLAUDE.md) andIMPLEMENTATION.md.
@astrojs/node adapter in standalone mode).@tailwindcss/vite, configured with @theme).prom-client) at /metrics, JSON logs to stdout collected
by Grafana Alloy → Loki, visualization in Grafana, and alerting via Alertmanager.Browser (Preact islands)
│ fetch()
▼
Astro SSR endpoints (src/pages/api/*.ts) ← all integration logic
│
├─ Providers (src/server/providers/)
│ ├─ npm.ts → Nginx Proxy Manager API
│ ├─ cloudflare.ts → Cloudflare API (public domains)
│ └─ mikrotik.ts → RouterOS 7 REST API (private domains)
│
├─ Reconciliation (src/server/…) → compares desired vs actual state
└─ Persistence (src/server/db/) → Postgres (metadata and desired state)
Source of truth: NPM is the domain list; our DB stores the type (public/private), the desired state, and the result of the last reconciliation. Network/integration logic runs only on the server.
The dev stack is self-contained: app with hot-reload + Postgres + full observability (Grafana, Prometheus, Loki, Alloy, Alertmanager).
npm run docker:dev
# same as: docker compose -f infra/dev/compose.yml up --build
Services exposed locally:
| Service | URL |
|---|---|
| App | http://localhost:4321 |
| Grafana | http://localhost:13000 |
| Prometheus | http://localhost:19090 |
| Loki | http://localhost:13100 |
| Postgres | localhost:15432 |
Requires a reachable Postgres and the environment variables in a root .env.
npm install
npm run db:migrate # apply Drizzle migrations
npm run dev # dev server at http://localhost:4321
In dev they are read from a root .env (do not commit). The DNS provider secrets
(Cloudflare/Mikrotik) are not passed via env: they are stored encrypted in the DB and edited from
the panel (Settings → DNS Providers).
DATABASE_URL=postgresql://user:pass@host:5432/db
NPM_BASE_URL=http://npm.lan:81
NPM_EMAIL=...
NPM_PASSWORD=...
SESSION_SECRET=... # signs the session cookie
SETTINGS_KEY=... # REQUIRED: encrypts (AES-256-GCM) the provider secrets in the DB
AUTH_ENABLED=true # false → disables login (LAN-only use)
Generate the access password hash with npm run auth:hash.
| Command | Action |
|---|---|
npm run dev | Dev server at localhost:4321 |
npm run build | Production build to ./dist/ |
npm run start | Run the build (node ./dist/server/entry.mjs) |
npm run typecheck | tsc --noEmit |
npm run format | Prettier --write over the whole repo |
npm run db:generate | Generate migrations from the schema (Drizzle Kit) |
npm run db:migrate | Apply migrations |
npm run db:studio | Open Drizzle Studio |
npm run auth:hash | Generate the argon2 hash of the access password |
npm run docker:dev | Full dev stack in Docker |
npm run docker:prod | Prod stack (usually via pi-infra) |
The app exposes /metrics (Prometheus) and /health. In the dev stack, the
proxy-control · Overview Grafana dashboard shows app status, domains by state, reconciliations,
and logs.
Note (dev only): the Astro dev server runs on Vite, which rejects requests with 403 when the
Hostheader is not inallowedHosts. Since Prometheus scrapes/metricsby the Docker service name (proxy-control), that host is explicitly allowed inastro.config.mjs(vite.server.allowedHosts). Without that entry,up{job="proxy-control"}is 0 and the metrics panels stay empty. Not applicable in prod (standalone build, no Vite).
Everything deployment-related lives in infra/ (shared multi-stage Node 24 Dockerfile). Two
environments: dev (self-contained) and prod (integrated with the pi-infra core, using its
Postgres/pgbouncer and Grafana). See details in AGENTS.md.
Prettier 3 (4 spaces, no ;, single quotes, trailingComma: all, printWidth 120) plus the structure
conventions described in AGENTS.md. Run npm run format after any change.
Content type
Image
Digest
sha256:7ef04b9fc…
Size
139 MB
Last updated
29 days ago
docker pull negrii/proxy-control