From 7fc2804bc6426e0f657c57c3f787a13e943b7567 Mon Sep 17 00:00:00 2001 From: nfebe Date: Sat, 18 Apr 2026 01:35:56 +0100 Subject: [PATCH 1/3] feat(ci): Publish UI as lightweight container image The dashboard UI is now shipped as a public container image on GHCR in addition to the existing dist archive. Pushes to main publish an image tagged with the short commit SHA; published releases publish versioned tags plus latest for non-prereleases. Images carry standard OCI metadata (title, description, vendor, licenses, source, documentation, revision, version, created) so the image is self-describing when inspected or browsed on GHCR. --- .dockerignore | 13 ++++++ .github/workflows/docker-image.yml | 64 ++++++++++++++++++++++++++++++ Dockerfile | 22 ++++++++++ nginx.conf | 20 ++++++++++ 4 files changed, 119 insertions(+) create mode 100644 .dockerignore create mode 100644 .github/workflows/docker-image.yml create mode 100644 Dockerfile create mode 100644 nginx.conf diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..83f0504 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,13 @@ +node_modules +dist +.git +.github +.vscode +.idea +coverage +*.log +.env +.env.* +!.env.example +Dockerfile +.dockerignore diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml new file mode 100644 index 0000000..b644769 --- /dev/null +++ b/.github/workflows/docker-image.yml @@ -0,0 +1,64 @@ +name: Docker Image + +on: + push: + branches: [main] + release: + types: [published] + +jobs: + build: + name: Build & Push + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository_owner }}/ui + tags: | + type=sha,format=short,enable=${{ github.event_name == 'push' }} + type=semver,pattern={{version}},enable=${{ github.event_name == 'release' }} + type=semver,pattern={{major}}.{{minor}},enable=${{ github.event_name == 'release' }} + type=raw,value=latest,enable=${{ github.event_name == 'release' && !github.event.release.prerelease }} + labels: | + org.opencontainers.image.title=FlatRun UI + org.opencontainers.image.description=Web interface for FlatRun container orchestration + org.opencontainers.image.vendor=FlatRun + org.opencontainers.image.licenses=MIT + org.opencontainers.image.documentation=https://github.com/${{ github.repository }}#readme + + - name: Build and push + uses: docker/build-push-action@v5 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + + - name: Ensure package is public + continue-on-error: true + run: | + curl -sS -X PATCH \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ + -H "X-GitHub-Api-Version: 2022-11-28" \ + "https://api.github.com/orgs/${{ github.repository_owner }}/packages/container/ui/visibility" \ + -d '{"visibility":"public"}' diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..9c8e5ee --- /dev/null +++ b/Dockerfile @@ -0,0 +1,22 @@ +# syntax=docker/dockerfile:1 + +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +RUN npm run build + +FROM nginx:alpine + +LABEL org.opencontainers.image.title="FlatRun UI" \ + org.opencontainers.image.description="Web interface for FlatRun container orchestration" \ + org.opencontainers.image.vendor="FlatRun" \ + org.opencontainers.image.licenses="MIT" \ + org.opencontainers.image.source="https://github.com/flatrun/ui" \ + org.opencontainers.image.url="https://github.com/flatrun/ui" \ + org.opencontainers.image.documentation="https://github.com/flatrun/ui#readme" + +COPY --from=build /app/dist /usr/share/nginx/html +COPY nginx.conf /etc/nginx/conf.d/default.conf +EXPOSE 80 diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..91ac78b --- /dev/null +++ b/nginx.conf @@ -0,0 +1,20 @@ +server { + listen 80 default_server; + listen [::]:80 default_server; + server_name _; + + root /usr/share/nginx/html; + index index.html; + + client_max_body_size 100M; + + location / { + try_files $uri $uri/ /index.html; + add_header Cache-Control "no-cache, no-store, must-revalidate"; + } + + location /assets/ { + expires 1y; + add_header Cache-Control "public, immutable"; + } +} From ef622d69ec5fe4c4bf8a79aa81eef34518822f3f Mon Sep 17 00:00:00 2001 From: nfebe Date: Sat, 18 Apr 2026 01:47:19 +0100 Subject: [PATCH 2/3] refactor(ci): Drop redundant package visibility API call Package visibility is set once in the GHCR package settings and persists across pushes, matching how the webservice image is handled. The extra API call added noise and required elevated token scopes that the default workflow token does not carry. --- .github/workflows/docker-image.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/.github/workflows/docker-image.yml b/.github/workflows/docker-image.yml index b644769..d59d12f 100644 --- a/.github/workflows/docker-image.yml +++ b/.github/workflows/docker-image.yml @@ -52,13 +52,3 @@ jobs: labels: ${{ steps.meta.outputs.labels }} cache-from: type=gha cache-to: type=gha,mode=max - - - name: Ensure package is public - continue-on-error: true - run: | - curl -sS -X PATCH \ - -H "Accept: application/vnd.github+json" \ - -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ - -H "X-GitHub-Api-Version: 2022-11-28" \ - "https://api.github.com/orgs/${{ github.repository_owner }}/packages/container/ui/visibility" \ - -d '{"visibility":"public"}' From e6f0ebfb8e32c85c78424295ad2bebd316097295 Mon Sep 17 00:00:00 2001 From: nfebe Date: Sat, 18 Apr 2026 01:47:24 +0100 Subject: [PATCH 3/3] refactor(ui): Relax static asset cache headers Index now allows conditional revalidation so returning visitors reuse the cached shell on 304s instead of re-downloading it every load. Static assets cache for thirty days instead of a year, giving a sane recovery window if a mishashed asset ever slips into a release. --- nginx.conf | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nginx.conf b/nginx.conf index 91ac78b..5c5f7f1 100644 --- a/nginx.conf +++ b/nginx.conf @@ -10,11 +10,11 @@ server { location / { try_files $uri $uri/ /index.html; - add_header Cache-Control "no-cache, no-store, must-revalidate"; + add_header Cache-Control "no-cache"; } location /assets/ { - expires 1y; - add_header Cache-Control "public, immutable"; + expires 30d; + add_header Cache-Control "public"; } }