Ranked on KILLS IN A SINGLE RUN. Games survived is the same information at a tenth the resolution — most players land between 5 and 20, so that board is a wall of ties broken by who got there first. Games ride along as the tiebreak and as a column. The image is python:3.12-alpine running server.py now, not nginx. The board needs one process that can write, and everything the nginx conf did here — the no-cache headers and the manifest MIME type — is a few lines of stdlib. /data must be a persistent volume or every deploy resets the ladder to empty. A name belongs to whoever took it first: the client mints a random id once and the row remembers it, so a stranger cannot overwrite your score. The id never leaves the server. That is the whole security model and it is enough. The prompt to sign the board appears on exactly one occasion — a run that beat your own best kill count, on the tap that leaves the death screen. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
17 lines
904 B
Docker
17 lines
904 B
Docker
FROM python:3.12-alpine
|
|
# Was nginx:alpine. The leaderboard needs one process that can WRITE, and nginx is not one.
|
|
# Everything the nginx conf was doing here — no-cache so a deploy is picked up on a plain reload,
|
|
# and a real MIME type for the manifest, without which Chrome refuses to install the homescreen
|
|
# app — is a few lines in server.py. Standard library only: no pip, no requirements file.
|
|
# ponytail: switch to hashed filenames if traffic ever matters.
|
|
WORKDIR /app
|
|
COPY index.html manifest.webmanifest *.js server.py /app/
|
|
COPY assets /app/assets
|
|
# THE SCORES LIVE HERE AND THIS MUST BE A PERSISTENT VOLUME. Without one the json sits in the
|
|
# container's writable layer and every deploy silently resets the leaderboard to empty — which
|
|
# looks exactly like the feature working until the first time you push.
|
|
ENV SPLAT_DATA=/data
|
|
VOLUME /data
|
|
EXPOSE 80
|
|
CMD ["python", "-u", "server.py"]
|