feat(deploy): add Dockerfile and vendor htmx for deployable image
Stage-0 web UI needs a self-contained container image. Two changes: - Dockerfile: multi-stage build (golang:1.25 builder, CGO off + static link, -trimpath -s -w) into distroless static nonroot. The committed templ output and vendored asset mean a plain `go build` suffices — no codegen or CDN at build/run time. Existing .gitea CI already builds and pushes localhost:5000/tapir:<sha> + mirrors to GitHub (deploy patch intentionally omitted — cutover is held), so it only needed this file. - Vendor htmx 1.9.12 locally (internal/web/static/, embed.FS, served at /static/ outside the auth guard) and point Layout at /static/htmx.min.js instead of unpkg. The deployed UI must not depend on an external CDN being reachable from the cluster. task check green. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
+29
@@ -0,0 +1,29 @@
|
|||||||
|
# syntax=docker/dockerfile:1
|
||||||
|
|
||||||
|
# ── build ───────────────────────────────────────────────────────────────────
|
||||||
|
# templ output (*_templ.go) and the vendored htmx asset are committed, so a
|
||||||
|
# plain `go build` produces a self-contained binary — no codegen, no CDN.
|
||||||
|
FROM golang:1.25 AS build
|
||||||
|
|
||||||
|
WORKDIR /src
|
||||||
|
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
|
||||||
|
# CGO off + static linking so the binary runs in a distroless/scratch image with
|
||||||
|
# no libc. Trim symbols/DWARF to shrink the layer.
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -trimpath -ldflags='-s -w' -o /out/tapir ./cmd/tapir
|
||||||
|
|
||||||
|
# ── runtime ─────────────────────────────────────────────────────────────────
|
||||||
|
# distroless static + nonroot: no shell, no package manager, runs as uid 65532.
|
||||||
|
# ca-certificates are bundled, which the OIDC/HTTPS clients need.
|
||||||
|
FROM gcr.io/distroless/static-debian12:nonroot
|
||||||
|
|
||||||
|
COPY --from=build /out/tapir /tapir
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
USER nonroot:nonroot
|
||||||
|
ENTRYPOINT ["/tapir"]
|
||||||
|
CMD ["serve"]
|
||||||
@@ -47,6 +47,7 @@ func (a *App) logger() *slog.Logger {
|
|||||||
func (a *App) Router() http.Handler {
|
func (a *App) Router() http.Handler {
|
||||||
root := http.NewServeMux()
|
root := http.NewServeMux()
|
||||||
root.HandleFunc("GET /healthz", a.handleHealthz)
|
root.HandleFunc("GET /healthz", a.handleHealthz)
|
||||||
|
root.Handle("GET /static/", staticHandler())
|
||||||
root.Handle("/auth/", a.Auth.Routes())
|
root.Handle("/auth/", a.Auth.Routes())
|
||||||
|
|
||||||
app := http.NewServeMux()
|
app := http.NewServeMux()
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
package web
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// staticFS holds the vendored client assets (htmx) served under /static/. They
|
||||||
|
// are embedded into the binary so a deployed image works without any external
|
||||||
|
// CDN — the UI must not depend on unpkg being reachable from the cluster.
|
||||||
|
//
|
||||||
|
//go:embed static/*
|
||||||
|
var staticFS embed.FS
|
||||||
|
|
||||||
|
// staticHandler serves the embedded assets with a long cache lifetime. The
|
||||||
|
// files are content-stable (versioned by filename, e.g. htmx.min.js pinned to a
|
||||||
|
// release), so aggressive caching is safe.
|
||||||
|
func staticHandler() http.Handler {
|
||||||
|
fs := http.FileServer(http.FS(staticFS))
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.Header().Set("Cache-Control", "public, max-age=31536000, immutable")
|
||||||
|
fs.ServeHTTP(w, r)
|
||||||
|
})
|
||||||
|
}
|
||||||
Vendored
+1
File diff suppressed because one or more lines are too long
@@ -16,7 +16,7 @@ templ Layout(title string) {
|
|||||||
<meta charset="utf-8"/>
|
<meta charset="utf-8"/>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
<meta name="viewport" content="width=device-width, initial-scale=1"/>
|
||||||
<title>{ title }</title>
|
<title>{ title }</title>
|
||||||
<script src="https://unpkg.com/htmx.org@1.9.12" defer></script>
|
<script src="/static/htmx.min.js" defer></script>
|
||||||
@templ.Raw(styleTag)
|
@templ.Raw(styleTag)
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -51,7 +51,7 @@ func Layout(title string) templ.Component {
|
|||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><script src=\"https://unpkg.com/htmx.org@1.9.12\" defer></script>")
|
templ_7745c5c3_Err = templruntime.WriteString(templ_7745c5c3_Buffer, 2, "</title><script src=\"/static/htmx.min.js\" defer></script>")
|
||||||
if templ_7745c5c3_Err != nil {
|
if templ_7745c5c3_Err != nil {
|
||||||
return templ_7745c5c3_Err
|
return templ_7745c5c3_Err
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user