Files
selfpost/cmd/panel/httpserver.go
T
mix e6aceeb811 Phase 1: Docker image, supervisord, three-process cold start
Single bookworm-slim image running opendkim + panel + postfix under
supervisord with enforced start ordering (spec 4):

- build/Dockerfile: multi-stage static Go build; runtime installs postfix,
  opendkim, cyrus-sasl, supervisor, logrotate; unprivileged panel user (7.6.8).
- build/supervisord.conf: priority ordering opendkim -> panel -> postfix;
  crashexit event listener terminates the container on any FATAL process.
- build/postfix-wrapper.sh: waits for both milter sockets (test -S, 30s
  timeout) before `postfix start-fg`, exits non-zero on timeout.
- panel: HTTP :8080 stub + /healthz, journal-milter socket stub (so the
  wrapper's readiness probe passes), log-tailer stub; SIGTERM graceful stop.

Verified on the dev server: image builds, three processes live, panel serves
the stub, wrapper waits for sockets, and an unrecoverable panel failure brings
the container down cleanly.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-11 15:14:08 +03:00

68 lines
1.6 KiB
Go

package main
import (
"context"
"errors"
"log"
"net/http"
"time"
)
// serveHTTP runs the panel's HTTP server until ctx is cancelled. Phase 1 serves
// only a placeholder page and a health check; the login flow and real UI arrive
// in Phase 2.
func serveHTTP(ctx context.Context, addr string) error {
mux := http.NewServeMux()
mux.HandleFunc("/healthz", handleHealth)
mux.HandleFunc("/", handleIndex)
srv := &http.Server{
Addr: addr,
Handler: mux,
ReadHeaderTimeout: 10 * time.Second,
}
// Shut the server down cleanly when the process is asked to stop.
go func() {
<-ctx.Done()
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_ = srv.Shutdown(shutdownCtx)
}()
log.Printf("http panel listening on %s", addr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
return err
}
return nil
}
func handleHealth(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte("ok\n"))
}
func handleIndex(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
_, _ = w.Write([]byte(indexHTML))
}
const indexHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SelfPost</title>
</head>
<body>
<h1>SelfPost</h1>
<p>The control panel is starting up. Administrator setup and login arrive in a later build.</p>
</body>
</html>
`