diff --git a/README.md b/README.md index 5dbddf0..c9a6969 100644 --- a/README.md +++ b/README.md @@ -144,30 +144,30 @@ season is shared or synchronised. The only thing they have in common is the leaderboard they both post to at the end. The skin preference is the one thing still kept in the browser. -## Deploying - -Three containers behind the host's nginx, which does TLS and routing and serves -nothing off disk: - -| Path | Container | What it is | -| ----------- | ----------------- | -------------------------- | -| `/` | `games-hub` | the list of games | -| `/lemonade/`| `lemonade-web` | this game, a static bundle | -| `/api/` | `lemonade-scores` | the shared leaderboard | +## Running it anywhere ```bash -docker compose -f deploy/compose.yml up -d --build +docker compose up -d --build +# game http://localhost:8080 +# scores http://localhost:5184/api/scores ``` -`deploy/nginx-host.conf.example` is the host-side server block. The game's -asset paths are baked in at build time, so `BASE_PATH` in the compose file has -to match the path nginx proxies it to. Only the scores container writes -anything: a named volume holding the SQLite database, which is the one piece of -state that must survive a redeploy. +Two containers: a static bundle behind nginx, and the leaderboard. Only the +scores container writes anything — a named volume holding the SQLite database, +which is the one piece of state worth keeping. + +The game asks for `/api`, so in production put both behind one origin and +proxy `/api/` to the scores container. Serving the game from a sub-path means +rebuilding it for that path, because asset URLs are baked in: set `BASE_PATH` +(the `web/Dockerfile` build arg) to match. Set `TRUST_PROXY=1` on the scores +container when something else terminates TLS in front of it, so the rate limit +counts players rather than counting the proxy. ## Licence -AGPL-3.0-or-later — see [LICENSE](LICENSE). +AGPL-3.0-or-later — see [LICENSE](LICENSE). Source: +, also linked from every screen in the +game, which is what section 13 asks for. Affero rather than plain GPL because the leaderboard is a network service: anyone running a modified copy of it for other people has to offer them the diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..ae90acf --- /dev/null +++ b/compose.yml @@ -0,0 +1,52 @@ +# Run the game and its leaderboard locally, or on any host you like. +# +# docker compose up -d --build +# -> game http://localhost:8080 +# scores http://localhost:5184/api/scores +# +# The game talks to /api, so put both behind one origin in production (see +# README.md). Nothing here is specific to any particular deployment. + +services: + lemonade-web: + build: + context: . + dockerfile: web/Dockerfile + args: + # Where the game is served from. "/" unless it sits under a path. + BASE_PATH: / + image: lemonade-web:latest + restart: unless-stopped + ports: + - '8080:8080' + read_only: true + tmpfs: + - /tmp + - /var/cache/nginx + - /var/run + security_opt: + - no-new-privileges:true + + lemonade-scores: + build: + context: ./server + image: lemonade-scores:latest + restart: unless-stopped + ports: + - '5184:5184' + environment: + # Set to 1 when something else terminates TLS in front of this, so the + # rate limit counts players rather than counting the proxy. + TRUST_PROXY: '0' + DB_PATH: /data/scores.db + # The only writable path, and the only state worth keeping. + volumes: + - lemonade-scores-data:/data + read_only: true + tmpfs: + - /tmp + security_opt: + - no-new-privileges:true + +volumes: + lemonade-scores-data: diff --git a/deploy/compose.yml b/deploy/compose.yml deleted file mode 100644 index ac0a539..0000000 --- a/deploy/compose.yml +++ /dev/null @@ -1,73 +0,0 @@ -# games.jcoffey.dev -# -# Three containers, one domain. The host's nginx does TLS and routes paths to -# them; nothing is served off the host's disk. -# -# / -> games-hub the list of games -# /lemonade/ -> lemonade-web the game, a static bundle -# /api/ -> lemonade-scores the shared leaderboard -# -# Run from the repository root: -# docker compose -f deploy/compose.yml up -d --build - -services: - games-hub: - build: - context: .. - dockerfile: hub/Dockerfile - image: games-hub:latest - container_name: games-hub - restart: unless-stopped - ports: - - '127.0.0.1:5186:8080' - read_only: true - tmpfs: - - /tmp - - /var/cache/nginx - - /var/run - security_opt: - - no-new-privileges:true - - lemonade-web: - build: - context: .. - dockerfile: web/Dockerfile - args: - # Must match the path the host proxies, or asset URLs will be wrong. - BASE_PATH: /lemonade/ - image: lemonade-web:latest - container_name: lemonade-web - restart: unless-stopped - ports: - - '127.0.0.1:5185:8080' - read_only: true - tmpfs: - - /tmp - - /var/cache/nginx - - /var/run - security_opt: - - no-new-privileges:true - - lemonade-scores: - build: - context: ../server - dockerfile: Dockerfile - image: lemonade-scores:latest - container_name: lemonade-scores - restart: unless-stopped - ports: - - '127.0.0.1:5184:5184' - environment: - TRUST_PROXY: '1' - DB_PATH: /data/scores.db - # The only writable path, and the only state that must survive a redeploy. - volumes: - - lemonade-scores-data:/data - read_only: true - tmpfs: - - /tmp - security_opt: - - no-new-privileges:true - -volumes: - lemonade-scores-data: diff --git a/deploy/nginx-host.conf.example b/deploy/nginx-host.conf.example deleted file mode 100644 index 96eea50..0000000 --- a/deploy/nginx-host.conf.example +++ /dev/null @@ -1,50 +0,0 @@ -# Host-side nginx for games.jcoffey.dev. TLS, logging and proxying only - -# nothing is read from disk here, every path goes to a container. -# -# Copy into the host's sites-available, adjust the certificate paths, then -# `nginx -t && systemctl reload nginx`. - -server { - listen 443 ssl http2; - listen [::]:443 ssl http2; - server_name games.jcoffey.dev; - - ssl_certificate /etc/letsencrypt/live/games.jcoffey.dev/fullchain.pem; - ssl_certificate_key /etc/letsencrypt/live/games.jcoffey.dev/privkey.pem; - - access_log /var/log/nginx/games.jcoffey.dev.access.log; - error_log /var/log/nginx/games.jcoffey.dev.error.log; - - # The leaderboard. TRUST_PROXY=1 in the container means it reads the - # forwarded address, so the rate limit applies per player rather than - # counting every submission as coming from nginx. - location /api/ { - proxy_pass http://127.0.0.1:5184; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - location /lemonade/ { - proxy_pass http://127.0.0.1:5185/; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } - - # Everything else is the list of games. - location / { - proxy_pass http://127.0.0.1:5186; - proxy_set_header Host $host; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - } -} - -server { - listen 80; - listen [::]:80; - server_name games.jcoffey.dev; - return 301 https://$host$request_uri; -} diff --git a/hub/Dockerfile b/hub/Dockerfile deleted file mode 100644 index 9aac357..0000000 --- a/hub/Dockerfile +++ /dev/null @@ -1,8 +0,0 @@ -# The front door for games.jcoffey.dev. Static, no build step: adding a game is -# one more card in index.html. -FROM nginx:alpine -COPY hub/nginx.conf /etc/nginx/conf.d/default.conf -COPY hub/public /usr/share/nginx/html -EXPOSE 8080 -HEALTHCHECK --interval=30s --timeout=3s --retries=3 \ - CMD wget -qO- http://127.0.0.1:8080/healthz >/dev/null || exit 1 diff --git a/hub/nginx.conf b/hub/nginx.conf deleted file mode 100644 index 05359d8..0000000 --- a/hub/nginx.conf +++ /dev/null @@ -1,15 +0,0 @@ -server { - listen 8080; - server_name _; - root /usr/share/nginx/html; - - location / { - try_files $uri $uri/ /index.html; - add_header Cache-Control "no-cache"; - } - - location = /healthz { - access_log off; - return 200 "ok\n"; - } -} diff --git a/hub/public/favicon.svg b/hub/public/favicon.svg deleted file mode 100644 index d3e7ab2..0000000 --- a/hub/public/favicon.svg +++ /dev/null @@ -1,9 +0,0 @@ - - - - - - - - - diff --git a/hub/public/index.html b/hub/public/index.html deleted file mode 100644 index ad1f596..0000000 --- a/hub/public/index.html +++ /dev/null @@ -1,163 +0,0 @@ - - - - - - - Games · jcoffey.dev - - - - - -
-
-

