From 0ed480777ef37e91f27e4d33b356c58de4603234 Mon Sep 17 00:00:00 2001 From: Andras Schmelczer Date: Wed, 3 Jun 2026 21:03:13 +0100 Subject: [PATCH] Deploy --- .dockerignore | 14 ++++ .forgejo/workflows/deploy.yml | 110 ++++++++++++++++++++++++++ .github/workflows/main.yaml | 26 ------ Dockerfile | 50 ++++++++++++ README.md | 27 ++++++- backend/package.json | 5 +- frontend/package.json | 3 +- frontend/src/scripts/configuration.ts | 45 +++-------- shared/package.json | 2 +- 9 files changed, 214 insertions(+), 68 deletions(-) create mode 100644 .dockerignore create mode 100644 .forgejo/workflows/deploy.yml delete mode 100644 .github/workflows/main.yaml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..2bd8264 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,14 @@ +# The server image only needs `shared/` and `backend/`. +**/node_modules +**/dist +**/lib +.git +.github +.forgejo +.devcontainer +.vscode +frontend +media +*.log +Dockerfile +.dockerignore diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..93bc22e --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,110 @@ +name: Build & deploy + +on: + push: + branches: [main] + tags: ['v*'] + pull_request: + branches: [main] + workflow_dispatch: + +concurrency: + group: ${{ gitea.workflow }}-${{ gitea.ref }} + cancel-in-progress: true + +jobs: + # ---- Static website -> /pages mount on the runner host -------------------- + website: + name: Build & deploy website + runs-on: docker + steps: + - uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 14 + + - name: Install dependencies + # No lockfiles are committed (see .gitignore), so use `npm install`. + run: npm install && npm run init + + - name: Build (shared -> frontend -> backend) + run: npm run build + + - name: Deploy to host pages mount + if: github.event_name == 'push' && github.ref == 'refs/heads/main' + run: | + apt-get update && apt-get install -y rsync + rsync -a --delete frontend/dist/ /pages/declared + + # ---- Server Docker image -> Forgejo container registry -------------------- + server-image: + name: Build & publish server image + runs-on: docker + # No registry push on PRs; build validation still happens in the website job. + if: github.event_name != 'pull_request' + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install Docker CLI + run: | + ARCH=$(uname -m) + curl -fsSL "https://download.docker.com/linux/static/stable/${ARCH}/docker-27.5.1.tgz" \ + | tar xz --strip-components=1 -C /usr/local/bin docker/docker + docker --version + + - name: Set up Docker Buildx + uses: https://github.com/docker/setup-buildx-action@v3 + with: + driver-opts: | + network=host + + - name: Resolve registry vars + id: registry + env: + CONTAINER_REGISTRY_HOST: ${{ vars.CONTAINER_REGISTRY_HOST }} + run: | + host="${CONTAINER_REGISTRY_HOST:-${{ gitea.server_url }}}" + host="${host#https://}" + host="${host#http://}" + host="${host%/}" + if [ "$host" = "forgejo:3000" ]; then + host="127.0.0.1:13000" + fi + repo=$(echo "${{ gitea.repository }}" | tr '[:upper:]' '[:lower:]') + owner="${repo%%/*}" + { + echo "host=${host}" + echo "owner=${owner}" + echo "image=${host}/${repo}-server" + } >> "$GITHUB_OUTPUT" + + - name: Log in to Forgejo Container Registry + uses: https://github.com/docker/login-action@v3 + with: + registry: ${{ steps.registry.outputs.host }} + username: ${{ steps.registry.outputs.owner }} + password: ${{ secrets.FORGEJO_PACKAGE_TOKEN }} + + - name: Extract metadata + id: meta + uses: https://github.com/docker/metadata-action@v5 + with: + images: ${{ steps.registry.outputs.image }} + tags: | + type=sha,format=short + type=raw,value=latest,enable={{is_default_branch}} + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + + - name: Build and push + uses: https://github.com/docker/build-push-action@v6 + with: + context: . + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=registry,ref=${{ steps.registry.outputs.image }}:buildcache + cache-to: type=registry,ref=${{ steps.registry.outputs.image }}:buildcache,mode=max diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml deleted file mode 100644 index 17aa5ee..0000000 --- a/.github/workflows/main.yaml +++ /dev/null @@ -1,26 +0,0 @@ -name: Build and deploy project -on: - push: - branches: - - main -env: - CONTAINER_REGISTRY: schmelczera - -jobs: - build-project: - runs-on: ubuntu-latest - steps: - - name: Checkout current branch with lfs - uses: actions/checkout@master - with: - lfs: true - - name: Build project - run: | - npm i && npm run init && npm run build - - name: Deploy frontend - uses: w9jds/firebase-action@master - with: - args: deploy --only hosting --project decla-red - env: - FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }} - PROJECT_PATH: frontend diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dac1c44 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,50 @@ +# syntax=docker/dockerfile:1 + +# decla.red game server (the `declared-server` package). +# The frontend is a static site and is NOT built here — see .forgejo/workflows. + +# ---- Stage 1: build the shared lib, then bundle the server ------------------- +# Node 14 matches the project toolchain (webpack 4 / TypeScript 4, see +# .devcontainer). The full (non-slim) image carries the build tools that +# socket.io's optional native deps (bufferutil/utf-8-validate) compile against. +FROM node:14-bullseye AS build +WORKDIR /app + +# `shared` is consumed by the backend as `file:../shared` and is bundled into +# the server bundle by webpack, so it must be installed and built first. +COPY shared/package.json shared/ +RUN cd shared && npm install +COPY shared/ shared/ +RUN cd shared && npm run build + +# Install the backend deps. `file:../shared` now resolves to the built /app/shared. +COPY backend/package.json backend/ +RUN cd backend && npm install +COPY backend/ backend/ +RUN cd backend && npm run build + +# Drop devDependencies; the runtime only needs the production deps that webpack +# left external (express, socket.io, cors, gl-matrix, minimist, msgpack parser). +RUN cd backend && npm prune --production + +# ---- Stage 2: minimal runtime ---------------------------------------------- +FROM node:14-bullseye-slim +WORKDIR /app +ENV NODE_ENV=production + +# Run as an unprivileged user. +RUN groupadd -r app && useradd -r -g app -d /app app + +COPY --from=build /app/backend/dist ./dist +COPY --from=build /app/backend/node_modules ./node_modules + +USER app +EXPOSE 3000 + +# Hits the same /state endpoint the website polls (see serverInformationEndpoint). +HEALTHCHECK --interval=30s --timeout=5s --start-period=15s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/state',r=>process.exit(r.statusCode===200?0:1)).on('error',()=>process.exit(1))" + +ENTRYPOINT ["node", "dist/main.js"] +# Override these to tune the server, e.g. `--name`, `--playerLimit`, `--worldSize`. +CMD ["--port", "3000"] diff --git a/README.md b/README.md index 2d2ce2d..c7f1f53 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,5 @@ # [decla.red](https://decla.red) -![Deploy everything](https://github.com/schmelczerandras/decla.red/workflows/Deploy%20everything/badge.svg) - A 2-dimensional multiplayer game utilising ray tracing. > **Available at [decla.red](https://decla.red).** @@ -10,3 +8,28 @@ A 2-dimensional multiplayer game utilising ray tracing. ![three screenshots taken at iPhone SE screen size](media/collage-iphone.png) For optimised 2D ray tracing, [SDF-2D](https://github.com/schmelczerandras/sdf-2d) is used. + +## Deployment + +CI/CD runs on Forgejo Actions (`.forgejo/workflows/deploy.yml`). On a push to +`main` it: + +- builds the static frontend and rsyncs `frontend/dist/` to the `/pages/declared` + mount on the runner host, and +- builds the server image from the root `Dockerfile` and pushes it to the Forgejo + container registry as `//-server`. + +The registry job needs a `FORGEJO_PACKAGE_TOKEN` secret (with package write +scope) and, optionally, a `CONTAINER_REGISTRY_HOST` variable to override the +registry host. + +The website's server list is hardcoded in +[`frontend/src/scripts/configuration.ts`](frontend/src/scripts/configuration.ts) — +edit it to add or remove game-server origins. + +Run the server image locally: + +```sh +docker build -t declared-server . +docker run -p 3000:3000 declared-server --name "My server" --playerLimit 16 +``` diff --git a/backend/package.json b/backend/package.json index fe436f2..ded05b8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -17,11 +17,10 @@ "@types/config": "0.0.36", "cors": "^2.8.5", "express": "^4.17.1", - "gl-matrix": "^3.3.0", + "gl-matrix": "3.3.0", "minimist": "^1.2.5", "socket.io": "^2.3.0", - "socket.io-msgpack-parser": "^2.0.0", - "uws": "^10.148.1" + "socket.io-msgpack-parser": "^2.0.0" }, "devDependencies": { "@types/cors": "^2.8.7", diff --git a/frontend/package.json b/frontend/package.json index fd503a4..a819ba1 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -26,8 +26,7 @@ "clean-webpack-plugin": "^3.0.0", "common-config-webpack-plugin": "^2.0.1", "css-loader": "^1.0.1", - "firebase": "^7.22.0", - "gl-matrix": "^3.3.0", + "gl-matrix": "3.3.0", "html-webpack-inline-source-plugin": "^1.0.0-beta.2", "html-webpack-inline-svg-plugin": "^2.3.0", "html-webpack-plugin": "^4.5.0", diff --git a/frontend/src/scripts/configuration.ts b/frontend/src/scripts/configuration.ts index 49e2017..2fafd25 100644 --- a/frontend/src/scripts/configuration.ts +++ b/frontend/src/scripts/configuration.ts @@ -1,42 +1,19 @@ -import firebase from 'firebase/app'; -import 'firebase/firebase-remote-config'; +/** + * Hardcoded list of game servers the landing page offers to players. + * + * Each entry is the public origin of a dockerized `declared-server` instance. + * The join screen polls `/state` (see `serverInformationEndpoint`) and + * only shows a server once it responds, so listing an offline origin here is + * harmless. Add or remove origins as you deploy more server containers. + */ +const servers: Array = ['https://server.decla.red']; export abstract class Configuration { - private static remoteConfig: firebase.remoteConfig.RemoteConfig; - private static initialized = false; - public static async initialize(): Promise { - if (this.initialized) { - return; - } - - const firebaseConfig = { - apiKey: 'AIzaSyBG85dp-AhaCW-qi_6mu77wDPSipzipIF4', - authDomain: 'decla-red.firebaseapp.com', - projectId: 'decla-red', - appId: '1:635208271441:web:c910843ae7e0549dadda70', - }; - - firebase.initializeApp(firebaseConfig); - - this.remoteConfig = firebase.remoteConfig(); - - this.remoteConfig.settings = { - minimumFetchIntervalMillis: 0, // todo: 3600 * 1000, - fetchTimeoutMillis: 15 * 1000, - } as any; - - await this.remoteConfig.ensureInitialized(); - await this.remoteConfig.fetchAndActivate(); - - this.initialized = true; + // Kept async for call-site compatibility; the server list is static now. } public static get servers(): Array { - if (!this.initialized) { - throw new Error('Configuration should be initialized'); - } - - return JSON.parse(this.remoteConfig.getValue('online_servers').asString()); + return servers; } } diff --git a/shared/package.json b/shared/package.json index 9f272e1..e03d76c 100644 --- a/shared/package.json +++ b/shared/package.json @@ -15,7 +15,7 @@ "devDependencies": { "clean-webpack-plugin": "^3.0.0", "file-loader": "^6.1.0", - "gl-matrix": "^3.3.0", + "gl-matrix": "3.3.0", "prettier": "^2.0.5", "terser-webpack-plugin": "^2.3.8", "ts-loader": "^8.0.3",