Razzia builds on Node 26 and ships on Alpine's own Node, with one volume for everything
A self-hosted, open-source quiz platform for smaller events.
At a glance
- What is it?
- Razzia is a self-hosted quiz platform aimed at smaller events, published as a single image running nginx, a socket server and a static web build under supervisor. The deployment story is short and the persistence story is short too: one bind mount for the config folder, a manager password that fails closed, and a compose file whose link text names a different file than the one it points at.
- Who is it for?
- Razzia fits an event of a size where standing up infrastructure is more work than the quiz deserves, and the deployment path is a single compose up with one volume to back up. Three things to check first.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository last received commits 1 day ago.
- What is it written in?
- Mainly TypeScript, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The image builds on Node 26 and then runs on whatever Alpine ships
The Dockerfile is a three-stage file and the Node version changes between the first and the last. The base stage is `FROM node:26-alpine`, and the builder inherits it for `pnpm build`. The runner is a different base entirely: `FROM alpine:3.24.1 AS runner`, followed by `RUN apk add --no-cache nginx nodejs supervisor`. There is no version constraint on that `nodejs` package, so the runtime version is whatever the Alpine repository carries for that release, not the 26 the code was compiled against. Everything else about the runner is hand-assembled rather than inherited. Two build outputs are copied in, the web dist to `/app/web` and the socket bundle to `/app/socket/index.cjs`, and the entrypoint is `supervisord` with a config from `docker/supervisord.conf` and an nginx config from `docker/nginx.conf`. Only the port is exposed, 3000.
The compose link is labelled docker-compose.yml and points at /compose.yml
The Docker section introduces itself with a sentence saying the compose configuration is in the repository, then a link whose text reads `docker-compose.yml` and whose target is `/compose.yml`. The file at the top level of the tree is `compose.yml`, with no hyphenated twin, so the label and the destination disagree and only one of them resolves. The target is the accurate one, and the command that follows it works from the repository root.
docker compose up -dThe file itself is short: one service, the image `ralex91/razzia:latest`, a `3000:3000` port mapping, and the single bind mount. There is no restart policy and no environment block, so nothing about logging or recovery is configured on the Compose path. The manual `docker run` command beside it is the same shape, with the identical mount, port and tag, which is a useful comparison since it tells you the image expects to be told about the config folder rather than shipping one.
The manager interface sits on the game port and fails closed on the default password
The only credential in the project is a manager password, and it lives in `config/game.json` under the key `managerPassword`. The default written into the example is the literal string `PASSWORD`, and the rule attached to it is that it must be changed, because leaving the default blocks manager access rather than opening it. That is the right way round: a fresh install is not administrable by accident, and the fix is editing one file in a folder you already have to mount. What the password does not add is a network boundary. The manager interface is at `/manager` on the same origin and the same port as the game, and participants are given the game URL and a room code on that same address. Anyone who can reach the host can reach the manager path, so the password is the entire perimeter.
Two linters are configured and only one of them is ever invoked
The development dependencies include `oxlint`, `oxlint-tsgolint`, `@stylistic/eslint-plugin` and Prettier with a Tailwind plugin, and the root holds both `oxlint.config.ts` and `.prettierrc.json`. The scripts only run two of them. `lint` is `oxlint` and `lint:fix` is `oxlint --fix`; `format` is `prettier --check . --log-level warn` and `format:fix` writes. No script invokes ESLint, even though a stylistic plugin for it is pinned in the manifest at `latest`. Either the project moved from ESLint to oxlint and left the plugin behind, or the plugin is there for editor integration. Either way, the plugin version is unpinned while Prettier and TypeScript carry carets, and `oxlint` itself is also `latest`, so a lint run depends on when you installed.
A product built around a WebSocket protocol ships no test script
The documentation includes a page on the WebSocket protocol, framed as the way to build a custom client and giving an ESP32 physical buzzer as the example. That is a real commitment: it implies a documented wire contract other people are expected to implement against. The manifest does not have a `test` entry. Its scripts are `dev`, `dev:web`, `dev:socket`, `build`, `start`, `clean`, `lint`, `lint:fix`, `format` and `format:fix`, and there is no top-level test directory in the tree either. The workspace is three packages, `common`, `web` and `socket`, and the filtered scripts make the split explicit enough that you can see which piece owns the socket server. Whether the packages carry their own tests is not visible from the root, but the top level offers no way to run any.
One bind mount is the entire persistence story, and both commands pull latest
Every documented way to run the thing mounts the same volume, `./config:/app/config`, and nothing else. The page explains why in three lines: edit configuration files directly on the host, keep settings when updating the container, and back up quizzes and game configuration easily. The folder is created automatically on first run and arrives with an example quiz. That makes the backup story genuinely one directory, which is the strongest argument for the project. The flip side is that nothing outside that folder is durable, and the image ships nothing under `/app/config` of its own, so a run without the mount starts from the generated example rather than from anything you configured. Both the Compose file and the manual command reference a `latest` tag, on Docker Hub and on the GitHub Container Registry respectively, with no digest or version pinned in either.
The manual prerequisites name Node 24 and pnpm 10.16 while the image pins neither
The prerequisites section splits by deployment route. Without Docker it asks for Node.js version 24 or higher and PNPM version 10.16 or higher. With Docker it asks only for Docker and Docker Compose. The image side does not honour those floors. It starts from `node:26-alpine`, well above the stated Node 24, and installs the package manager with `RUN npm install -g pnpm`, which resolves to whatever pnpm is current at build time rather than to the 10.16 named on the page. That is fine for a published image, and it means the version requirements are really aimed at the source route. The source route itself is four commands: clone, `pnpm install`, then `pnpm dev` for development or `pnpm build` followed by `pnpm start` for production. All four run inside the workspace root, since the root scripts fan out to every package in parallel.
The image runs its own nginx, and the docs also tell you how to sit behind one
Two details sit oddly together. The runtime stage installs nginx and copies `docker/nginx.conf` into place, so the container is already a web server sitting in front of the static build. The documentation index still includes a reverse proxy page for running behind Traefik, Nginx, Caddy or something else, which is the normal arrangement when the port is meant to be published to a different proxy rather than to the world. The other oddity is a disclaimer. The project states that it is independent and open source, not affiliated with, endorsed by or sponsored by any third-party quiz platform or service, and that resemblance to other quiz platforms is purely incidental. That sentence does real work given the name, and it is the first thing a reader meets.
Editorial conclusion
Razzia fits an event of a size where standing up infrastructure is more work than the quiz deserves, and the deployment path is a single compose up with one volume to back up. Three things to check first. The manager interface is served on the same port as the game, so anything that can reach the host can reach the manager URL and the only barrier is the password in the config file. Both documented container commands pull a `latest` tag, so pin a version before an event you care about. And the image builds on one Node version and runs on another, which is worth knowing if you hit anything version-specific under load.
Frequently asked questions
What does Razzia need before I can run it?
Either Docker and Docker Compose, or Node.js 24 or higher with PNPM 10.16 or higher for the source route. There is no separate database step described; the whole application is one image on port 3000.
How do I protect the Razzia manager interface?
Set `managerPassword` in `config/game.json`. The default value is the literal string `PASSWORD`, and leaving it blocks manager access instead of enabling it. The manager page is served at `/manager` on the same port as the game.
What does Razzia keep when the container is replaced?
Only what is under the mounted `./config` folder, which holds game settings and quizzes. Both documented container commands use the same `-v ./config:/app/config` mount, and that folder is created automatically on first run with an example quiz.
Can I write my own client for Razzia?
The documentation includes a WebSocket protocol page for building a custom client, and gives an ESP32 physical buzzer as the example hardware. The application is a pnpm workspace with `common`, `web` and `socket` packages.
Which version of Node runs inside the Razzia image?
The build stage starts from node:26-alpine. The runtime stage is a separate Alpine base and installs the `nodejs` package from that repository with no version constraint, so the Node version at run time is not the one used to build.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/ralex91-razzia)