Skip to content

chore(site): migrate scorecrux.com preview image to Chainguard nginx - #28

Merged
CueCrux-Myles merged 1 commit into
mainfrom
chore/chainguard-nginx-site
Aug 20, 2026
Merged

chore(site): migrate scorecrux.com preview image to Chainguard nginx#28
CueCrux-Myles merged 1 commit into
mainfrom
chore/chainguard-nginx-site

Conversation

@CueCrux-Myles

Copy link
Copy Markdown
Contributor

ExecPlan chainguard-image-migrationM2 (frontdoor / web-facing), ScoreCrux slice.

Merging this does NOT deploy to production

Checked before opening: .github/workflows/ holds exactly two workflows, and neither builds or ships a container.

  • .github/workflows/ci.ymlpush: branches: [main] + pull_request, but the job is only pnpm install / typecheck / test:coverage / build on Node 20 and 22. No Docker step, no path filter on site/**.
  • .github/workflows/publish.yml — triggers on release: [created] only, and runs npm publish for the npm package. Not reachable by a merge to main.

site/Dockerfile has no consumer beyond a human running it locally — no compose file, no deploy script, and nothing in ScoreCrux-Frontdoor/ or InfraCrux/ references site/Dockerfile or a scorecrux-site image (grep -rn 'scorecrux/site\|scorecrux-site\|site/Dockerfile' over both returns nothing). The image was added in bdee1dd purely as a local preview of scorecrux.com.

What changed

site/Dockerfile (the only file touched)

before after
base nginx:alpine cgr.dev/chainguard/nginx:latest
EXPOSE 80 8080
COPY index.html -> /usr/share/nginx/html/index.html unchanged

Plus a header comment recording the mechanism and the corrected run command.

Why the port moves. Upstream nginx:alpine runs its master process as root, so its bundled server block can bind port 80. The Chainguard image runs as uid 65532 (Config.User: "65532" on the pulled image), which cannot bind a privileged port, so the config it ships listens on 8080:

# /etc/nginx/conf.d/nginx.default.conf, extracted from cgr.dev/chainguard/nginx:latest
server {
    listen       8080;
    ...
    location / { root /usr/share/nginx/html; index index.html index.htm; }
}

The alternative was keeping port 80 by shipping a custom config and switching back to a root UID — which hands back the non-root posture that is the point of the migration. Since the only consumer is the run command documented in bdee1dd's commit message, this follows the base instead. The local run command changes from -p 8888:80 to -p 8888:8080, and that is now written into the Dockerfile header so it does not have to be rediscovered.

What deliberately did not change. The document root is /usr/share/nginx/html on both bases, so the COPY line is untouched and no custom nginx config is needed. The base sets ENTRYPOINT ["/usr/sbin/nginx"] with CMD ["-c","/etc/nginx/nginx.conf","-e","/dev/stderr","-g","daemon off;"]; both are left alone rather than overridden, because an added CMD would append to that entrypoint. The runtime has no shell, so anything added here later must be exec form. No RUN, no shell-form directive, and no named volume is involved, so the uid-65532 write-permission trap does not apply — the copied file is root:root 0644, world-readable, and nginx serves it fine as 65532.

Digest pins dropped

None. The previous base was the floating tag nginx:alpine — already unpinned, so there is no digest to give up here. For the record, the new base is also unpinnable: free-tier Chainguard is :latest-only (plan policy, decided 2026-04-10). The digest resolved at build time was sha256:f6cbe96998972d87ebe30952ec1b6f3cff4103c33c889ffca7e13053e2036571 (image created 2026-08-20).

Build + smoke evidence (actually run, WSL2, Docker 29.4.3)

$ docker build -f site/Dockerfile -t cg-test-scorecrux-site:probe site/
#5 [1/2] FROM cgr.dev/chainguard/nginx:latest@sha256:f6cbe96998972d87ebe30952ec1b6f3cff4103c33c889ffca7e13053e2036571
#6 [2/2] COPY index.html /usr/share/nginx/html/index.html
#7 naming to docker.io/library/cg-test-scorecrux-site:probe done
BUILD_EXIT=0
$ docker run -d --name cg-scorecrux-site -p 8091:8080 cg-test-scorecrux-site:probe
$ curl -s -o body.html -w '%{http_code}\n' --max-time 10 http://127.0.0.1:8091/
200
$ cmp body.html site/index.html          # served bytes vs source
IDENTICAL                                 # 26485 bytes both sides
$ grep -o '<title>[^<]*</title>' body.html
<title>ScoreCrux — Agent Effectiveness Measurement</title>
$ curl -s -o /dev/null -w '%{http_code}\n' --max-time 10 http://127.0.0.1:8091/nope
404

docker logs cg-scorecrux-site:

2026/08/20 21:09:35 [warn] 1#1: the "user" directive makes sense only if the master process runs with super-user privileges, ignored in /etc/nginx/nginx.conf:2
2026/08/20 21:09:35 [notice] 1#1: using the "epoll" event method
2026/08/20 21:09:35 [notice] 1#1: nginx/1.31.4
2026/08/20 21:09:35 [notice] 1#1: built by gcc 16.2.0 (Wolfi 16.2.0-r0)
2026/08/20 21:09:35 [notice] 1#1: start worker processes
172.17.0.1 - - [20/Aug/2026:21:09:35 +0000] "GET / HTTP/1.1" 200 26485 "-" "curl/8.5.0" "-"

The single [warn] is the base's own nginx.conf carrying a user nginx; directive that a non-root master ignores; it is emitted by the unmodified Chainguard image and is not caused by this change. Container status Up, ports 0.0.0.0:8091->8080/tcp. Resulting image is 26.3MB. Probe container and both images (cg-test-scorecrux-site:probe, the pulled base) were removed afterwards; no docker system prune was run.

Not done here

Production rollout stays in the plan's M8 — and in this repo's case there is no container rollout to do, since nothing deploys this image.

🤖 Generated with Claude Code

ExecPlan chainguard-image-migration, M2 (frontdoor / web-facing).

site/Dockerfile: FROM nginx:alpine -> cgr.dev/chainguard/nginx:latest, and
EXPOSE 80 -> EXPOSE 8080.

Why the port moves: upstream nginx:alpine runs its master as root and its
bundled server block listens on 80. The Chainguard image runs as uid 65532,
which cannot bind a privileged port, so its
/etc/nginx/conf.d/nginx.default.conf listens on 8080 instead. The choice is
either to keep port 80 by shipping a custom config plus a UID change (giving
back the non-root posture that is the point of the migration), or to follow
the base and move the published port. This takes the second option; nothing
in this repo or in a sibling compose/proxy consumes the image, so the only
consumer is the documented local run command, updated in the header comment
from `-p 8888:80` to `-p 8888:8080`.

What deliberately did not change: the document root is /usr/share/nginx/html
on both bases, so the COPY is untouched and no custom nginx config is needed.
The base sets ENTRYPOINT ["/usr/sbin/nginx"] with a CMD carrying the config
flags; both are left alone rather than overridden, since an added CMD would
append to that entrypoint. The runtime has no shell, so any future addition
here must be exec form.

Supply-chain note: no digest pin was dropped -- the previous base was the
floating tag `nginx:alpine`, already unpinned. Free-tier Chainguard is
:latest-only, so the new base cannot be pinned either (plan policy,
2026-04-10).

Verified locally: `docker build -f site/Dockerfile -t cg-test-scorecrux-site:probe site/`
exit 0; container on -p 8091:8080 returned HTTP 200 for `/` with a body
byte-identical to site/index.html (26485 bytes) and 404 for an unknown path;
nginx 1.31.4, only the benign "user directive ignored" warning from the
base's own nginx.conf.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@CueCrux-Myles
CueCrux-Myles merged commit 4601ff2 into main Aug 20, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant