Bracket runs a whole tournament on one gunicorn worker, and its ports move between README and compose
Selfhosted tournament system
At a glance
- What is it?
- Bracket is an AGPL-3.0 selfhosted tournament system: FastAPI on the back, Vite and Mantine on the front, Postgres underneath, Docker Compose to start it. The operational details in the Dockerfile and compose file disagree with the quickstart on ports and credentials, which is the first thing to sort out before deploying.
- Who is it for?
- Adopt Bracket if you run a club, league or LAN event and want single elimination, round-robin or swiss formats with drag-and-drop court scheduling, all on infrastructure you control. Do not adopt it if your users need browser languages that Bracket does not publish, because language selection follows the browser.
- Can I use it commercially?
- Yes, with strict conditions. AGPL-3.0 is a network copyleft licence: if people use a modified version over a network, for example as a hosted service, you must offer them its source code under the same licence.
- Is it still maintained?
- Yes. The repository last received commits 3 days 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 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The quickstart works, but only for thirty minutes of demo data
The shortest path to seeing Bracket work is three commands:
git clone [email protected]:evroon/bracket.git
cd bracket
sudo docker compose up -dThat starts the backend, the frontend and a Postgres instance. The README says you can then view Bracket at http://localhost:3000 and log in with the username `[email protected]` and the password `aeGhoe1ahng2Aezai0Dei6Aih6dieHoo`.
Those credentials and that port are the first thing to change. Separately, the hosted demo at https://www.bracketapp.nl/demo is free but lasts 30 minutes, after which the data is deleted, so it is a look rather than an evaluation.
Seeding test data is a separate step rather than something `compose up` does for you:
docker exec bracket-backend uv run --no-dev ./cli.py create-dev-dbThat `docker exec bracket-backend` is worth noticing. The README's compose run names the service `bracket`, not `bracket-backend`, so the seeding command and the quickstart appear to describe different service names. Expect to reconcile that against your own `docker-compose.yml` before the command works.
The project also documents a non-Docker route: Bracket can be run by itself using `uv` and `pnpm`, with `pnpm`, `postgresql` and `uv` as the development prerequisites.
Port 3000 in the quickstart, port 8400 everywhere else
The compose file in the repository publishes 8400, not 3000:
services:
bracket:
container_name: bracket
depends_on:
- postgres
environment:
ENVIRONMENT: DEVELOPMENT
CORS_ORIGINS: http://localhost:8400
PG_DSN: postgresql://bracket_dev:bracket_dev@postgres:5432/bracket_dev
SERVE_FRONTEND: true
API_PREFIX: '/api'
image: ghcr.io/evroon/bracket
ports:
- 8400:8400And the Dockerfile bakes the same number in twice. It exposes 8400, and its health check hits `http://0.0.0.0:8400/ping` every 3 seconds with a 5 second timeout and 10 retries. The frontend build step passes `VITE_API_BASE_URL=http://localhost:8400/api` into the Vite build, so the port is compiled into the frontend bundle rather than resolved at runtime.
That last point is the operationally significant one. Because the API base URL is a build-time variable in the frontend bundle, changing the port after the image is built is not a matter of setting an environment variable at run time; you rebuild the frontend. The compose file sets `API_PREFIX: '/api'` on the backend, which is the server side, and those two must agree.
So the README's port 3000 does not match the repository files. Either the quickstart reflects a different compose file than the one committed here, or it is stale. Resolve this before you build a reverse proxy config around it.
The health check endpoint itself, `/ping`, is worth noting as the thing to point your own monitoring at.
One worker, and that is a deliberate-looking constraint
The Dockerfile's final command is the most informative line in the repository:
CMD [ \
"uv", \
"run", \
"--no-dev", \
"--locked", \
"--", \
"gunicorn", \
"-k", \
"uvicorn.workers.UvicornWorker", \
"bracket.app:app", \
"--bind", \
"0.0.0.0:8400", \
"--workers", \
"1" \
]Gunicorn is running the UvicornWorker class with an explicit worker count of one, and the app is bound on port 8400. One worker means one event loop and one process handling all requests, which is a normal configuration for an async Python service behind a proxy that scales horizontally. It also means that if you scale Bracket by adding gunicorn workers inside one container, you may hit state that assumes a single process. The README does not discuss worker scaling, so this is worth verifying against the docs before you change it.
The rest of the image is careful in ways that matter. The frontend is built in a separate `node:26-alpine` stage with `CI=true pnpm install`, and only the resulting `dist` directory is copied into the Python image, which is `python:3.14-alpine3.22`. `uv` is copied in from `ghcr.io/astral-sh/uv:latest`, then `uv sync --no-dev --locked` installs exactly the locked dependency set. The `--locked` flag is the reason upgrades are explicit rather than opportunistic.
Security-wise the image drops to a non-root user before installing: an `addgroup --system bracket` and `adduser --system bracket --ingroup bracket`, `chown -R bracket:bracket /app`, then `USER bracket`.
Two environment layers, and the docs are the only reference
Configuration is described in prose rather than enumerated. The backend is configured using `.env` files, with `prod.env` for production and `dev.env` for development, but you can also set environment variables directly, for example by specifying them in `docker-compose.yml`. The frontend can be configured with environment variables as well as `.env` files, using Vite's mechanism for loading them.
So there are two configuration systems in one application, with different file names and different mechanisms, and the README does not list a single key. The compose file shows four backend variables in practice: `ENVIRONMENT`, `CORS_ORIGINS`, `PG_DSN` and `SERVE_FRONTEND`, plus `API_PREFIX`.
`CORS_ORIGINS` deserves attention in production. In the committed compose file it is `http://localhost:8400`, a single origin matching the container's own port. Once you put Bracket behind a hostname, that value has to change or the browser will refuse the API calls, and the failure will look like a frontend that loads and then cannot fetch anything.
`PG_DSN` is a full SQLAlchemy-style URL, `postgresql://bracket_dev:bracket_dev@postgres:5432/bracket_dev` in development, with the password inline. The Postgres service uses `POSTGRES_DB`, `POSTGRES_USER` and `POSTGRES_PASSWORD`, all set to `bracket_dev` in the committed file. Two named volumes persist state: `bracket_pg_data` mapped to `/var/lib/postgresql` and `bracket_static_data` mapped to `/app/static`.
The README routes all of this to https://docs.bracketapp.nl/docs/running-bracket/configuration for the actual key list, and to the deployment docs for production. There is also a `process-compose-example.yml` at the top level, which suggests a process manager alternative to Docker for running the `uv` and `pnpm` components directly.
What the feature list actually covers
The stated feature set is specific enough to judge fit. Three formats are supported: single elimination, round-robin and swiss. Tournaments are built from multiple stages, and a stage can contain multiple groups or brackets. That two-level structure is what separates this from a flat bracket generator.
Scheduling is the part that looks like the hardest work and is handled directly: drag-and-drop matches onto different courts, or rescheduling them to another start time. For an event with several courts running in parallel, that is the difference between a spreadsheet and a tool.
Organisational structure runs club to tournament: multiple clubs, multiple tournaments per club, teams created and updated, players added to teams. Swiss tournaments can be handled dynamically with automatic scheduling of matches, which is the one format here that implies ongoing state rather than a structure you define up front.
For public display there are dashboard pages that can be shown to the public and customised with a logo, so a club can point a screen at the standings without giving anyone an admin login.
The repository's `SECURITY.md` and `CONTRIBUTING.md` are both at the top level, and the docs directory uses Docusaurus, with the site at https://docs.bracketapp.nl.
Where Bracket is the wrong choice
AGPL-3.0 is the first thing to check before self-hosting or offering Bracket as a network service. AGPL's network clause means that if you modify Bracket and let users interact with it over a network, you owe them the modified source. If you were planning to fork it and run a hosted version for paying clubs, read the licence before writing any code.
Second, the release channel. The most recent tag is v3.0.0-rc1, a release candidate from 2025-12-29, sitting above the stable 2.2.x line whose last entry is v2.2.5 from 2025-08-08. The repository is not archived and the last push was on 2026-10-14, so work continues, but the newest published artifact is a release candidate. You are choosing between a candidate and a stable line, and the README does not say which one the documentation site describes.
Third, language selection. Based on browser settings the language is detected and loaded automatically, and the README states there is currently no manual way of choosing a different language. For a venue where some users' browsers are set to a language you do not publish, this is a real gap. Translations are contributed through Crowdin, so adding a language is a community process rather than a config change.
Fourth, scope. This is a tournament system, not a league management package. The README does not describe player registration forms, payment handling, or statistics beyond standings and schedules.
Editorial conclusion
Adopt Bracket if you run a club, league or LAN event and want single elimination, round-robin or swiss formats with drag-and-drop court scheduling, all on infrastructure you control. Do not adopt it if your users need browser languages that Bracket does not publish, because language selection follows the browser. Before going live, replace the demo credentials and the `bracket_dev` database credentials from docker-compose.yml, decide whether you run the Compose stack on port 3000 as the quickstart says or the container's 8400, and note that the newest tagged release is the v3.0.0-rc1 release candidate from 2025-12-29, so you are choosing between a release candidate and the 2.2.x line.
Frequently asked questions
How do I run Bracket for the first time?
Clone the repository and run `sudo docker compose up -d`, which starts the backend, frontend and Postgres. The README says you can then view it at http://localhost:3000 and sign in with `[email protected]`. To seed dummy rows, run `docker exec bracket-backend uv run --no-dev ./cli.py create-dev-db`.
Which port does Bracket run on?
The repository's docker-compose.yml publishes 8400:8400 and the Dockerfile exposes 8400 with a health check against http://0.0.0.0:8400/ping, while the README quickstart says port 3000. The compose and Dockerfile values are what the images are built around.
Which tournament formats does Bracket support?
Single elimination, round-robin and swiss. Tournaments are built from multiple stages, each of which can hold multiple groups or brackets, and swiss tournaments can be scheduled dynamically with automatic match scheduling.
Can I change the interface language in Bracket?
Not manually. The README states the language is detected from browser settings and loaded automatically, and that for now there is no manual way of choosing a different language. Translations are added through Crowdin.
Can I run Bracket without Docker?
Yes. The README states Bracket can be run in Docker or by itself using `uv` and `pnpm`, with `pnpm`, `postgresql` and `uv` as the development prerequisites. A process-compose-example.yml is included in the repository.
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/evroon-bracket)Was this useful?