Task 1
Random City
Build a tiny full-stack app: a page with one button, and a backend that answers with a random city name. Package it as a Docker container, push it to GitHub, and submit the repository here. Our server builds it, runs it in a sandbox, drives it with a real browser and gives you a verdict.
1. What to build
- Frontend. The page served at
/has a<button id="test">and a<span id="out">. Use any framework you like, or none. - On click, the page calls your backend with
GET /cities. - Backend.
GET /citiesreturns the name of a random city, picked fresh on every call from a list of at least 5 cities. - Display. The city name from the response is shown as the text of
#out.
2. API contract
GET /cities HTTP/1.1
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
{"city": "Nairobi"}- Status
200,Content-Type: application/json, body is a JSON object with a non-empty stringcity(max 100 characters). - The answer must be random and must not be cacheable. A browser must not be able to reuse an earlier answer for a later click.
- Other methods on
/cities(for examplePOST) should be rejected with405. - The frontend must call it with a relative URL (
/cities). Both frontend and backend are served from the same origin and port.
3. Container contract
| Dockerfile | A file named Dockerfile at the repository root. The whole app (frontend and backend) runs from this one image. |
|---|---|
| Port | Listen on 0.0.0.0 at the port in the PORT environment variable (always 8080). localhost-only binds are unreachable. |
| Health | GET /healthz must return 200 within 60 seconds of the container starting. |
| Network | The build step has internet access (install your dependencies there). At runtime there is no outbound network. Your app cannot call external APIs, and the page may not load CDN scripts or fonts. |
| Filesystem | Read-only root. /tmp and /data are writable (64 MB each, wiped on exit). |
| Resources | 512 MB RAM, 1 CPU, 256 processes. Linux capabilities are dropped, so run as a non-root user if you can. |
| Limits | Repository up to 300 MB, build up to 10 minutes, image up to 1 GB. |
Minimal Dockerfile shape
# Any base image works: node, python, go, java...
FROM node:24-alpine
WORKDIR /app
COPY . .
# Install dependencies at build time (the build has internet access)
RUN npm ci --omit=dev
USER node
ENV PORT=8080
EXPOSE 8080
# Your server must answer "/", "/cities" and "/healthz"
CMD ["node", "server.js"]4. How to submit
- Push your project to a GitHub repository.
- Register your team (once) and log in.
- Open Submit, paste the repository URL, optionally a branch or tag (default branch if empty).
- Private repository? Create a fine-grained personal access token limited to that one repository with "Contents: Read-only" and paste it in the token field. It is encrypted at rest and deleted as soon as judging finishes.
- Watch the live status page. You can resubmit after each result (short cooldown applies, one submission at a time per team).
5. How you are judged
Every submission goes through the same automated pipeline:
- Clone your repository at its latest commit (the commit hash is recorded with your result).
- Build the Docker image.
- Run it in an isolated sandbox with no outbound network.
- Test the API directly: 30 calls to
/citiescheck status, payload shape, headers, speed and randomness. - Test the UI with a real browser (Chromium). The judge clicks
#testrepeatedly while a monitoring proxy records every request that leaves the page and every answer your server gives. It verifies that each click really produces a fresh request to your server, that the response reaches the browser unchanged, and that#outshows what the server actually sent. It also replaces a response in flight to confirm your page renders whatever it receives, so hard-coded or locally generated answers fail. - Score the checks below and issue a verdict.
Verdicts
| PASSED | Every required check passed. Your score shows how many optional checks passed too. |
| FAILED | Your app ran, but at least one required check failed. |
| START_FAILED | The container exited, crashed, ran out of memory, or never answered /healthz. |
| BUILD_FAILED | No Dockerfile at the root, docker build failed, or a limit was exceeded. |
| CLONE_FAILED | We couldn't fetch the repository (bad URL or branch, or missing token for a private repo). |
| ERROR | A problem on our side. Resubmit, and tell an organizer if it repeats. |
Scoring rubric
Total 100 points. Checks marked required must all pass for a PASSED verdict.
| Check | What we verify | Points |
|---|---|---|
| Container | ||
| Health endpoint required | GET /healthz returns 200 within 60 seconds of the container starting. | 4 |
| Backend | ||
| GET /cities succeeds required | GET /cities returns HTTP 200 every time. | 5 |
| JSON payload required | Content-Type is application/json and the body is {"city": "<non-empty string>"}. | 8 |
| Random city required | Across 30 calls the API returns at least 3 distinct cities. | 6 |
| Not cacheable | Responses do not allow caching (no positive max-age, s-maxage, immutable or future Expires). | 4 |
| Fast responses | 95th percentile response time is under 1 second. | 2 |
| Rejects non-GET | POST /cities is rejected with 404 or 405 (405 preferred). | 2 |
| Frontend | ||
| Required elements required | Exactly one <button id="test"> and one <span id="out"> exist on the page at /. | 8 |
| Every click sends a request required | Each click makes the browser send exactly one fresh GET /cities request to your own origin. Cached responses, service workers, missing requests and request storms fail. | 12 |
| Shows the server's answer required | After each click, #out shows exactly the city the server sent in that response. | 12 |
| Renders what it receives required | When the judge swaps the network response for a value your server never produces, #out shows that value. | 10 |
| Result changes between clicks | Over repeated clicks, #out shows more than one distinct city. | 4 |
| Responsive UI | #out updates within 2 seconds of each click. | 2 |
| Same-origin requests only | The page only talks to its own origin. No third-party APIs, CDNs or absolute URLs to other hosts. | 3 |
| No page errors | No uncaught exceptions or failed requests while using the page. | 3 |
| Traffic | ||
| Server received the clicks required | A monitoring proxy in front of your server sees one GET /cities per click, answered with HTTP 200. | 10 |
| Payload reaches the browser intact | The body your server sent (seen at the proxy) is byte-for-byte what the browser received. | 5 |
6. Common mistakes
- Server listens on
127.0.0.1or a hard-coded port instead of0.0.0.0:$PORT. - Frontend calls
http://localhost:5000/citiesor another absolute URL instead of/cities. - Response is cached (a
Cache-Control: max-age, or a service worker), so later clicks never reach the server. - Backend returns plain text instead of
{"city": "..."}JSON. - Writing files outside
/tmpor/data(the root filesystem is read-only). - Loading fonts, icons or scripts from a CDN. There is no network at runtime, so bundle assets into the image.
- Duplicate ids or the wrong element types: it must be exactly one
<button id="test">and one<span id="out">.
Fair play. Submissions run in a sandbox and are judged by their observable behavior. Attempts to detect or tamper with the judge, escape the sandbox or attack the portal lead to disqualification.