Route GitLab stats and health checks through a configurable proxy

This commit is contained in:
2026-09-07 09:51:47 +02:00
parent dfe246b7ca
commit e41e2f928f
5 changed files with 176 additions and 39 deletions
+1
View File
@@ -54,6 +54,7 @@ automatically. Non-secret settings go directly in `docker-compose.yml`:
| `OPENWEBUI_TOKEN` | Open WebUI API key — enables the prompt line + model list (**set in `.env`**) |
| `GITEA_TOKEN` | Gitea API token — private repo stats (public repos work without it) |
| `GITLAB_TOKEN` | GitLab API token — private project stats (public projects work without it) |
| `GITLAB_PROXY` | Forward proxy (HTTP CONNECT) for all GitLab requests — git stats **and** the online badge (**set in `.env`**). Change the URL when the proxy stack moves; unset = direct connection. With the proxy on the docker host use `http://host.docker.internal:8118` (the container resolves `host.docker.internal` via `extra_hosts`)
| `GITEA_URL` / `GITLAB_URL` / `NETBIRD_URL` / `OPENWEBUI_URL` | Override the default service URLs |
| `GPU_STATS_URL` | GPU stats upstream (default: the `gpu-stats` compose service) |
| `OLLAMA_URL` | Ollama instance for loaded-model stats (default: `http://100.103.83.12:11435`) |
+94
View File
@@ -0,0 +1,94 @@
// ---------------------------------------------------------------------------
// Outbound proxy support for the container backend scripts.
//
// Node's core http/https modules ignore proxy env vars, so requests that
// should go through a forward proxy are tunneled manually via HTTP CONNECT.
// Currently used for GitLab access (see GITLAB_PROXY below).
//
// Configure in .env / docker-compose.yml (no rebuild needed, container
// restart is enough):
// GITLAB_PROXY=http://127.0.0.1:8118 (updater run on the host)
// GITLAB_PROXY=http://host.docker.internal:8118 (proxy on the docker host)
// GITLAB_PROXY=http://10.0.0.5:8118 (proxy moved elsewhere)
// Empty/unset disables proxying (direct connection).
// ---------------------------------------------------------------------------
const http = require("http");
const tls = require("tls");
const { URL } = require("url");
const GITLAB_PROXY = process.env.GITLAB_PROXY || "";
const GITLAB_HOST = (() => {
try {
return new URL(process.env.GITLAB_URL || "https://gitlab.ixsol.wien").hostname;
} catch {
return "gitlab.ixsol.wien";
}
})();
// Returns the proxy URL to use for a given target URL, or null for direct.
function proxyFor(url) {
if (!GITLAB_PROXY) return null;
try {
if (new URL(url).hostname === GITLAB_HOST) return GITLAB_PROXY;
} catch {
// unparseable URL — no proxy
}
return null;
}
// Opens a CONNECT tunnel through the proxy and resolves to a socket usable
// as the connection for a request to targetUrl: a TLS socket for https
// targets, the raw tunnel socket otherwise.
function connectThroughProxy(proxyUrl, targetUrl, timeoutMs, tlsOptions = {}) {
return new Promise((resolve, reject) => {
const target = new URL(targetUrl);
const proxy = new URL(proxyUrl);
const port = target.port || (target.protocol === "https:" ? "443" : "80");
const req = http.request({
host: proxy.hostname,
port: proxy.port || 80,
method: "CONNECT",
path: `${target.hostname}:${port}`,
headers: { Host: `${target.hostname}:${port}` },
timeout: timeoutMs || 10000,
});
req.on("connect", (res, socket) => {
if (res.statusCode !== 200) {
socket.destroy();
reject(new Error(`Proxy CONNECT to ${target.hostname} failed: HTTP ${res.statusCode}`));
return;
}
resolve(
target.protocol === "https:"
? tls.connect({ socket, servername: target.hostname, ...tlsOptions })
: socket
);
});
req.on("timeout", () => {
req.destroy();
reject(new Error(`Proxy CONNECT to ${target.hostname} timed out`));
});
req.on("error", reject);
req.end();
});
}
// Request options for client.request() that route the request through the
// proxy: https via the CONNECT tunnel (caller passes the socket returned by
// connectThroughProxy), plain http via an absolute-form request URI.
function proxiedRequestOptions(proxyUrl, targetUrl, socket) {
const u = new URL(targetUrl);
if (u.protocol === "https:") {
return { agent: false, createConnection: () => socket };
}
const p = new URL(proxyUrl);
return {
host: p.hostname,
port: p.port || 80,
path: u.href,
headers: { Host: u.host },
};
}
module.exports = { proxyFor, connectThroughProxy, proxiedRequestOptions };
+24 -8
View File
@@ -13,6 +13,7 @@ const https = require("https");
const fs = require("fs");
const path = require("path");
const { URL } = require("url");
const { proxyFor, connectThroughProxy, proxiedRequestOptions } = require("./proxy");
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/data.json";
const TZ = process.env.TZ || "UTC";
@@ -41,18 +42,33 @@ const GITLAB_TOKEN = process.env.GITLAB_TOKEN || "";
const OPENWEBUI_URL = process.env.OPENWEBUI_URL || "http://100.103.83.12:3000";
const OPENWEBUI_TOKEN = process.env.OPENWEBUI_TOKEN || "";
function request(url, options = {}) {
// Central outbound request helper: honors the outbound proxy (GITLAB_PROXY)
// for the configured host — see api/proxy.js.
async function request(url, options = {}) {
const u = new URL(url);
const client = u.protocol === "https:" ? https : http;
const requestOptions = {
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 15000,
};
const proxy = proxyFor(url);
if (proxy) {
// https is tunneled through the proxy via CONNECT; plain http uses an
// absolute-form request URI addressed to the proxy.
let socket = null;
if (u.protocol === "https:") {
socket = await connectThroughProxy(proxy, url, requestOptions.timeout);
}
Object.assign(requestOptions, proxiedRequestOptions(proxy, url, socket));
}
return new Promise((resolve, reject) => {
const start = Date.now();
const u = new URL(url);
const client = u.protocol === "https:" ? https : http;
const req = client.request(
u,
{
method: options.method || "GET",
headers: options.headers || {},
timeout: options.timeout || 15000,
},
requestOptions,
(res) => {
let body = "";
res.setEncoding("utf8");
+48 -31
View File
@@ -15,6 +15,7 @@ const https = require("https");
const fs = require("fs");
const path = require("path");
const { URL } = require("url");
const { proxyFor, connectThroughProxy, proxiedRequestOptions } = require("./proxy");
const OUT_FILE = process.env.OUT_FILE || "/usr/share/nginx/html/health.json";
const TIMEOUT = parseInt(process.env.HEALTH_TIMEOUT || "5000", 10);
@@ -51,40 +52,56 @@ function checkUrl(url) {
resolve(result);
};
let req;
try {
const u = new URL(url);
const client = u.protocol === "https:" ? https : http;
req = client.request(
u,
{
(async () => {
let req;
try {
const u = new URL(url);
const client = u.protocol === "https:" ? https : http;
const options = {
method: "GET",
agent: u.protocol === "https:" ? httpsAgent : httpAgent,
timeout: TIMEOUT,
},
(res) => {
const code = res.statusCode;
res.destroy(); // response headers are all we need
done({
status: code >= 500 ? "error" : "online",
code,
latencyMs: Date.now() - started,
error: null,
});
}
);
} catch (e) {
return done({ status: "offline", code: null, latencyMs: null, error: e.message });
}
};
req.on("timeout", () => {
req.destroy();
done({ status: "offline", code: null, latencyMs: null, error: "Timeout" });
});
req.on("error", (e) => {
done({ status: "offline", code: null, latencyMs: null, error: e.message });
});
req.end();
// Honor the outbound proxy (GITLAB_PROXY) so the GitLab badge checks
// the same path the git stats actually take (see api/proxy.js).
const proxy = proxyFor(url);
if (proxy) {
let socket = null;
if (u.protocol === "https:") {
socket = await connectThroughProxy(proxy, url, TIMEOUT, { rejectUnauthorized: false });
}
Object.assign(options, proxiedRequestOptions(proxy, url, socket));
} else {
options.agent = u.protocol === "https:" ? httpsAgent : httpAgent;
}
req = client.request(
u,
options,
(res) => {
const code = res.statusCode;
res.destroy(); // response headers are all we need
done({
status: code >= 500 ? "error" : "online",
code,
latencyMs: Date.now() - started,
error: null,
});
}
);
} catch (e) {
return done({ status: "offline", code: null, latencyMs: null, error: e.message });
}
req.on("timeout", () => {
req.destroy();
done({ status: "offline", code: null, latencyMs: null, error: "Timeout" });
});
req.on("error", (e) => {
done({ status: "offline", code: null, latencyMs: null, error: e.message });
});
req.end();
})();
});
}
+9
View File
@@ -22,6 +22,11 @@ services:
# Uncomment and fill in to enable private git stats.
# - GITLAB_TOKEN=
# - NETBIRD_TOKEN=
# Forward proxy for GitLab API access + health checks (api/proxy.js).
# Set the URL in .env — change it there when the proxy stack moves
# (unset = direct connection). With the proxy on the docker host:
# GITLAB_PROXY=http://host.docker.internal:8118
- GITLAB_PROXY=${GITLAB_PROXY:-}
# GPU stats upstream; defaults to the gpu-stats service below.
# To run gpu_stats.py on the host instead, use:
# http://host.docker.internal:9101/stats
@@ -29,6 +34,10 @@ services:
# - GPU_STATS_URL=http://gpu-stats:9101/stats
# Ollama instance for the loaded-model stats in the GPU widget.
# - OLLAMA_URL=http://100.103.83.12:11435
extra_hosts:
# Alias for the docker host, so container services can reach host
# processes (e.g. the proxy behind GITLAB_PROXY) by a stable name.
- "host.docker.internal:host-gateway"
volumes:
# Persists the Outlook OAuth refresh token across container updates.
- outlook-tokens:/opt/dashboard/tokens