Add Dockerfile, Compose stacks, and /healthz endpoint

Dockerfile is multi-stage:
- build: golang:1.25-bookworm, CGO_ENABLED=0 (modernc.org/sqlite
  is pure-Go) + -trimpath + -ldflags "-s -w" so the resulting
  binary is small and reproducible-ish.
- runtime: gcr.io/distroless/static:nonroot, ~2 MB. Runs as uid
  65532. /data and /var/lib/librenotes are declared volumes so
  per-tenant notes and the SQLite database survive container
  restarts.

healthcheck subcommand: distroless static has no shell or
wget/curl, so /healthz is reachable but no client to call it. A
new "librenotes healthcheck" subcommand uses net/http to GET
$LIBRENOTES_HEALTHCHECK_URL (default 127.0.0.1:8080/healthz) and
exits non-zero on failure. Both compose files invoke it from the
HEALTHCHECK directive.

httpapi adds a tiny GET /healthz that returns {"status":"ok"}
(no DB ping yet — added when readiness probes need it).

docker-compose.yml: dev stack on :8080 with named volumes and a
LogMailer; everything via env vars, JWT secret defaulted to a
dev value.
docker-compose.prod.yml: layered overrides — pulls a registry
image, expects LIBRENOTES_* env, sets memory limits and JSON-
file log rotation.

Closes #25.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-28 22:47:42 +02:00
co-authored by Claude Opus 4.7
parent d71db09d45
commit db3b99edcc
6 changed files with 168 additions and 5 deletions
+31
View File
@@ -0,0 +1,31 @@
package main
import (
"fmt"
"net/http"
"os"
"time"
)
// runHealthcheck pings /healthz on the local server and exits 0
// if the response is 2xx. Useful as a Docker HEALTHCHECK on
// distroless images that don't ship curl/wget.
//
// Defaults to http://127.0.0.1:8080/healthz; override with
// LIBRENOTES_HEALTHCHECK_URL.
func runHealthcheck(_ []string) error {
url := os.Getenv("LIBRENOTES_HEALTHCHECK_URL")
if url == "" {
url = "http://127.0.0.1:8080/healthz"
}
client := http.Client{Timeout: 5 * time.Second}
resp, err := client.Get(url)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("status %d", resp.StatusCode)
}
return nil
}
+14 -5
View File
@@ -9,12 +9,21 @@ import (
func main() {
args := os.Args[1:]
if len(args) > 0 && args[0] == "serve" {
if err := runServe(args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "serve:", err)
os.Exit(1)
if len(args) > 0 {
switch args[0] {
case "serve":
if err := runServe(args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "serve:", err)
os.Exit(1)
}
return
case "healthcheck":
if err := runHealthcheck(args[1:]); err != nil {
fmt.Fprintln(os.Stderr, "healthcheck:", err)
os.Exit(1)
}
return
}
return
}
notesium.Run()
}