mirror of
https://github.com/tale/headplane.git
synced 2026-08-25 20:06:48 +00:00
feat: automate health check with a written file
This commit is contained in:
@@ -42,10 +42,17 @@ if (certPath || keyPath) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Read by the bundled Docker healthcheck. Includes the basename
|
||||||
|
// (`__PREFIX__`) so the Go binary can stay completely dumb — just
|
||||||
|
// GET whatever URL is in this file. `/tmp` is writable in
|
||||||
|
// distroless and in every dev shell we care about.
|
||||||
|
const healthURL = `${tls ? "https" : "http"}://127.0.0.1:${config.server.port}${__PREFIX__}/healthz`;
|
||||||
|
|
||||||
startHttpServer({
|
startHttpServer({
|
||||||
host: config.server.host,
|
host: config.server.host,
|
||||||
port: config.server.port,
|
port: config.server.port,
|
||||||
tls,
|
tls,
|
||||||
|
listenFile: { path: "/tmp/headplane-listen", url: healthURL },
|
||||||
listener: composeListener({
|
listener: composeListener({
|
||||||
basename: __PREFIX__,
|
basename: __PREFIX__,
|
||||||
staticRoot: clientDir,
|
staticRoot: clientDir,
|
||||||
|
|||||||
@@ -5,27 +5,37 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
// hp_healthcheck pings the Headplane /healthz endpoint. It's invoked
|
// hp_healthcheck pings the Headplane healthz endpoint. It's invoked
|
||||||
// from the Docker HEALTHCHECK directive, so it must work whether the
|
// from the Docker HEALTHCHECK directive inside the same container
|
||||||
// container is serving plain HTTP or has TLS termination enabled.
|
// as Headplane itself.
|
||||||
//
|
//
|
||||||
// Configuration (via env, all optional):
|
// To stay fully zero-config, Headplane writes the exact URL the
|
||||||
// - HEADPLANE_HEALTHCHECK_URL full URL, takes precedence over the
|
// healthcheck should hit — scheme, port, and basename included — to
|
||||||
// pieces below (default: http://localhost:3000/admin/healthz)
|
// listenFile when it starts accepting connections (see
|
||||||
// - HEADPLANE_HEALTHCHECK_TLS "true" to use https://
|
// runtime/http.ts and app/server/main.ts). This binary just reads
|
||||||
// - HEADPLANE_HEALTHCHECK_HOST default: localhost
|
// that file and GETs the URL verbatim. No env vars, no YAML
|
||||||
// - HEADPLANE_HEALTHCHECK_PORT default: 3000
|
// parsing, no path-joining, no compile-time knowledge of the
|
||||||
// - HEADPLANE_HEALTHCHECK_PATH default: /admin/healthz
|
// basename.
|
||||||
|
//
|
||||||
|
// If the file is missing (e.g. the server hasn't finished booting
|
||||||
|
// on the very first probe, or this is an old image being run with
|
||||||
|
// a new healthcheck) we fall back to the historical default.
|
||||||
|
const (
|
||||||
|
listenFile = "/tmp/headplane-listen"
|
||||||
|
defaultURL = "http://localhost:3000/admin/healthz"
|
||||||
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
url := healthcheckURL()
|
url := readListenFile()
|
||||||
|
|
||||||
client := http.Client{
|
client := http.Client{
|
||||||
Timeout: 5 * time.Second,
|
Timeout: 5 * time.Second,
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
// We just care that the server is alive and with us
|
// Self-signed certs are normal for in-process TLS termination
|
||||||
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -43,33 +53,16 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fmt.Println("Health check passed.")
|
fmt.Println("Health check passed.")
|
||||||
os.Exit(0)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthcheckURL() string {
|
func readListenFile() string {
|
||||||
if v := os.Getenv("HEADPLANE_HEALTHCHECK_URL"); v != "" {
|
data, err := os.ReadFile(listenFile)
|
||||||
return v
|
if err != nil {
|
||||||
|
return defaultURL
|
||||||
}
|
}
|
||||||
|
url := strings.TrimSpace(string(data))
|
||||||
scheme := "http"
|
if url == "" {
|
||||||
if os.Getenv("HEADPLANE_HEALTHCHECK_TLS") == "true" {
|
return defaultURL
|
||||||
scheme = "https"
|
|
||||||
}
|
}
|
||||||
|
return url
|
||||||
host := os.Getenv("HEADPLANE_HEALTHCHECK_HOST")
|
|
||||||
if host == "" {
|
|
||||||
host = "localhost"
|
|
||||||
}
|
|
||||||
|
|
||||||
port := os.Getenv("HEADPLANE_HEALTHCHECK_PORT")
|
|
||||||
if port == "" {
|
|
||||||
port = "3000"
|
|
||||||
}
|
|
||||||
|
|
||||||
path := os.Getenv("HEADPLANE_HEALTHCHECK_PATH")
|
|
||||||
if path == "" {
|
|
||||||
path = "/admin/healthz"
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Sprintf("%s://%s:%s%s", scheme, host, port, path)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -113,12 +113,10 @@ For most deployments we still recommend terminating TLS at a reverse proxy
|
|||||||
and other services. Built-in TLS is meant for the simpler "Headplane on a
|
and other services. Built-in TLS is meant for the simpler "Headplane on a
|
||||||
single box" scenarios.
|
single box" scenarios.
|
||||||
|
|
||||||
If you're running Headplane in Docker with TLS enabled, the bundled
|
The bundled Docker healthcheck picks up the right scheme and port
|
||||||
healthcheck binary needs to know it should use HTTPS. Set the following
|
automatically — Headplane writes its loopback URL to `/tmp/headplane-listen`
|
||||||
environment variables on the container:
|
when it starts, and the healthcheck reads it from there. No extra environment
|
||||||
|
variables, no duplicated config.
|
||||||
- `HEADPLANE_HEALTHCHECK_TLS=true`
|
|
||||||
- `HEADPLANE_HEALTHCHECK_PORT` (if you've changed it from the default `3000`)
|
|
||||||
|
|
||||||
## Reverse Proxying
|
## Reverse Proxying
|
||||||
|
|
||||||
|
|||||||
+16
-1
@@ -4,7 +4,7 @@
|
|||||||
// React Router request listener from `@react-router/node`) and serves
|
// React Router request listener from `@react-router/node`) and serves
|
||||||
// static assets out of a directory.
|
// static assets out of a directory.
|
||||||
import { createReadStream } from "node:fs";
|
import { createReadStream } from "node:fs";
|
||||||
import { stat } from "node:fs/promises";
|
import { stat, writeFile } from "node:fs/promises";
|
||||||
import {
|
import {
|
||||||
type IncomingMessage,
|
type IncomingMessage,
|
||||||
type RequestListener,
|
type RequestListener,
|
||||||
@@ -172,6 +172,14 @@ export interface StartOptions {
|
|||||||
listener: RequestListener;
|
listener: RequestListener;
|
||||||
tls?: HttpsServerOptions;
|
tls?: HttpsServerOptions;
|
||||||
logger?: Logger;
|
logger?: Logger;
|
||||||
|
/**
|
||||||
|
* If set, writes `url` to `path` once the server is accepting
|
||||||
|
* connections. Used by the bundled Docker healthcheck binary to
|
||||||
|
* discover the right scheme, port, and basename without any
|
||||||
|
* duplicate configuration. The caller assembles the URL so that
|
||||||
|
* the runtime stays generic.
|
||||||
|
*/
|
||||||
|
listenFile?: { path: string; url: string };
|
||||||
/**
|
/**
|
||||||
* Optional async hook invoked on SIGINT/SIGTERM after the HTTP
|
* Optional async hook invoked on SIGINT/SIGTERM after the HTTP
|
||||||
* server stops accepting new connections but before the process
|
* server stops accepting new connections but before the process
|
||||||
@@ -194,6 +202,13 @@ export function startHttpServer(opts: StartOptions): Server {
|
|||||||
server.listen(opts.port, opts.host, () => {
|
server.listen(opts.port, opts.host, () => {
|
||||||
const proto = opts.tls ? "https" : "http";
|
const proto = opts.tls ? "https" : "http";
|
||||||
log.info("Listening on %s://%s:%s", proto, opts.host, opts.port);
|
log.info("Listening on %s://%s:%s", proto, opts.host, opts.port);
|
||||||
|
|
||||||
|
if (opts.listenFile) {
|
||||||
|
const { path, url } = opts.listenFile;
|
||||||
|
writeFile(path, `${url}\n`, "utf8").catch((err) => {
|
||||||
|
log.error("Failed to write listen file %s: %s", path, err);
|
||||||
|
});
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const shutdown = async (signal: string) => {
|
const shutdown = async (signal: string) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user