Deployment overview
Internal doc. Describes how production actually runs today, taken from the code and the deploy workflow, not from older notes.
What runs where
Production is a single campus Linux server (server 7, cs-reed-07), reachable from the campus network at sjsu.cs30.app. No Kubernetes, no docker-compose, no reverse proxy. The backend terminates TLS itself and binds port 443 directly. The pieces:
- Backend — the unified
cs30.jar, run by thecs30.servicesystemd unit as usercs30backend. Listens on 443 over HTTPS and serves the API and the bundled web frontend. It’s a non-root user, so the unit grantsAmbientCapabilities=CAP_NET_BIND_SERVICEto let it bind the privileged port. - Judge —
kt-judge.jar, run by thekt-judge.serviceunit as usercs30judge, listening on 8000 (localhost only). It grades a submission by running thejudge-sandboxDocker image in a throwaway container, so its unit hasAfter=docker.service/Requires=docker.serviceandcs30judgeis in thedockergroup. - PostgreSQL — local on the same host.
- Git repositories — the problem pool and student repos live on the host filesystem; the backend writes to them directly.
- GitHub Actions self-hosted runner — runs on the server as
github-runner, so deploys happen locally with no SSH or inbound access from GitHub’s cloud.
The backend calls the judge at judge.url (http://localhost:8000) on startup and for every submission.
How a release gets there
flowchart TB
push["Push to main"] --> build["build-test (GitHub-hosted):\nrun tests + build cs30.jar and kt-judge.jar"]
build --> art["upload cs30-<sha> + kt-judge-<sha> jar artifacts"]
art --> deploy["deploy-prod (self-hosted runner):\ndownload the jar artifacts"]
subgraph server["Production server (/opt/cs30)"]
deploy --> ghcr["docker pull judge-sandbox:latest (GHCR)"]
deploy --> rel["releases/<sha>/{cs30.jar, kt-judge.jar}"]
rel --> link["current -> this release"]
link --> rj["restart kt-judge (non-fatal)"]
rj --> rb["restart cs30 (deploy gate)"]
rb -->|443 /health ok| done["prune to last 5"]
rb -->|fails| rollback["symlink back, restart BOTH services"]
end
Build runs on a GitHub-hosted runner; deploy runs on the self-hosted runner on the server (the only one that can reach the campus network and local systemd). The deploy is a file copy, a symlink flip, and service restarts. Full pipeline on the CI/CD page.
On-server layout
/opt/cs30/
current # symlink to the active release
releases/<sha>/ # one dir per deployed release (last 5 kept)
cs30.jar
kt-judge.jar
application.properties # config, content overwritten from the repo each deploy
cs30.env # secrets (DB password, Google client secret), mode 0600
/etc/ssl/cs30/
fullchain.pem # TLS cert (referenced by application.properties)
privkey.pem
/etc/systemd/system/
cs30.service # backend unit (user cs30backend)
kt-judge.service # judge unit (user cs30judge)
The tracked deploy/cs30.service and deploy/kt-judge.service are reference copies. The units that actually run are the installed ones under /etc/systemd/system/; if you change a unit, edit the installed one and daemon-reload.
The release model
A release is a directory named after the git SHA holding both jars. current points at the live release. Deploying = new release dir → point current at it → restart the judge, then the backend. Rolling back = point current at an older release and restart both. The deploy keeps the last 5 release dirs.
Rollback only reverts the jars, not the database. Schema is managed by Hibernate spring.jpa.hibernate.ddl-auto=update, so rolling back across a schema change can leave the DB ahead of the code — check what changed before rolling back over a migration. It also does not revert the judge sandbox image (the deploy pulls the mutable :latest tag).
Permissions model
CI never sets file permissions — the deploy job copies content with cp (not install), so it preserves whatever owner/group/mode the server already set. Permissions are owned by the server and set once by two scripts:
scripts/setup-service-users.sh— creates thecs30backendandcs30judgeusers and thecs30problemsgroup, putscs30judgeindocker, and sets the deploy dir/opt/cs30to root-owned with per-user ACLs grantingcs30backendandcs30judgeread (plus default ACLs so copied-in jars/config inherit read).scripts/grant-pool-access.sh— ACLs on the problem pool and student repos:cs30backendgets read/write (it owns and commits them), andcs30problemsgets read on the pool (the judge sandbox container runs as that group’s gid to read problems). The student repo is backend-only.
cs30problems is the pool-reader / container group; it is not what makes the deploy dir readable (that’s the per-user ACLs above).
Prerequisites set up once on the server
Done once, listed so you know they exist:
- Service users, group, docker membership, and
/opt/cs30ACLs —scripts/setup-service-users.sh. - Problem pool / student repo ACLs —
scripts/grant-pool-access.sh(withjudge.sandbox.group=cs30problemsinapplication.properties). cs30.servicehasAmbientCapabilities=CAP_NET_BIND_SERVICEsocs30backendcan bind 443.- Both systemd units installed under
/etc/systemd/system/and enabled. - The runner’s
GITHUB_TOKENcan read the privatejudge-sandboxGHCR package (package linked to the repo, or made readable), andgithub-runneris in thedockergroup. - TLS cert at
/etc/ssl/cs30/. - Google OAuth redirect URI
https://sjsu.cs30.app/callbackregistered in Google Cloud. - PostgreSQL and JDK 21 installed.