# private-isu turns a retired ISUCON contest into a self-hosted performance lab

> private-isu is a Go benchmarker paired with reference web applications in five languages, an Ansible provisioning setup, an Amazon machine image and a Docker Compose path, so a team can run a full web performance contest inside its own network. It suits engineers who want a repeatable tuning environment, and its parts are older than the packaging around them, with the portal and the old Ansible tree marked deprecated.

**catatsuy/private-isu** — a web application performance improvement training

- Repository: https://github.com/catatsuy/private-isu
- Stars: 580 · Forks: 357
- Language: Go
- License: MIT
- Published: 2026-10-22 · Updated: 2026-10-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/catatsuy-private-isu

## The benchmarker is a Go binary that drives HTTP requests at a target

The core of the project is the benchmarker, and it is a self-contained Go program rather than a script. It is run with a target URL and a userdata directory, and it returns a JSON result. The output line in the documentation shows the shape, a pass flag, a score, counts of successful and failed checks, and a messages array.

```bash
cd benchmarker
make
./bin/benchmarker -t "http://localhost:8080" -u ./userdata
```

The userdata directory is the definition of what is being measured, and it is downloaded rather than committed, which is why the Makefile fetches an archive of image files from the repository's releases and unpacks it. So the checks are fixed content: a set of images that the target application is expected to serve correctly, plus a scoring function over the results. The benchmarker is not tied to the contest tooling, and the documentation runs it from a different working directory on the contestant instance than on the benchmarker instance, with the target as a local address in one case and a remote address in the other.

```bash
$ sudo su - isucon
$ /home/isucon/private_isu/benchmarker/bin/benchmarker -u /home/isucon/private_isu/benchmarker/userdata -t http://localhost
```

## Five reference implementations, with Ruby as the default

The contestant side is a small e-commerce style application provided in five languages, and the choice of language is the point of the exercise. The documentation lists Ruby as the default, with Go, PHP, Python and Node.js also provided, so the same functional requirements exist in each and a team can pick the stack it intends to tune. The manual is the document that explains how to switch, and it is also the document that the repository warns about, noting that some of its text assumes internal events took place. The directory layout reflects the split, with a webapp directory holding the per-language reference implementations and a sql subdirectory holding the initial database dump, a benchmarker directory with the source and the userdata, a provisioning directory for the Ansible scripts that set up both the contestant and the benchmarker instances, an ansible_old directory and a portal directory that are both marked as deprecated versions of the older benchmarker and portal, and a Makefile with an init target that prepares the initial data.

## make init is the step everything else depends on

The README puts a warning at the top of the startup instructions, stating that before running any of the following procedures you must first run make init in the project root to prepare the initial data. The Makefile shows exactly what that means, and the reason is that two artefacts are not in the repository. The first is the database dump, webapp/sql/dump.sql.bz2, which the target fetches from a release URL with curl. The second is the benchmarker userdata image archive, benchmarker/userdata/img.zip, which is fetched the same way and then unzipped into place.

```bash
webapp/sql/dump.sql.bz2:
	cd webapp/sql && \
	curl -L -O https://github.com/catatsuy/private-isu/releases/download/img/dump.sql.bz2

benchmarker/userdata/img.zip:
	cd benchmarker/userdata && \
	curl -L -O https://github.com/catatsuy/private-isu/releases/download/img/img.zip
```

The consequence is that a first-time setup needs network access to GitHub releases even before anything is installed, and that the two published assets are from 2016, which is the only release entry the repository has. The Docker Compose section repeats the requirement, warning that if webapp/sql/dump.sql.bz2 is not in place the initial data will not be imported into MySQL.

## Docker Compose is the easiest path, and the port mapping is the first thing to hit

The compose route is two commands from the webapp directory.

```bash
cd webapp
docker compose up
```

The documentation warns that the file was renamed from docker-compose.yml to compose.yml, so an old checkout will not find it. The next thing it warns about is ports, because the compose environment maps TCP port 80 and 3306 to the host, and if anything else on the host is already using them the stack will not start. The fix is either to stop the conflicting process or to edit the ports definitions in the nginx and mysql sections of compose.yml, and the documentation gives the before and after shape of those definitions, moving the host side to 8080 and 13306 while the container side stays at 80 and 3306. Changing the implementation also has a rule, and it is not optional: to reflect a change to compose.yml or to a language implementation you stop the stack with docker compose down and bring it back up with docker compose up --build, because the build flag is what rebuilds the application container image.

## Switching away from Ruby means editing the build section and the nginx config

The default implementation is Ruby, and the compose file carries a build setting for the app service that decides which one is used. Switching to PHP needs an extra step beyond editing that setting, and the documentation gives it as a file move inside the webapp/etc directory, where the default nginx configuration is renamed to default.conf.org and the PHP configuration is renamed from php.conf.org into place.

```bash
cd webapp/etc
mv nginx/conf.d/default.conf nginx/conf.d/default.conf.org
mv nginx/conf.d/php.conf.org nginx/conf.d/php.conf
```

The reason is that the nginx virtual host decides which upstream the application is served from, so the language choice is expressed in two places, the image that gets built and the server configuration that points at it. The same pattern applies to the other languages, since the documentation says to check the execution method for each language separately, and for the benchmarker itself the requirement is simply a Go runtime and the userdata directory.

## Running the benchmarker against the compose stack from a container

The benchmarker can also be built and run as its own container, which is the path to take when you want the measurement to run in isolation from the application. The build is a plain docker build in the benchmarker directory, and the run needs host networking so the benchmarker can reach the application on the host.

```bash
cd benchmarker
docker build -t private-isu-benchmarker .
docker run --network host -i private-isu-benchmarker /bin/benchmarker -t http://host.docker.internal -u /opt/userdata
```

On Linux the host needs an extra mapping, and the documentation gives the variant that adds host.docker.internal pointing at the host gateway. If that name does not resolve, the fallback is to look up the address assigned to the docker0 interface and use it directly, and the documentation shows the output of ip a with the docker0 line, where the address to use is the one in the inet row.

```
3: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:ca:63:0c:59 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
    inet6 fe80::42:caff:fe63:c59/64 scope link
       valid_lft forever preferred_lft forever
```

## There are four provisioning paths and only one is maintained

The environment can be built several ways, and the differences matter. The old Vagrant environment was removed because it was not maintained, and the documentation says to use Docker Compose or a manual setup locally, and an AMI or cloud-init in the cloud. The AMI path is the most packaged, and the documentation is blunt about the risk, stating that security updates are not performed so you use it at your own responsibility. It also notes that the contestant instance needs TCP port 80 open in its security group, and that the benchmarker instance should be a machine with enough headroom, a compute optimised instance for example. The recommended instance types are given as c7a.large for the contestant side and c7a.xlarge for the benchmarker side, with the AMI available in ap-northeast-1 and the caveat that newer AMIs may exist since the one linked is a snapshot of a particular moment. A separate cloud-init collection by another author covers AWS, Azure, Google Cloud, Oracle Cloud, Sakura Cloud, Multipass and VMware, with Multipass recommended for a local Apple Silicon machine. The provisioning directory holds the Ansible scripts for anyone setting up the instances themselves.

## Where private-isu is the wrong choice

The first wrong case is a single benchmark. Setting up the environment, the database dump, the language runtime and a second instance for the benchmarker is more work than most one-off measurements justify, and a tool like a load generator pointed at a running service answers the same question in minutes. The second wrong case is a modern application, since the reference implementations and the SQL dump date from the era of the original contest, and the measurement they support is a set of image serving checks against a specific schema, not a general load profile of a modern API. The third is a team that wants a supported contest, because what is here is an internal contest that was revised in 2021 and then used as the basis of a book, so nothing here is a maintained upstream competition with new problems. The fourth is a team that cannot run two machines, since the benchmarker is expected to sit on a separate instance from the application to avoid the measurement competing with the thing being measured. The fifth is a deployment on Windows, since every documented path is Linux, with Ubuntu 24.04 named as the operating system.

## Conclusion

Teams that want to practise web performance tuning as a training exercise, or that need a repeatable way to measure the effect of a change on a PHP and MySQL application, are the people private-isu is for, and the fact that the benchmarker is a single Go binary talking HTTP to a target address means you can point it at anything you like. It is the wrong tool for a one-off benchmark, since setting up the environment takes longer than most one-off measurements do, and it is the wrong tool for a team that wants a maintained upstream contest, since what is here is derived from a 2016 internal contest that was revised in 2021 and then turned into a book. Check three things before you build on it: that the language you want to tune has a reference implementation under webapp, that your machine has the disk space and the memory the compose path expects, since the documentation asks for a generously equipped machine, and that you read the day-of manual carefully, because the manual contains notes that assume some internal events took place. The licence is MIT, the last push was on 2026-10-22, and the only release asset is a bundle of image files from 2016.

## FAQ

### What does private-isu consist of?

A benchmarker written in Go, reference web applications in Ruby, Go, PHP, Python and Node.js with Ruby as the default, an Ansible provisioning directory for the contestant and benchmarker instances, and older ansible_old and portal directories that are marked as deprecated.

### Why do I have to run make init before anything else?

Because the initial data is not in the repository. The init target downloads webapp/sql/dump.sql.bz2 and the benchmarker userdata image archive from the repository's release assets and unpacks them, and the Docker Compose path warns that without the dump file the initial data is not imported into MySQL.

### How do I run the benchmarker?

Build it with make in the benchmarker directory, then run ./bin/benchmarker with a target URL and the userdata directory, for example ./bin/benchmarker -t "http://localhost:8080" -u ./userdata. The result is printed as JSON with a pass flag, a score and success and fail counts.

### Which languages can I use in private-isu?

Ruby, which is the default, plus Go, PHP, Python and Node.js. Switching the compose setup means changing the build setting for the app service, and the PHP variant additionally needs the nginx configuration files swapped as described in the manual.

### Can I run private-isu with Docker Compose?

Yes, from the webapp directory with docker compose up, on a machine with generous memory. The file is now named compose.yml rather than docker-compose.yml, and the setup maps host ports 80 and 3306, which you may need to change in the nginx and mysql service definitions if those ports are already in use.

## Sources

- [catatsuy/private-isu on GitHub](https://github.com/catatsuy/private-isu)
- [Issues](https://github.com/catatsuy/private-isu/issues)
- [License: MIT](https://github.com/catatsuy/private-isu/blob/master/LICENSE)
- [README](https://github.com/catatsuy/private-isu/blob/master/README.md)
- [Releases](https://github.com/catatsuy/private-isu/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/catatsuy-private-isu
