DOMjudge 9
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 SecureAnyCloud 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. 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. The image is backed by 24/7 SecureAnyCloud support.
What you get:
- 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,democontest 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/sac/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-sac.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 larger domserver VM and add dedicated judgehosts (Step 10).
Deploy from the Azure Marketplace
- Sign in to the Azure portal, choose Create a resource, search the Marketplace for DOMjudge by SecureAnyCloud 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, and then Create. First boot finishes within about two minutes of the VM starting.
Deploy from the Azure CLI
Run these commands on your workstation. 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 1010Connect over SSH
ssh azureuser@<vm-public-ip>Retrieve your credentials
First boot writes your VM's credentials to a root-only file:
sudo cat /root/domjudge-credentials.txtIt 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/sac/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.
Verify the services
systemctl is-active mariadb php8.3-fpm nginx create-cgroups domjudge-judgedaemon@0 ls /var/lib/sac/data/domjudge-firstboot.doneAll five services print active, and the first-boot marker exists. First boot runs once, so 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 listen on localhost:
sudo ss -Hltn | awk '{print $4}' | sort -uThe 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}/" || trueMariaDB 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 command runs a small connection probe from inside php8.3-fpm.service as www-data, and then from your shell. The first probe 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/sac-egress-probe php8.3-fpm.service" /usr/local/sbin/sac-egress-probe your-shell || trueSign 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.
The jury interface opens. 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.
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.
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/sac/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.
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.
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. You type the password into the form, so it is 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=4096Server 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 --versionFilesystem 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/sac/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/sac/data/domjudge-firstboot.done First boot marker; nginx and the judgehost start only when it exists
Managing the services
systemctl status nginx php8.3-fpm mariadb domjudge-judgedaemon@0 --no-pager -n 0Restart 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.
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: run sudo chmod 600 on 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.
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/sac/data/domjudge-firstboot.done
sudo journalctl -u domjudge-firstboot --no-pager | tail -n 20Submissions 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.
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; SecureAnyCloud is not affiliated with or endorsed by the DOMjudge project.
Every SecureAnyCloud image includes 24/7 technical support by email and live chat: sales@secureanycloud.com.
Deploy on Azure
Launch DOMjudge 9 on Ubuntu 24.04 LTS on Azure with 24/7 support from SecureAnyCloud.
View this image
Need Help?
Our support team is available 24/7.
sales@secureanycloud.com