diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 000000000..0d47540c8 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +node_modules +dist +.git +.github +.devcontainer +docs +CLAUDE.md diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml new file mode 100644 index 000000000..7689a4a9a --- /dev/null +++ b/.github/workflows/docker.yml @@ -0,0 +1,56 @@ +name: Docker image + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + paths: + - Dockerfile + - docker/** + - .github/workflows/docker.yml + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-qemu-action@v3 + + - uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + if: github.event_name != 'pull_request' + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ghcr.io/${{ github.repository }} + tags: | + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}} + type=sha + + - name: Build and push + uses: docker/build-push-action@v6 + with: + context: . + # arm64 so the image runs on a Raspberry Pi in the pit + platforms: linux/amd64,linux/arm64 + push: ${{ github.event_name != 'pull_request' }} + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..4899295b0 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM node:22-alpine AS build +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci +COPY . . +# GitHub Pages serves from /QRScout/; containers serve from the domain root. +ARG BASE_PATH=/ +RUN npm run build -- --base=$BASE_PATH + +FROM nginx:1.27-alpine +COPY docker/nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +HEALTHCHECK --interval=30s --timeout=3s \ + CMD wget -qO /dev/null http://127.0.0.1/healthz || exit 1 diff --git a/docker/nginx.conf b/docker/nginx.conf new file mode 100644 index 000000000..ea6d0181e --- /dev/null +++ b/docker/nginx.conf @@ -0,0 +1,26 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + gzip on; + gzip_types text/css application/javascript application/json image/svg+xml; + + location = /healthz { + access_log off; + return 200 "ok"; + } + + # Hashed build assets are safe to cache forever. + location /assets/ { + add_header Cache-Control "public, max-age=31536000, immutable"; + } + + # Teams mount their scouting config at + # /usr/share/nginx/html/team-config.json and load it in the app + # via https:///team-config.json. + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/index.html b/index.html index 60067c950..e31c7a627 100644 --- a/index.html +++ b/index.html @@ -11,20 +11,20 @@ QR Scout - + - + diff --git a/public/manifest.json b/public/manifest.json index a33744d06..ef206c5f9 100644 --- a/public/manifest.json +++ b/public/manifest.json @@ -5,8 +5,8 @@ "background_color": "#f03d41", "display": "standalone", "orientation": "portrait", - "scope": "/QRScout/", - "start_url": "/QRScout/", + "scope": "./", + "start_url": "./", "icons": [ {"src": "icons/icon-36x36.png", "sizes": "36x36", "type": "image/png"}, {"src": "icons/icon-48x48.png", "sizes": "48x48", "type": "image/png"},