Games

-

- Small browser games. Nothing to install, no account to make, and no - tracking — open one and play. -

-
- - - -
-

- Free software, AGPL-3.0-or-later. Built by John Coffey. -

-
-
- - diff --git a/src/App.css b/src/App.css index ad0bca9..c8deaa5 100644 --- a/src/App.css +++ b/src/App.css @@ -752,6 +752,13 @@ html[data-skin='modern'] .btn-ghost:active:not(:disabled) { box-shadow: none; } +/* The source link is an anchor, not a button, but wears the same clothes. */ +a.btn { + text-decoration: none; + display: inline-flex; + align-items: center; +} + .controls { display: flex; flex-wrap: wrap; diff --git a/src/App.tsx b/src/App.tsx index b9c101f..594a36a 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -22,6 +22,12 @@ import { SetupScreen } from './components/SetupScreen' import { TitleScreen } from './components/TitleScreen' import './App.css' +/** + * AGPL section 13: anyone playing this over a network is entitled to the + * source of the version they are playing, so the offer sits on every screen. + */ +const SOURCE_URL = 'https://github.com/Coffey-Labs/lemonade' + export default function App() { const [seed] = useState(randomSeed) const [state, dispatch] = useReducer(reducer, seed, initialState) @@ -241,6 +247,15 @@ export default function App() { NEW GAME + + SOURCE + SEED {state.seed} } diff --git a/vite.config.ts b/vite.config.ts index 05970a0..9f3cfc6 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -3,8 +3,8 @@ import { defineConfig } from 'vite' // https://vite.dev/config/ export default defineConfig({ - // The game is served under a path on games.jcoffey.dev, so asset URLs have - // to be built for it. Local development stays at the root. + // Asset URLs are baked in at build time, so this has to match the path the + // game is served from. Local development stays at the root. base: process.env.BASE_PATH ?? '/', plugins: [react()], server: {