Configuration
Both apps read one shared file, deploy/application.properties in the repo. On each deploy the workflow copies it to /opt/cs30/application.properties, so the repo file is the source of truth for all non-secret config.
Both services load that deployed file explicitly:
- Backend (
cs30.service):-Dspring.config.additional-location=file:/opt/cs30/application.propertieson thejavacommand. - Judge (
kt-judge.service):--spring.config.additional-location=file:/opt/cs30/application.properties.
This matters for the judge: without that flag it silently ignores the deployed file and falls back to the compiled-in default judge-sandbox:latest for judge.image, so it would run the wrong sandbox image. The flag is what makes the judge use the GHCR image. (The file is also bundled into each jar at build time via from("../application.properties"), but the external file overrides it.)
Where secrets come from
Secrets are never in the properties file and never in git. The file has placeholders (${DB_PASSWORD}, ${GOOGLE_CLIENT_SECRET}) that Spring fills from environment variables at startup.
flowchart LR
gh["GitHub 'production' env secrets"] -->|written on deploy| envfile["/opt/cs30/cs30.env (0600)"]
envfile -->|systemd EnvironmentFile| proc["service process env"]
proc -->|Spring placeholder substitution| app["running app"]
The two secrets are PROD_DB_PASSWORD and PROD_GOOGLE_CLIENT_SECRET (GitHub → Settings → Environments → production). Each deploy writes them to /opt/cs30/cs30.env with umask 077 (mode 0600); the systemd unit loads that file as its EnvironmentFile. To rotate a secret: update it in GitHub, redeploy.
The settings in deploy/application.properties
| Key | Value | Meaning |
|---|---|---|
server.port |
443 |
Backend HTTPS port (bound directly; no proxy) |
app.timezone |
America/Los_Angeles |
App timezone (AppTimeZoneService) |
server.ssl.enabled |
true |
The app terminates TLS itself |
server.ssl.certificate |
/etc/ssl/cs30/fullchain.pem |
TLS cert |
server.ssl.certificate-private-key |
/etc/ssl/cs30/privkey.pem |
TLS key |
server.compression.* |
— | Compress HTML/CSS/JS/JSON/wasm/SVG over 1 KB |
spring.datasource.url |
jdbc:postgresql://localhost:5432/cs30db |
Local Postgres |
spring.datasource.username |
cs30 |
DB user |
spring.datasource.password |
${DB_PASSWORD} |
From env (secret) |
spring.jpa.hibernate.ddl-auto |
update |
Hibernate manages schema; no migration tool |
google.client-id |
(literal) | OAuth client id (not secret) |
google.client-secret |
${GOOGLE_CLIENT_SECRET} |
From env (secret) |
google.redirect-uri |
https://sjsu.cs30.app/callback |
Must match the Google console exactly |
judge.url |
http://localhost:8000 |
Where the backend reaches the judge |
judge.image |
ghcr.io/sjsu-cs-systems-group/judge-sandbox:latest |
Sandbox image the judge runs per submission |
server.servlet.session.timeout |
1h |
Servlet HTTP session (OAuth round-trip only) |
cs30.backend.url |
https://sjsu.cs30.app |
Base URL the frontend calls |
cs30.allowed-ips |
(empty) | CIDR allowlist; empty = allow all |
docker.path |
/usr/bin/docker |
Docker binary the backend uses for git ops |
editor.max-custom-test-cases |
1 |
Custom inputs the editor allows on a run |
Dead keys: git.server.ssh-host and git.server.ssh-user are in the file but no code reads them — they only appear in old script comments. Ignore them.
Judge settings (judge.*, bound by JudgeProperties)
The judge reads these from the same file (defaults shown):
judge.port=8000— the judge’s own HTTP port (aJudgePortCustomizerapplies it, overridingserver.portso the judge and backend can share one file).judge.image— sandbox image (see table).judge.sandbox.*— container limits:memoryMb=1024,cpus=1.0,pidsLimit=256,fsizeBytes=33554432,workTmpfsMb=512,tmpTmpfsMb=128,uid=1000,group="".judge.sandbox.groupis a host group name; the judge resolves its GID viagetentat runtime and runs the container as that gid so it can read the problem pool (set it tocs30problemsin prod).judge.concurrency.maxWorkers(defaults to CPU count),judge.concurrency.maxQueueSize=100.judge.timeouts.runAllWallSeconds=60.judge.limits.maxCustomCases=10.judge.languages— extension map (c/.c,cpp/.cpp,java/.java,python/.py).
Backend keys with defaults (not in the file)
These have compiled-in defaults and are only set if you add them:
git.repos.base-path(default/var/git/courses),git.server.email(server@cs30.edu),git.server.name(CS30 Server) — used byGitService.backup.directory(/var/backups/cs30-db),backup.retain-days(7),backup.enabled(true) —DatabaseBackupService.
Things to keep straight
- Two session timeouts.
server.servlet.session.timeoutis the servlet HTTP session, used only for OAuth bookkeeping. The login session that matters for API calls has its own heartbeat TTL inApiTokenStore— not the same thing. cs30.allowed-ipsempty = open. TheIpWhitelistFilterallows everything when the list is blank. For campus-only access, put the lab CIDRs here.- Redirect URI must match Google exactly. Change the host or port and you must update the redirect URI in Google Cloud, or login breaks.