Containerize for independent deployment
All checks were successful
Docker / build-and-push (push) Successful in 46s

Adds a Dockerfile, nginx config, docker-compose.yml, and a Gitea Actions
workflow (mirroring goonk's), so rack builds and ships as its own image
rather than being embedded in goonk-cv's build - deploys stay fully
decoupled. Targets a subdomain, not a goonk.se subpath, to avoid needing
path-prefix rewriting kept in sync across the build, edge proxy, and
container.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Fredrik Johansson
2026-07-02 22:23:26 +02:00
parent f0314e7492
commit 85c87e674e
8 changed files with 165 additions and 15 deletions

4
.dockerignore Normal file
View File

@@ -0,0 +1,4 @@
node_modules
dist
.git
*.log

2
.env.example Normal file
View File

@@ -0,0 +1,2 @@
# Copy to .env (or set in your shell) to override docker-compose.yml defaults
HOST_PORT=5677

View File

@@ -0,0 +1,61 @@
name: Docker
on:
push:
branches: [main]
workflow_dispatch:
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Derive image name and tags
id: meta
shell: bash
run: |
set -euo pipefail
# Gitea Actions aims for GitHub Actions compatibility, but runners may expose
# slightly different env vars depending on configuration.
SERVER_URL="${GITHUB_SERVER_URL:-${GITEA_SERVER_URL:-}}"
REPO="${GITHUB_REPOSITORY:-${GITEA_REPOSITORY:-}}"
SHA="${GITHUB_SHA:-${GITEA_SHA:-}}"
if [[ -z "${SERVER_URL}" || -z "${REPO}" || -z "${SHA}" ]]; then
echo "Missing SERVER_URL/REPO/SHA env vars." >&2
echo "SERVER_URL='${SERVER_URL}' REPO='${REPO}' SHA='${SHA}'" >&2
exit 1
fi
HOST=$(echo "${SERVER_URL}" | sed 's|https://||;s|http://||')
IMAGE=$(echo "${HOST}/${REPO}" | tr '[:upper:]' '[:lower:]')
SHORT_SHA=$(echo "${SHA}" | cut -c1-7)
echo "host=${HOST}" >> "$GITHUB_OUTPUT"
echo "image=${IMAGE}" >> "$GITHUB_OUTPUT"
echo "short_sha=${SHORT_SHA}" >> "$GITHUB_OUTPUT"
- name: Log in to Gitea container registry
uses: docker/login-action@v3
with:
registry: ${{ steps.meta.outputs.host }}
username: ${{ github.actor }}
# Create a Gitea PAT with packages:write scope and add it as a
# repository secret named TKNTKN (Settings → Secrets)
password: ${{ secrets.TKNTKN }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
uses: docker/build-push-action@v6
with:
context: .
file: Dockerfile
push: true
tags: |
${{ steps.meta.outputs.image }}:latest
${{ steps.meta.outputs.image }}:${{ steps.meta.outputs.short_sha }}

View File

@@ -205,3 +205,39 @@
project (`goonk`, `adventure`, etc.), which each have their own `.git`. project (`goonk`, `adventure`, etc.), which each have their own `.git`.
Ran `git init` here to match that convention ahead of the eventual real Ran `git init` here to match that convention ahead of the eventual real
deploy. deploy.
## 2026-07-02 — containerize, decoupled from goonk-cv's own deploy
- Corrected course from the previous entry: copying rack's build into
`goonk/public/rack/` would mean every rack change requires rebuilding
and redeploying goonk-cv's own image — exactly the coupling explicitly
ruled out ("I don't necessarily want to redeploy goonk-cv when updating
this"). `goonk-cv` is built as its own Docker image via Gitea Actions
and pulled to host independently of any other project's repo, so rack
needed the same shape, not a file dropped into goonk's build.
- Added `Dockerfile` + `nginx/default.conf` (multi-stage: `node:24-alpine`
build → `nginx:alpine` static serve), mirroring `goonk`'s own Dockerfile
structure exactly, minus the `/admin` and `/api` proxy blocks it needs
and rack doesn't (no backend). Verified locally: `docker build` succeeds
and the resulting container serves the app correctly on a test port.
- Added `.gitea/workflows/docker.yml`, copied from `goonk`'s workflow
(which already derives the registry image name from the repo path, so
no rack-specific edits were needed beyond dropping the `-api` build
step goonk has and rack doesn't). Requires a `TKNTKN` repo secret (Gitea
PAT, `packages:write`) to be added manually in this repo's settings —
not something settable from here.
- Added `docker-compose.yml` (host-side reference, `image:
repo.explewd.com/explewd/rack:latest`) and `.env.example` for the
`HOST_PORT` override, matching `goonk`'s compose shape.
- Decision: **subdomain, not `goonk.se/rack`.** Discussed both — a subpath
deployment needs the `--base=/rack/` build *and* the edge proxy to
forward `/rack/...` through unstripped *and* the container to serve
files at that same sub-path, three things that have to stay in sync. A
subdomain (`rack.explewd.com`) needs one DNS record and one new
`server_name`/proxy-host entry, with the app built completely vanilla —
same decoupled-deploy story, far less to keep synchronized. Kept
`npm run build:subpath` in `package.json` in case the subpath route is
wanted later, but it's not part of the current deploy path.
- Host port: `5677`, matching the existing dev-server port for
memorability, mapped via `docker-compose.yml`'s `HOST_PORT` (defaults to
`5677`, override via `.env`).

13
Dockerfile Normal file
View File

@@ -0,0 +1,13 @@
# Build static site
FROM node:24-alpine AS build
WORKDIR /app
COPY package.json package-lock.json* ./
RUN npm ci || npm install
COPY . .
RUN npm run build
# Serve static output
FROM nginx:alpine
COPY --from=build /app/dist/ /usr/share/nginx/html/
COPY nginx/default.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

View File

@@ -85,26 +85,40 @@ uninstall) re-render immediately via a direct callback instead of waiting
on that interval, and it's skipped entirely while a `<select>` has focus so on that interval, and it's skipped entirely while a `<select>` has focus so
the service-picker dropdown doesn't get yanked shut mid-render. the service-picker dropdown doesn't get yanked shut mid-render.
## Deploying as a subdirectory (e.g. goonk.se/rack) ## Deployment
`npm run build` emits root-relative asset URLs (`/assets/...`), which only rack is its own container — deployed independently of `goonk`/`goonk-cv`,
works if rack is served from the domain root. To deploy it under a same pattern as every other project on this host: a `Dockerfile` builds
subpath instead, use: the static site and serves it via nginx, `.gitea/workflows/docker.yml`
builds and pushes that image to the Gitea registry on every push to
`main`, and `docker-compose.yml` is the host-side reference for running
it. The CI workflow needs a repo secret named `TKNTKN` (a Gitea PAT with
`packages:write`) — same requirement as `goonk`'s own workflow, set under
Settings → Secrets.
Served from its own subdomain (e.g. `rack.explewd.com`), not a subpath of
goonk.se — that keeps the build completely vanilla (root-relative asset
URLs work as-is, no `--base` flag needed) and means the edge nginx just
needs one new `server_name` block or NPM proxy host pointing at whichever
port `docker-compose.yml`'s `HOST_PORT` exposes (`5677` by default — no
`--base=/rack/`/path-prefix plumbing required, unlike a subpath
deployment would need).
```bash ```bash
npm run build:subpath # vite build --base=/rack/ docker build -t rack .
docker run -d -p 5677:80 rack # or: docker compose up -d
``` ```
That rewrites `dist/index.html`'s asset URLs to `/rack/assets/...`. Drop `rack`'s own `localStorage` key (`rack-save-v1`) is namespaced regardless
the resulting `dist/` contents into the host site wherever it serves of URL shape, so it won't collide with anything else on the same origin.
static files under `/rack/` — for the `goonk` Astro site specifically,
that's `goonk/public/rack/` (Astro copies `public/` verbatim into its own If a subpath deployment (`goonk.se/rack`) is wanted later instead, use
`dist/` at build time, and its nginx config already does a `npm run build:subpath` (`vite build --base=/rack/`) — it rewrites
`try_files $uri $uri/ /index.html` fallback, so no nginx changes are `dist/index.html`'s asset URLs to `/rack/assets/...`. That requires the
needed - existing `public/waste`, `public/pitwall` follow the same edge proxy to forward the `/rack/...` path through *unchanged* (not
one-subdir-per-project convention). `rack`'s own `localStorage` key strip the prefix) and the container to serve files at that same
(`rack-save-v1`) is namespaced, so it won't collide with anything else sub-path — more moving parts to keep in sync than the subdomain route
served from the same origin. above, which is why subdomain is the current choice.
## Tests ## Tests

8
docker-compose.yml Normal file
View File

@@ -0,0 +1,8 @@
services:
web:
image: repo.explewd.com/explewd/rack:latest
container_name: rack-web
restart: unless-stopped
ports:
- "${HOST_PORT:-5677}:80"
pull_policy: always

12
nginx/default.conf Normal file
View File

@@ -0,0 +1,12 @@
server {
listen 80;
server_name _;
root /usr/share/nginx/html;
index index.html;
# Single static page, no client-side router - just serve what's asked for.
location / {
try_files $uri $uri/ =404;
}
}