Deployment guide

DOMjudge 9.0.1

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.

This SecureAnyCloud image runs DOMjudge 9.0.1 on Ubuntu 24.04 on Amazon EC2. It is an all-in-one appliance: the domserver (jury and team web interfaces, public scoreboard, REST API) and one judgehost run on the same instance. A demo contest with three problems is loaded, so you can see a submission judged within minutes of launch.

The image is one version of the SecureAnyCloud Hardened Moodle & E-Learning Platforms AMIs listing on AWS Marketplace. Select it as the software version when you subscribe and launch. The other learning platforms in the listing are described on the listing's guide hub.

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, and 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 instance's first boot, and port 80 redirects to HTTPS.
  • domjudge-firstboot.service, which creates your instance's database, admin password, judgehost credential, demo team password and application secret.
  • domjudge-set-hostname, which moves the site to your own DNS name.
  • Defaults: default user ubuntu, architecture x86_64, root volume 12 GiB gp3.

Security posture

  • No database or password ships in the image. The MariaDB data directory in the image is empty. On your instance's first boot it is initialised (the MariaDB root account authenticates over the local socket only and has no password). DOMjudge's own generators then 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 instances share any of these values.
  • 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.1 only, 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 instance's addresses recorded by cloud-init at launch. nginx answers only that name (and the instance's own IP addresses), drops any other Host, and passes DOMjudge the configured name.
  • Real client addresses. nginx removes X-Forwarded-For, X-Real-IP and Forwarded headers before they reach DOMjudge, so a forged header cannot change the address DOMjudge records or uses for IP-based login.
  • No instance metadata access. DOMjudge can be asked to fetch URLs (an external contest source in shadow mode, for example). For this reason, nginx, PHP-FPM, MariaDB, the judgedaemon and the first-boot unit are all blocked from reaching the instance metadata service (169.254.169.254 and fd00:ec2::254). Code submitted by teams runs in its own network namespace with no network at all.
  • IMDSv2 required. The AMI is registered with ImdsSupport v2.0, so instances launched from it refuse token-less (IMDSv1) metadata requests.
  • Key-only SSH. Password and keyboard-interactive logins, root login and X11 forwarding are turned off in /etc/ssh/sshd_config.d/00-sac.conf. You sign in as ubuntu with your EC2 key pair.
  • Per-instance identity. SSH host keys, the machine ID and the site certificate are generated fresh on every instance. No compiler or build tool is installed on the host. The compilers live inside the judging chroot.

Prerequisites

Before you launch, make sure you have:

  • An AWS account where you can subscribe to AWS Marketplace products and launch EC2 instances.
  • An EC2 key pair in the target Region.
  • A VPC and subnet, with a public IP address on the instance if teams reach it over the internet.
  • A security group that allows 22/tcp from your management network, and 80/tcp and 443/tcp from your teams' and jury's networks. No other port is needed, and you must not open 3306.
  • The recommended instance type, m5.large (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 instance and add dedicated judgehosts (Step 10).
1

Launch from AWS Marketplace

  • Open the Hardened Moodle & E-Learning Platforms AMIs listing by SecureAnyCloud, choose View purchase options, then subscribe.
  • Choose Continue to Configuration. Under Software version, choose DOMjudge 9.0.1 on Ubuntu 24.04, pick your Region, and choose Continue to Launch.
  • Under Choose Action, select Launch through EC2.
  • Set the instance type to m5.large and choose your key pair. Use the seller's security group settings (ports 22, 80 and 443), and narrow port 22 to your management network.
  • Launch the instance. First boot finishes within about two minutes of the instance starting.
2

Launch from the AWS CLI

Run these commands on your workstation. Replace <ami-id> with the AMI ID shown for the DOMjudge 9.0.1 on Ubuntu 24.04 software version on the listing's launch page, and use your own VPC, subnet, key pair and address ranges.

aws ec2 create-security-group --group-name domjudge-sg \
  --description "DOMjudge" --vpc-id <vpc-id>
aws ec2 authorize-security-group-ingress --group-id <security-group-id> \
  --protocol tcp --port 22 --cidr <your-ip>/32
aws ec2 authorize-security-group-ingress --group-id <security-group-id> \
  --protocol tcp --port 80 --cidr <users-cidr>
aws ec2 authorize-security-group-ingress --group-id <security-group-id> \
  --protocol tcp --port 443 --cidr <users-cidr>
aws ec2 run-instances --image-id <ami-id> --instance-type m5.large \
  --key-name <key-name> --subnet-id <subnet-id> \
  --security-group-ids <security-group-id> --associate-public-ip-address \
  --metadata-options HttpTokens=required \
  --tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=domjudge}]'
aws ec2 describe-instances --instance-ids <instance-id> \
  --query "Reservations[].Instances[].[PublicDnsName,PublicIpAddress]" --output text
3

Connect over SSH

Connect with your key pair as the ubuntu user:

ssh -i <key.pem> ubuntu@<public-dns>
4

Retrieve your credentials

First boot writes your instance's credentials to a root-only file:

sudo cat /root/domjudge-credentials.txt

It looks like this (your values differ):

domjudge.url=https://ec2-203-0-113-10.compute-1.amazonaws.com/ domjudge.admin.user=admin domjudge.admin.pass=<generated-per-instance> domjudge.demo.user=demo domjudge.demo.pass=<generated-per-instance> domjudge.judgehost.user=judgehost domjudge.judgehost.pass=<generated-per-instance> domjudge.db.name=domjudge domjudge.db.user=domjudge domjudge.db.pass=<generated-per-instance>

domjudge.url is the site address. First boot takes it from /etc/sac/domjudge-hostname if that file exists. Otherwise, it uses the addresses cloud-init recorded at launch: the instance's public DNS name, else its public IPv4 address, else its private IPv4 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 AWS Secrets Manager.

5

Verify the services

Check the four services and the first-boot marker:

systemctl is-active mariadb php8.3-fpm nginx domjudge-judgedaemon@0 ls /var/lib/sac/data/domjudge-firstboot.done

All four services print active, and the first-boot marker exists. First boot runs once, and 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:

P=$(sudo sed -n 's/^domjudge.admin.pass=//p' /root/domjudge-credentials.txt)
curl -s -u "admin:${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 instance metadata service. This command runs a small connection probe from inside php8.3-fpm.service as www-data, then from your shell. The first probe prints verdict=BLOCKED (the metadata service is 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 || true
6

Sign in

  • Open domjudge.url from Step 4 in a browser. Plain http:// redirects to HTTPS. The public scoreboard of the demo contest is at /public.
  • The certificate is self-signed and unique to your instance, so the browser warns the first time. Accept it, or install a trusted certificate (Step 9).
  • Choose Login, enter admin and the domjudge.admin.pass value, 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.
7

Judge a first submission

The demo contest (demo) runs from first boot for five hours 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 -u "demo:${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 -u "admin:${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}"

The command 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.

8

Use your own hostname

To serve the site on your own DNS name, point a DNS record (for example in Amazon Route 53) at the instance's public IP. An Elastic IP keeps the address stable. Then run this command on the instance:

sudo domjudge-set-hostname contest.example.org

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.

An instance without an Elastic IP gets a new public DNS name when it is stopped and started. Run the helper again with the new name.

9

Install a trusted certificate

The site certificate is /etc/ssl/domjudge/domjudge.crt and its key is /etc/ssl/domjudge/domjudge.key (mode 600, root). To use Let's Encrypt, first set your DNS name (Step 8) and open port 80 to the internet. Then run:

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.

10

Set up your own contest

  • In the jury interface, open Contests and create a contest (or edit demo). Set its short name and its 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.

More judging capacity. The local judgehost judges one submission at a time. For a larger contest, install DOMjudge's judgehost on additional instances (same release, 9.0.1). Point them at https://<your-host>/api with the judgehost user and domjudge.judgehost.pass from Step 4 in their restapi.secret. Open 443 from those instances only.

11

Server components

  • 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: listens on 127.0.0.1 only and is created on first boot.
  • Judging chroot (Ubuntu 24.04, debootstrap): GCC, G++, OpenJDK and PyPy 3.

Check the installed versions:

grep DOMJUDGE_VERSION /opt/domjudge/domserver/etc/domserver-static.php nginx -v 2>&1; php -v | head -n 1; mariadb --version
12

Filesystem layout

  • /opt/domjudge/domserver: the domserver (web application, bin/dj_console, and etc/ secrets in mode 640, root:www-data).
  • /opt/domjudge/judgehost: the judgehost (bin/judgedaemon, bin/runguard, etc/restapi.secret, and the judgings/ work area).
  • /chroot/domjudge: the chroot in which submissions are compiled and run.
  • /etc/nginx/sites-available/domjudge: the nginx site. /etc/nginx/domjudge-host.conf holds the configured name.
  • /etc/ssl/domjudge/: the 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: the first-boot marker. nginx and the judgehost start only when it exists.
13

Managing the services

Check the status of 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.

The Amazon CloudWatch agent can ship /var/log/nginx/*.log to CloudWatch Logs.

14

Backups

DOMjudge's own tool dumps the database. Run it 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 in S3). Copy the dump to an Amazon S3 bucket or another durable destination. dj_setup_database -s load <name> restores it.

EBS snapshots of the root volume are a simple whole-instance backup:

aws ec2 create-snapshot --volume-id <volume-id> --description "domjudge $(date +%F)"
15

Troubleshooting

The browser cannot connect, or the connection is dropped. Browse to the configured name (domjudge.url in Step 4), because other host names are dropped by design. Check that ports 80 and 443 are open in the security group.

nginx or the judgehost is not running after launch. Both start only after first boot completes. Check the marker and the first-boot log:

ls -la /var/lib/sac/data/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). 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.

16

Security recommendations

  • Keep port 22 limited to your management network. If you can, limit 80/443 to your contest network during a contest.
  • 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, and 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.
17

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.

SecureAnyCloud provides 24/7 technical support by email and live chat: sales@secureanycloud.com. Every product in this listing is on the Hardened Moodle & E-Learning Platforms AMIs guide hub.

Deploy on AWS

Launch DOMjudge 9.0.1 on Ubuntu 24.04 on AWS with 24/7 support from SecureAnyCloud.

View on Marketplace

Need Help?

Our support team is available 24/7.

sales@secureanycloud.com

Deploy on AWS