Files
jcoffey a157fed8f2 Release weekly, and publish an image (#5)
INBUXA Admin had CI and nothing after it: twelve tags inherited from
upstream's numbering, no GitHub releases at all, and no image. Deploying
it meant building the tree yourself.

This adds the three workflows ihasmail already runs -- weekly release,
publish, prune -- and the Dockerfile they need.

Monday 09:37 UTC, and nothing on a quiet week. Staggered twenty minutes
behind ihasmail-inbuxa's and twenty ahead of the server's, so three
releases do not compete for runners and a bad Monday names one
repository rather than three.

The version is the difference from ihasmail. ihasmail derives its
version from the commit it builds, so its release only reads. INBUXA
Admin keeps its version in inbuxa-version.json, so the release writes
it: the bump is committed to main and the tag names that commit. The
tree a tag points at therefore reports the version the tag claims, which
a tag placed beside an unbumped file cannot promise. The bump is written
with a JSON parser rather than sed, because a version substituted into
JSON as a string is one stray quote from a file nothing can read.

The image is nginx serving the built files and nothing else -- the
interface talks to the mail server from the browser, never from the
container. It is built from source in the image rather than copied from
dist/, so an image always matches the commit it claims.

One image serves any installation: API_BASE_URL writes the
`<meta name="api-base-url">` tag that README already documents as the
deploy-time way to point the interface at its server. Set nothing and
the container still starts, for a build that was given
VITE_API_BASE_URL instead.

Two things the smoke test found rather than review. Unprivileged nginx
runs as uid 101, so the copied files are chowned to it or the tag can
never be written. And the directory stays root's, so the entrypoint
writes back through the existing file instead of `sed -i`, which
replaces the file and needs to create a temp file in the directory.

Verified by running it: the tag lands, a deep route falls back to
index.html, hashed assets come back immutable while index.html is
no-cache, it runs as uid 101, and it starts with no API_BASE_URL set.
2026-09-20 16:12:20 -07:00

32 lines
1.5 KiB
Docker

# INBUXA Admin as an image: the built interface and a static server for it.
#
# The interface is static files and nothing else -- it talks to the mail server
# from the browser, never from here -- so this is nginx with a SPA fallback and
# no back end of its own.
#
# It is built here rather than copied from `dist/`, which is committed for the
# convenience of people serving the tree directly. An image built from a stale
# `dist/` would be a build nobody can reproduce from the commit it claims.
FROM docker.io/node:26-alpine AS build
WORKDIR /build
# The lockfile alone first, so a commit that changes no dependency reuses this
# layer instead of resolving the tree again.
COPY package.json package-lock.json ./
RUN npm ci
COPY . .
# The version comes from inbuxa-version.json, which the release commits before
# this builds, so there is nothing to pass in here.
RUN npm run build
FROM docker.io/nginxinc/nginx-unprivileged:1.29-alpine
# Unprivileged nginx, which runs as uid 101 and cannot bind 80. 8080 is the
# port it listens on and the one to publish.
EXPOSE 8080
# Owned by the nginx user (uid 101 in this image), not root: the entrypoint
# below rewrites index.html, and cannot if the file is root's. The directory
# stays root's, which is why the entrypoint writes through the file rather
# than replacing it.
COPY --from=build --chown=101:101 /build/dist /usr/share/nginx/html
COPY docker/nginx.conf /etc/nginx/conf.d/default.conf
COPY --chmod=0755 docker/entrypoint.sh /docker-entrypoint.d/40-api-base-url.sh