From 3a67241cbe7295f5a236b3754bd136acbf3afd4d Mon Sep 17 00:00:00 2001 From: Rustem Kamalov Date: Tue, 21 Apr 2026 02:25:29 +0300 Subject: [PATCH] feat: refactor docker setup, use `chromedp/headless` --- .dockerignore | 14 ++++++++++ Dockerfile | 41 +++++++++++++++++---------- core/browser_unit_test.go | 59 +++++++++++++++++++++++++++++++++++++++ docker-compose.yaml | 3 ++ 4 files changed, 103 insertions(+), 14 deletions(-) create mode 100644 .dockerignore create mode 100644 core/browser_unit_test.go diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..6bcb590 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +.git +.gitignore + +.release/ +docs/*.md +*.md +testdata/ + +openserp +openserp.exe +bin/ +dist/ +*.tgz +*.tar diff --git a/Dockerfile b/Dockerfile index d422b82..284d1ef 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,28 +1,41 @@ # Build -FROM golang:alpine as builder - -LABEL stage=gobuilder -RUN apk update --no-cache && apk add --no-cache tzdata +FROM --platform=$BUILDPLATFORM golang:1.24.6-bookworm@sha256:ab1d1823abb55a9504d2e3e003b75b36dbeb1cbcc4c92593d85a84ee46becc6c AS builder WORKDIR /build -ADD go.mod . -ADD go.sum . +COPY go.mod go.sum ./ RUN go mod download COPY . . -RUN go build -o /app/openserp . +ARG TARGETOS +ARG TARGETARCH +RUN CGO_ENABLED=0 GOOS=${TARGETOS:-linux} GOARCH=${TARGETARCH:-amd64} go build -trimpath -ldflags="-s -w" -o /app/openserp . - -FROM zenika/alpine-chrome:with-chromedriver +# `chromedp/headless-shell:stable` also works here +FROM chromedp/headless-shell:stable@sha256:aac539266027f91cf47610da1129dce360d23f45f8f150683cca94223fa2f1e2 WORKDIR /usr/src/app -COPY --from=builder /app/openserp /usr/local/bin/openserp -COPY config.yaml ./config.yaml +# wget: used by HEALTHCHECK (localhost, no TLS, so ca-certificates not required). +# dumb-init: already provided by `docker run --init` / compose `init: true`, so we do NOT add tini here — the PID1 reaper is supplied by the runtime. +RUN apt-get update \ + && apt-get install -y --no-install-recommends wget \ + && rm -rf /var/lib/apt/lists/* \ + && getent passwd chrome >/dev/null 2>&1 || useradd --create-home --uid 1001 --shell /bin/bash chrome \ + && chown chrome:chrome /usr/src/app -HEALTHCHECK --interval=30s --timeout=5s --start-period=10s --retries=3 \ - CMD wget --no-verbose --tries=1 --spider http://localhost:7000/health || exit 1 +COPY --from=builder /app/openserp /usr/local/bin/openserp +COPY --chown=chrome:chrome config.yaml ./config.yaml + +# Rod's launcher.LookPath does not know about /headless-shell/headless-shell. +# Viper auto-binds OPENSERP_APP_BROWSER_PATH to app.browser_path, so this pins the binary and avoids Rod's runtime chromium auto-download (which would fail in this non-root, network-restricted image). +ENV OPENSERP_APP_BROWSER_PATH=/headless-shell/headless-shell \ + OPENSERP_SERVER_HOST=0.0.0.0 \ + OPENSERP_SERVER_PORT=7000 + +USER chrome + +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD wget --quiet --tries=1 --spider "http://127.0.0.1:${OPENSERP_SERVER_PORT}/health" || exit 1 ENTRYPOINT ["openserp"] - diff --git a/core/browser_unit_test.go b/core/browser_unit_test.go new file mode 100644 index 0000000..0a7db9d --- /dev/null +++ b/core/browser_unit_test.go @@ -0,0 +1,59 @@ +package core + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolveBrowserBinaryPathPrefersExplicit(t *testing.T) { + dir := t.TempDir() + bin := filepath.Join(dir, "chromium") + if err := os.WriteFile(bin, []byte("test"), 0o755); err != nil { + t.Fatalf("write temp browser binary: %v", err) + } + + path, err := resolveBrowserBinaryPath(bin, func() (string, bool) { + return "/should/not/be/used", true + }) + if err != nil { + t.Fatalf("resolve browser path: %v", err) + } + if path != bin { + t.Fatalf("expected explicit browser path %q, got %q", bin, path) + } +} + +func TestResolveBrowserBinaryPathFallsBackToLookPath(t *testing.T) { + want := "/usr/bin/chromium" + path, err := resolveBrowserBinaryPath("", func() (string, bool) { + return want, true + }) + if err != nil { + t.Fatalf("resolve browser path: %v", err) + } + if path != want { + t.Fatalf("expected lookPath result %q, got %q", want, path) + } +} + +func TestResolveBrowserBinaryPathReturnsEmptyWhenNothingResolved(t *testing.T) { + path, err := resolveBrowserBinaryPath("", func() (string, bool) { + return "", false + }) + if err != nil { + t.Fatalf("resolve browser path: %v", err) + } + if path != "" { + t.Fatalf("expected empty path, got %q", path) + } +} + +func TestResolveBrowserBinaryPathRejectsInvalidExplicit(t *testing.T) { + dir := t.TempDir() + if _, err := resolveBrowserBinaryPath(dir, func() (string, bool) { + return "", false + }); err == nil { + t.Fatalf("expected error when explicit browser_path points to a directory") + } +} diff --git a/docker-compose.yaml b/docker-compose.yaml index 60b089d..6b61056 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -3,6 +3,9 @@ version: '3' services: openserp: container_name: serp + init: true + # Chrome crashes with "Out of memory" on page loads when /dev/shm is the default 64MB. Prefer an enlarged shm over `ipc:host` to keep the container isolated from the host IPC namespace. + shm_size: 2gb build: context: . ports: