Portainer stack docs and a real container healthcheck, slice 4 of M4
The README deploy section documents the GHCR image, the compose stack that joins Caddy's external network, and that the Caddyfile entry is managed by hand on the server. distroless has no shell, so the healthcheck is the binary itself: a new -check flag probes /healthz and exits 0 or 1, and the Dockerfile bakes it in as HEALTHCHECK. Verified locally: the container reports healthy.
This commit is contained in:
parent
51af5acf6a
commit
05b01348d5
3 changed files with 79 additions and 2 deletions
|
|
@ -9,9 +9,11 @@ import (
|
|||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/JustinZeus/spreadlab/internal/api"
|
||||
"github.com/JustinZeus/spreadlab/internal/engine"
|
||||
|
|
@ -21,6 +23,7 @@ import (
|
|||
func main() {
|
||||
addr := flag.String("addr", "localhost:8080", "address to serve the API on")
|
||||
table := flag.Bool("table", false, "print the three-scenario comparison and exit")
|
||||
check := flag.Bool("check", false, "probe a running server's /healthz and exit 0 or 1")
|
||||
flag.Parse()
|
||||
|
||||
if *table {
|
||||
|
|
@ -31,6 +34,16 @@ func main() {
|
|||
return
|
||||
}
|
||||
|
||||
// The runtime image has no shell or curl, so the compose healthcheck
|
||||
// runs this same binary against the server instance.
|
||||
if *check {
|
||||
if err := probeHealthz(*addr); err != nil {
|
||||
fmt.Fprintln(os.Stderr, "spreadlab:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// One mux, one origin: the longer /api/ pattern wins over / for API
|
||||
// calls, everything else falls through to the embedded frontend.
|
||||
mux := http.NewServeMux()
|
||||
|
|
@ -46,6 +59,31 @@ func main() {
|
|||
}
|
||||
}
|
||||
|
||||
// probeHealthz asks a running server whether it is healthy. The -addr
|
||||
// flag doubles as the target, so the healthcheck and the server agree
|
||||
// on the port by using the same default. A "localhost:8080" style addr
|
||||
// (or ":8080", where the host part is empty) becomes a loopback URL.
|
||||
func probeHealthz(addr string) error {
|
||||
host, port, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid addr %q: %w", addr, err)
|
||||
}
|
||||
if host == "" {
|
||||
host = "localhost"
|
||||
}
|
||||
|
||||
client := http.Client{Timeout: 3 * time.Second}
|
||||
response, err := client.Get(fmt.Sprintf("http://%s/healthz", net.JoinHostPort(host, port)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode >= 400 {
|
||||
return fmt.Errorf("healthz answered %s", response.Status)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// run executes the three-scenario comparison in the default world and
|
||||
// writes the table to out. Formatting is pure string building; the single
|
||||
// write at the end is the only error to handle.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue