DOMjudge 9 on Ubuntu 24.04 on Azure User Guide
Overview
DOMjudge is the automated jury system used to run programming contests such as the ICPC regionals and World Finals, and many university and school competitions and programming courses. Teams submit solutions through the web interface or the REST API; a judgehost compiles and runs each one against the problem's test data in a sandbox and returns a verdict; the jury watches progress, answers clarifications and publishes the scoreboard. DOMjudge is licensed under the GNU General Public License version 2 or later.
The cloudimg image is an all-in-one appliance on Ubuntu 24.04 LTS: the domserver (jury and team web interfaces, public scoreboard, REST API) and one judgehost run on the same VM, and a demo contest with three problems is loaded on the VM's first boot so you can see a submission judged within minutes of deployment. Backed by 24/7 cloudimg support.
What is on the image:
- DOMjudge 9.0.1, built from the official release tarball (checksum verified) into
/opt/domjudge - nginx and PHP 8.3-FPM serving the domserver at the site root and the REST API at
/api; MariaDB 10.11 for the database - One judgehost (
domjudge-judgedaemon@0) with its judging chroot at/chroot/domjudge(GCC, G++, OpenJDK and PyPy 3) - HTTPS from first boot: nginx serves the site on port 443 with a certificate created on your VM's first boot, and port 80 redirects to HTTPS
domjudge-firstboot.service, which creates your VM's database, admin password, judgehost credential, demo team password and application secretdomjudge-set-hostname, which moves the site to your own DNS name
Default user: the administrator you choose when you deploy (for example azureuser) · Architecture: x64 · Generation: Hyper-V Gen2

Security posture
- No database or password ships in the image. The MariaDB data directory in the image is empty. On your VM's first boot it is initialised (the MariaDB
rootaccount authenticates over the local socket only and has no password), then DOMjudge's own generators create the database password, the judgehost REST credential, the application secret and the admin password, and the database, demo contest and accounts are created with them. The demo team's stock password (demo) is replaced with a random one. No two VMs share any of these values. - No password on a command line. First boot hands every password to PHP and MariaDB on standard input, so none appears in the process list,
auth.logor the journal. - Fails closed. nginx and the judgehost start only after first boot has finished. If first boot cannot finish, the site stays down rather than open.
- Only nginx faces the network. The listening set is exactly 22, 80 and 443. MariaDB listens on
127.0.0.1only, and the judgehost reaches the REST API over a loopback-only listener. - The site address comes from configuration, never from the request. First boot reads it from
/etc/cloudimg/domjudge-hostname, or from the VM's addresses in Azure's instance metadata (its public IP, else its private IP). nginx answers only that name (and the VM's own IP addresses), drops any other Host, and passes DOMjudge the configured name. - Real client addresses. nginx removes
X-Forwarded-For,X-Real-IPandForwardedheaders before they reach DOMjudge, so a forged header cannot change the address DOMjudge records or uses for IP-based login. - No Azure metadata access. DOMjudge can be asked to fetch URLs (an external contest source in shadow mode, for example), so nginx, PHP-FPM, MariaDB, the judgedaemon and the first-boot unit are all blocked from reaching the Azure Instance Metadata Service (169.254.169.254) and the wire server (168.63.129.16). The Azure VM agent and cloud-init keep their access, so provisioning, extensions and password resets from the portal work as normal. Code submitted by teams runs in its own network namespace with no network at all.
- SSH as you chose it. Root login, empty passwords and X11 forwarding are turned off in
/etc/ssh/sshd_config.d/00-cloudimg.conf. Whether SSH accepts a password or only your key is the choice you make in the portal or CLI when you deploy. - Per-VM identity. SSH host keys, the machine ID and the site certificate are generated fresh on every VM. No compiler or build tool is installed on the host; the compilers live inside the judging chroot.
Prerequisites
- An Azure subscription where you can deploy Marketplace images
- An SSH key pair (or a password, if you choose password authentication)
- A virtual network and subnet in the target region, with a public IP address on the VM if teams reach it over the internet
- A network security group that allows
22/tcpfrom your management network, and80/tcpand443/tcpfrom your teams' and jury's networks. No other port is needed; do not open 3306
Recommended size: Standard_B2ms (2 vCPU, 8 GiB RAM). It runs the domserver and one judgehost comfortably for a course or a small contest. For a large contest, use a bigger domserver VM and add dedicated judgehosts (Step 10).
Step 1: Deploy from the Azure Marketplace
- Sign in to the Azure portal, choose Create a resource, search the Marketplace for DOMjudge by cloudimg and choose Create.
- On Basics pick your subscription, resource group, region and size (Standard_B2ms or larger). Under Administrator account choose SSH public key (recommended) or Password.
- Under Inbound port rules allow SSH (22), HTTP (80) and HTTPS (443), then narrow SSH to your management network.
- Choose Review + create, then Create. First boot finishes within about two minutes of the VM starting.
Step 2: Deploy from the Azure CLI
Workstation commands. Use your own resource group, network and key, and the image URN shown on the Marketplace listing.
az vm image terms accept --urn <marketplace-image-urn>
az vm create \
--resource-group <your-rg> \
--name domjudge \
--image <marketplace-image-urn> \
--size Standard_B2ms \
--admin-username azureuser \
--ssh-key-values ~/.ssh/id_ed25519.pub \
--vnet-name <your-vnet> --subnet <your-subnet> \
--public-ip-sku Standard
az vm open-port --resource-group <your-rg> --name domjudge --port 80,443 --priority 1010
Step 3: Connect over SSH
ssh azureuser@<vm-public-ip>
Step 4: Retrieve your credentials
First boot writes your VM's credentials to a root-only file:
sudo cat /root/domjudge-credentials.txt
It looks like this (your values differ):
domjudge.url=https://203.0.113.10/
domjudge.admin.user=admin
domjudge.admin.pass=<generated-per-vm>
domjudge.demo.user=demo
domjudge.demo.pass=<generated-per-vm>
domjudge.judgehost.user=judgehost
domjudge.judgehost.pass=<generated-per-vm>
domjudge.db.name=domjudge
domjudge.db.user=domjudge
domjudge.db.pass=<generated-per-vm>
domjudge.url is the site address: /etc/cloudimg/domjudge-hostname if that file exists, otherwise the VM's public IP address from Azure's instance metadata, else its private IP address. admin is the jury administrator, demo is the team account of the demo contest, and judgehost is the account judgehosts use to talk to the REST API. If you keep credentials centrally, copy them into Azure Key Vault.

Step 5: Verify the services
systemctl is-active mariadb php8.3-fpm nginx create-cgroups domjudge-judgedaemon@0
ls /var/lib/cloudimg/domjudge-firstboot.done
All five services print active, and the first-boot marker exists (first boot runs once; after a reboot its unit shows as skipped, which is expected). Only 22, 80 and 443 listen on a public address; MariaDB (3306) and the local resolver are on localhost:
sudo ss -Hltn | awk '{print $4}' | sort -u

The site answers over HTTPS on its configured name, port 80 redirects to HTTPS, and a request naming any other host is dropped (curl prints 000):
SITE=$(sudo sed -n 's#^domjudge.url=https://\([^/]*\)/#\1#p' /root/domjudge-credentials.txt)
PRIV=$(hostname -I | awk '{print $1}')
curl -sk -o /dev/null -w "%{http_code}\n" --resolve "${SITE}:443:127.0.0.1" "https://${SITE}/public"
curl -s -o /dev/null -w "%{http_code} %{redirect_url}\n" -H "Host: ${SITE}" "http://${PRIV}/public"
curl -sk -o /dev/null -w "%{http_code}\n" -H "Host: evil.example" "https://${PRIV}/" || true

MariaDB holds only the socket-authenticated system accounts and DOMjudge's own account, and listens on loopback only:
sudo mariadb -N -e "SELECT CONCAT(User,'@',Host) FROM mysql.global_priv ORDER BY 1; SELECT @@bind_address;"
The local judgehost has registered with the domserver. The admin password goes to curl in a configuration stream, never on its command line:
P=$(sudo sed -n 's/^domjudge.admin.pass=//p' /root/domjudge-credentials.txt)
curl -s -K <(printf 'user = "admin:%s"\n' "${P}") http://127.0.0.1/api/v4/judgehosts \
| python3 -c 'import sys,json;[print(j["hostname"], "enabled" if j["enabled"] else "disabled") for j in json.load(sys.stdin)]'
The services cannot reach the Azure metadata endpoints. This runs a small connection probe from inside php8.3-fpm.service as www-data, then from your shell: the first prints verdict=BLOCKED (IMDS and the wire server are unreachable while loopback works), the second shows imds4=REACHABLE:
CG=$(systemctl show -p ControlGroup --value php8.3-fpm.service)
sudo bash -c "echo \$\$ > /sys/fs/cgroup${CG}/cgroup.procs; exec setpriv --reuid=www-data --regid=www-data --init-groups /usr/local/sbin/cloudimg-egress-probe php8.3-fpm.service"
/usr/local/sbin/cloudimg-egress-probe your-shell || true

Step 6: Sign in
- Open
domjudge.urlfrom Step 4 in a browser. Plainhttp://redirects to HTTPS. The public scoreboard of the demo contest is at/public. - The certificate is self-signed and unique to your VM, so the browser warns the first time. Accept it, or install a trusted certificate (Step 9).
- Choose Login, enter admin and the
domjudge.admin.passvalue, and sign in.

You land in the jury interface. Before contest holds contests, problems, languages, teams and users; During contest holds clarifications, submissions, rejudgings and the scoreboard; Administrator holds the configuration settings and the config checker.

Step 7: Judge a first submission
The demo contest (demo) is created on first boot, runs for five hours from then, and has three problems: hello, fltcmp and boolfind. Submit a correct solution to hello as the demo team through the REST API, then read the verdict as the jury:
D=$(sudo sed -n 's/^domjudge.demo.pass=//p' /root/domjudge-credentials.txt)
P=$(sudo sed -n 's/^domjudge.admin.pass=//p' /root/domjudge-credentials.txt)
printf '#include <stdio.h>\nint main(void) { puts("Hello world!"); return 0; }\n' > /tmp/hello.c
SID=$(curl -s -K <(printf 'user = "demo:%s"\n' "${D}") -F problem=hello -F language=c -F "code[]=@/tmp/hello.c" \
http://127.0.0.1/api/v4/contests/demo/submissions | python3 -c 'import sys,json;print(json.load(sys.stdin)["id"])')
for i in $(seq 1 30); do
V=$(curl -s -K <(printf 'user = "admin:%s"\n' "${P}") "http://127.0.0.1/api/v4/contests/demo/judgements?submission_id=${SID}" \
| python3 -c 'import sys,json;print(next((j["judgement_type_id"] for j in json.load(sys.stdin) if j.get("end_time")), ""))')
[ -n "${V}" ] && break; sleep 2
done
echo "submission ${SID}: ${V}"
It prints the submission ID and AC (accepted). The submission appears in the jury's Submissions list with the result CORRECT, and on the scoreboard.


Teams normally submit through the team interface: they sign in with their own account, choose Submit, pick the problem and language, and upload their source file.
Step 8: Use your own hostname
To serve the site on your own DNS name, point a DNS record (for example in Azure DNS) at the VM's public IP (a Standard SKU public IP is static), then run on the VM:
sudo domjudge-set-hostname <your-dns-name>
The helper writes the name to /etc/cloudimg/domjudge-hostname, sets the nginx server_name, issues a new self-signed certificate for the name, updates domjudge.url in the credentials file and reloads nginx.
Step 9: Install a trusted certificate
The site certificate is /etc/ssl/domjudge/domjudge.crt and its key /etc/ssl/domjudge/domjudge.key (mode 600, root). To use Let's Encrypt, first set your DNS name (Step 8), allow port 80 from the internet in the network security group, then:
sudo apt-get install -y certbot
sudo certbot certonly --standalone -d contest.example.org \
--pre-hook "systemctl stop nginx" --post-hook "systemctl start nginx" \
--deploy-hook "install -m 644 \$RENEWED_LINEAGE/fullchain.pem /etc/ssl/domjudge/domjudge.crt && install -m 600 \$RENEWED_LINEAGE/privkey.pem /etc/ssl/domjudge/domjudge.key"
certbot keeps the hooks for every renewal. Running domjudge-set-hostname again replaces the certificate with a self-signed one, so run sudo certbot renew --force-renewal afterwards. For a certificate from another authority, copy the full chain and key to the same two paths and run sudo systemctl reload nginx.
Step 10: Set up your own contest
- In the jury interface open Contests and create a contest (or edit
demo): set its short name, start and end times, and make it public or private. - Under Problems import problems as Kattis/ICPC problem packages (zip), then add them to the contest.
- Under Teams and Users create the teams and one user per team (or import them as TSV/JSON under Import / export). Manage team passwords generates passwords for every team.
- Under Languages enable the languages your contest allows. The judging chroot ships compilers for C, C++, Java and Python 3 (PyPy).
- Use Config checker to confirm the setup before the contest starts.
To set a user's password, use Users in the jury interface; the password is typed into the form, never placed on a command line.

More judging capacity. The local judgehost judges one submission at a time. For a larger contest, install DOMjudge's judgehost on additional VMs (same release, 9.0.1) and point them at https://<your-host>/api with the judgehost user and domjudge.judgehost.pass from Step 4 in their restapi.secret. Allow 443 from those VMs only, and give each judgehost its own CPU cores for consistent timing.
The judging sandbox, memory limits and swap
The judgehost compiles and runs untrusted code inside the chroot with cgroup v2 limits enforced by runguard, in its own network namespace. The image ships with no swap (an Azure Marketplace image requirement); the per-submission memory limit is enforced directly by the memory controller, and a submission that exceeds it is reported as Memory Limit Exceeded. If you run contests with large memory limits and want swap, enable it on Azure's ephemeral resource disk (managed by the Azure Linux Agent) rather than on the OS disk, by setting these in /etc/waagent.conf and running sudo systemctl restart walinuxagent:
ResourceDisk.EnableSwap=y
ResourceDisk.SwapSizeMB=4096
Step 11: Server components
| Component | Version | Notes |
|---|---|---|
| DOMjudge | 9.0.1 | Official release tarball, GPL-2.0-or-later, installed in /opt/domjudge |
| PHP libraries | Symfony 6.4.48, Twig 3.30, league/commonmark 2.10 | DOMjudge's bundled libraries updated to their latest releases within DOMjudge's own version constraints, to include published security fixes |
| nginx | Ubuntu 24.04 package (1.24) | TLS front end on 80/443 and the loopback judgehost channel |
| PHP | 8.3 (FPM) | DOMjudge's own pool on a unix socket |
| MariaDB | 10.11 | 127.0.0.1 only; created on first boot |
| Judging chroot | Ubuntu 24.04 (debootstrap) | GCC, G++, OpenJDK, PyPy 3 |
grep DOMJUDGE_VERSION /opt/domjudge/domserver/etc/domserver-static.php
nginx -v 2>&1; php -v | head -n 1; mariadb --version
Step 12: Filesystem layout
| Path | Purpose |
|---|---|
/opt/domjudge/domserver |
domserver: web application, bin/dj_console, etc/ secrets (mode 640, root:www-data) |
/opt/domjudge/judgehost |
judgehost: bin/judgedaemon, bin/runguard, etc/restapi.secret, judgings/ work area |
/chroot/domjudge |
The chroot submissions are compiled and run in |
/etc/nginx/sites-available/domjudge |
nginx site; /etc/nginx/domjudge-host.conf holds the configured name |
/etc/ssl/domjudge/ |
Site certificate and key |
/etc/cloudimg/domjudge-hostname |
Your configured site name (created by domjudge-set-hostname) |
/var/lib/mysql |
MariaDB data (created on first boot) |
/root/domjudge-credentials.txt |
Your credentials (mode 600, root) |
/var/lib/cloudimg/domjudge-firstboot.done |
First boot marker; nginx and the judgehost start only when it exists |
Step 13: Managing the services
systemctl status nginx php8.3-fpm mariadb domjudge-judgedaemon@0 --no-pager -n 0
Restart the judgehost with sudo systemctl restart domjudge-judgedaemon@0 and reload nginx with sudo systemctl reload nginx. DOMjudge's command-line tools run as the web server user, for example sudo -u www-data /opt/domjudge/domserver/bin/dj_console list domjudge. Logs: sudo journalctl -u domjudge-judgedaemon@0 for the judge, /var/log/nginx/domjudge*.log and /opt/domjudge/domserver/webapp/var/log/ for the domserver.
Step 14: Backups
DOMjudge's own tool dumps the database (as root, over the local socket):
sudo /opt/domjudge/domserver/bin/dj_setup_database -s dump domjudge-$(date +%Y%m%d)
sudo ls /opt/domjudge/domserver/db-dumps/
The dump holds every account's password hash, so keep it private (sudo chmod 600 it, and encrypt it at rest). Copy it to Azure Blob Storage or another durable destination; dj_setup_database -s load <name> restores it. Azure Backup or a snapshot of the OS disk is a simple whole-VM backup.
Step 15: Troubleshooting
The browser cannot connect, or the connection is dropped. Browse to the configured name (domjudge.url in Step 4); other host names are dropped by design. Check that ports 80 and 443 are allowed in the network security group.
nginx or the judgehost is not running after deployment. Both start only after first boot completes:
ls -la /var/lib/cloudimg/domjudge-firstboot.done
sudo journalctl -u domjudge-firstboot --no-pager | tail -n 20
Submissions stay pending. Check that the judgehost is running and enabled (jury Judgehosts page, or systemctl status domjudge-judgedaemon@0), and look at its log with sudo journalctl -u domjudge-judgedaemon@0 --no-pager | tail -n 30. The contest must be active and the language enabled.
Step 16: Security recommendations
- Keep port 22 limited to your management network, and limit 80/443 to your contest network during a contest if you can.
- Change the admin password under Users if you share it with anyone, and give each jury member their own account with the jury role.
- Teams' code runs inside runguard's sandbox (its own network namespace, the chroot, CPU and memory limits); keep the judgehost on the same release as the domserver.
- The bundled PHP libraries were updated within DOMjudge's version constraints when the image was built. One library, enshrined/svg-sanitize 0.22 (used only when a jury member or administrator uploads or imports a problem with an SVG image), has advisories fixed only in a newer major release that DOMjudge 9.0.1 does not accept; only import problem packages from sources you trust.
- Apply Ubuntu security updates regularly (
sudo apt-get update && sudo apt-get upgrade); unattended security updates are enabled.
Support and licensing
DOMjudge is licensed under the GNU General Public License version 2 or later; some bundled components are under the MIT and BSD licences (COPYING, COPYING.MIT and COPYING.BSD in the DOMjudge 9.0.1 source release at domjudge.org). DOMjudge is a trademark of its developers; cloudimg is not affiliated with or endorsed by the DOMjudge project.
Every cloudimg image includes 24/7 technical support by email and live chat: support@cloudimg.co.uk.