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>
This commit is contained in:
2026-07-11 15:14:08 +03:00
parent 248a4db7a8
commit ed9e942e42
10 changed files with 467 additions and 3 deletions
+48
View File
@@ -0,0 +1,48 @@
package main
import (
"context"
"errors"
"log"
"net"
"os"
"path/filepath"
)
// serveJournalStub opens the journal-milter Unix socket so the Postfix start
// wrapper's readiness probe (test -S) succeeds and the cold-start ordering
// (spec 4) can be exercised end to end. The real milter protocol handler is
// implemented in Phase 6; here connections are simply accepted and closed.
func serveJournalStub(ctx context.Context, socketPath string) error {
if err := os.MkdirAll(filepath.Dir(socketPath), 0o755); err != nil {
return err
}
// Clear a stale socket left behind by an unclean shutdown, otherwise the
// listen below fails with "address already in use".
if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) {
return err
}
ln, err := net.Listen("unix", socketPath)
if err != nil {
return err
}
// Closing the listener unblocks Accept and unlinks the socket file.
go func() {
<-ctx.Done()
_ = ln.Close()
}()
log.Printf("journal-milter stub listening on %s", socketPath)
for {
conn, err := ln.Accept()
if err != nil {
if ctx.Err() != nil {
return nil // expected during shutdown
}
return err
}
_ = conn.Close()
}
}