Installing CodeScoring.Save in Kubernetes
CodeScoring.Save is installed using the save Helm chart. The same chart supports two deployment scenarios:
- a standard Kubernetes installation with PostgreSQL, Redis, and S3-compatible object storage;
- a hosted installation in a single-node k3s cluster with SQLite and filesystem storage.
Before installation, review the system requirements.
Installing with the Helm chart
The vendor provides the Helm repository and container registry URLs and access credentials. The commands below use these placeholders:
HELM_REPOSITORY_URL— Helm repository URL;REGISTRY_URL— container registry URL;USERNAMEandPASSWORD— access credentials;CHART_VERSION— Helm chart and CodeScoring.Save version.
-
Create a namespace:
-
Create a Secret for accessing the private container registry:
The Secret name must match
image.pullSecretsandosa-proxy.imagePullSecretsinvalues.yaml. -
Install Helm and add the CodeScoring.Save repository:
-
Download and unpack the chart:
Configuring Helm chart values
Make the required changes before the first installation. Before upgrading an existing installation, back up PostgreSQL and the object storage.
The complete installation configuration is stored in values.yaml. The examples below include only the fragments to change; preserve the remaining settings from the distributed file.
Container images
The main components use a shared registry, tag, and image pull Secret:
OSA Proxy is distributed as a chart dependency and has separate image settings:
Use the image versions specified in the distributed values.yaml; they are tested for compatibility with the selected chart version.
Application secrets
Sensitive backend, worker, scheduler, and auth settings are stored in secrets.save-secrets.data:
SECRETS_ENCRYPTION_KEY encrypts stored credentials. Generate it once, for example with openssl rand -base64 32, store it securely, and do not change it during upgrades. Previously stored data cannot be decrypted after the key is changed.
Do not keep production passwords and keys in version control. For a production installation, use a protected values file or External Secrets.
JWT signing key
On the first start, the cs-auth service creates an RSA-2048 key and stores it in its database. On restart and on other replicas, the key is read from there.
The key is stored in the database without encryption. A dump or backup of the database (including a database shared with Save) makes it possible to issue tokens on behalf of any user — store them as secrets.
Key rotation is available to the administrator through the API (an administrator token is required, see Getting an Admin JWT):
During a planned rotation, the new key is first published in JWKS and starts signing tokens 2 minutes later. The old key keeps verifying already issued tokens until they expire and is then removed automatically. During an emergency rotation, refresh tokens keep working, so the web interface simply obtains a new token pair. Every rotation is recorded in the audit log.
PostgreSQL and Redis from the Helm chart
By default, the chart creates PostgreSQL and Redis as StatefulSets. Enable their resources and configure the PersistentVolumeClaims:
Replace storageClassName and the volume sizes according to the cluster configuration and expected data volume.
The built-in PostgreSQL credentials are configured separately and must match DATABASE_NAME, DATABASE_USER, and DATABASE_PASSWORD in secrets.save-secrets.data:
Application connection settings for PostgreSQL and Redis are stored in the save-backend-envs ConfigMap:
The ConfigMap and Secret are already attached to the components through deploymentsGeneral.envConfigmaps and deploymentsGeneral.envSecrets. Preserve these references when editing the complete values.yaml:
External PostgreSQL and Redis
For an external PostgreSQL server, disable the built-in StatefulSet and PVC and change the connection settings:
Create the database and user before installing CodeScoring.Save.
For an external Redis server, disable the built-in StatefulSet, Service, and PVC:
S3-compatible object storage
The current chart does not deploy an S3-storage. For a standard Kubernetes installation, create a bucket in an external S3-compatible storage service before installation and specify its connection settings:
For storage services that support virtual-hosted-style URLs, set S3_FORCE_PATH_STYLE: "false".
The S3_REGION parameter is required. For MinIO and other storage services without regions, specify any value, for example us-east-1. To place several installations in one bucket, give each of them its own key prefix in S3_PREFIX.
Large file uploads
Save writes a file that does not exceed one part (8 MiB by default) to S3 with a single request. A larger file is written with a multipart upload: it is split into parts, and several parts are sent in parallel. As a result, Save stores artifacts larger than 5 GB (the S3 limit is 5 TiB per object) and is not affected by the MinIO limit of 16 MiB per chunk (the chunk too big error).
The default values usually fit. If needed, change them in configMaps.save-backend-envs.data:
Keep in mind:
- Memory. One multipart upload takes about
(S3_MULTIPART_CONCURRENCY + 2) × S3_MULTIPART_PART_SIZE_MB, and a pod in the worst case runsS3_MULTIPART_MAX_INFLIGHTsuch uploads. With the default values, this is 16 × 4 × 8 MiB = 512 MiB. Take this into account inresources.limits.memoryfor backend and worker. S3 allows no more than 10,000 parts, so for very large files (over ~78 GiB with 8 MiB parts) the part size grows automatically, and such an upload needs proportionally more memory. - MinIO. Do not set
S3_MULTIPART_PART_SIZE_MBabove16: MinIO over https rejects chunks larger than 16 MiB. - Temporary files. Before sending a file to S3, Save often stores the incoming file in the pod temporary directory (
/tmpby default): this is how Docker layers, RPM and Composer packages, and any streams that cannot be re-read are processed. The whole file is never kept in memory, but the disk needs free space of at least the total size of the files uploaded at the same time. In Kubernetes, this is the node ephemeral storage: reserve enoughephemeral-storagefor backend or mount a separate volume and set its path in theTMPDIRvariable. - Request time. Save waits for a single HTTP request for no longer than 10 minutes — within this time, the file must be both received and written to S3. For example, a 5 GB file requires at least ~10 MB/s. The ingress and load balancer timeouts must be at least as long (see Ingress).
- Interrupted uploads. If an upload breaks off, Save aborts the multipart upload in S3. If the abort fails (for example, S3 was unavailable), the parts already uploaded stay in the bucket and take up space. In AWS S3 and Yandex Object Storage, configure an
AbortIncompleteMultipartUploadlifecycle rule for the bucket (for example, 1 day). MinIO removes incomplete uploads by itself (thestale_uploads_expiryparameter, 24 hours by default). - Checksums. If
S3_ENDPOINTis set, Save adds checksums to requests only when the storage requires them — this improves compatibility with MinIO, Ceph, and other S3 storage services. The behavior can be changed with the standard AWS SDK variablesAWS_REQUEST_CHECKSUM_CALCULATIONandAWS_RESPONSE_CHECKSUM_VALIDATION(when_supportedorwhen_required). - Custom S3 certificate. If S3 is served over https with a certificate issued by a corporate certificate authority, set the path to its certificate in
AWS_CA_BUNDLE.
Hosted installation in k3s
The hosted option is intended for a single-server k3s installation and uses SQLite for metadata and the local filesystem for artifacts.
Install k3s and configure cluster access:
To switch the standard chart profile to hosted, change three variables in configMaps.save-backend-envs.data:
The save-backend-envs ConfigMap is attached to backend, worker, scheduler, and auth, so no additional ConfigMap is required. Preserve the other settings from the distributed values.yaml.
The worker and scheduler components require PostgreSQL and do not support SQLite. Both deployments must be disabled in a hosted installation:
Persistent data storage
A hosted installation requires three separate persistent volumes:
db-data— the backend SQLite database;auth-data— the auth SQLite database;storage— filesystem artifact storage.
Attach the PVCs to the deployments through volumes and volumeMounts. For auth, also set an empty initContainers value to remove the inherited wait-for-postgres init container:
Backend and auth use the same DATABASE_SQLITE_PATH value but separate PVCs, so each service stores its own database in its /app/data directory. The default STORAGE_ROOT_PATH is ./storage, which corresponds to the /app/storage mount point when the working directory is /app.
For Dynamic Volume Provisioning, specify a StorageClass for each PVC. The corresponding PVs will be created automatically:
Choose the volume sizes and storageClassName according to the workload and cluster configuration.
You can also use pre-created PVs of any Kubernetes-supported type. In this case, create the PVs separately or describe them under pvs, and set an empty storageClassName and the corresponding volumeName in each PVC, for example:
In this example, for the codescoring-save Helm release, the chart creates the codescoring-save-db-data PV from pvs.db-data, and the PVC binds to it explicitly through volumeName. Replace the NFS server address and export path with values from your infrastructure.
Create and bind the auth-data and storage volumes in the same way. The volume source can be NFS, CSI, a local disk, or another storage system; its parameters depend on the cluster infrastructure.
Optional Redis
Redis is optional for a hosted installation. To run Save without Redis, disable it in the application configuration and disable the related StatefulSet, Service, and PVC:
If Redis is used, keep REDIS_ENABLED: "true", and leave its StatefulSet, Service, and PVC enabled.
SQLite supports a single replica. Do not increase deploymentsGeneral.replicas, and disable autoscaling for backend and worker:
The application no longer uses PostgreSQL after this switch. You can also disable its unused StatefulSet and PVC:
SQLite and filesystem storage are suitable only for a single-server installation. Keep the SQLite database and artifact directories on persistent k3s storage and back them up regularly. Do not use this profile for a multi-replica deployment.
Ingress
Ingress is disabled by default. Enable it and replace the domain name to expose the web interface and API:
If TLS is terminated by an external load balancer, configure tls and the Ingress annotations according to the cluster infrastructure.
To let large files (Docker layers, archives, packages) pass through the Ingress, remove the request size limit and increase the timeouts to 10 minutes — this is how long Save waits for a single request. Example for ingress-nginx:
Without these settings, ingress-nginx rejects requests larger than 1 MB by default with the 413 Request Entity Too Large error.
HTTPRoute (Gateway API)
If the cluster uses Kubernetes Gateway API, configure a route to the frontend through the httpRoutes section. The referenced Gateway resource must exist before CodeScoring.Save is installed.
Set gatewayName, gatewayNamespace, gatewaySectionName, and hostnames to match the Gateway and DNS name in your cluster.
The backendRefs.name value must be formed as {helm-release-name}-frontend. For example, with helm install codescoring-save ..., specify name: codescoring-save-frontend.
OSA Proxy
OSA Proxy is disabled by default. To enable it set the osa-proxy.enabled field to true. In the existing osa-proxy.config.content block, replace codescoring.url with the CodeScoring installation URL. Pass the token through the osa-proxy Secret:
If component checks through OSA Proxy are not required, disable the dependency:
osa-proxy.config.content contains the complete service configuration. Preserve the remaining sections from the distributed values.yaml when modifying it.
Resource limits
The distributed values.yaml does not limit the main component containers. For a production installation, set requests and limits for every component based on load testing:
Configure deployments.scheduler, deployments.auth, deployments.save-frontend, statefulSets.save-postgresql, and statefulSets.save-redis in the same way. CPU requests must be defined for HPA to work correctly with scalable containers.
External Secrets
The chart can obtain secrets from an external store through External Secrets Operator. Install the operator and create a SecretStore or ClusterSecretStore in the cluster first.
Enable the required resource under vaults:
Then attach the generated Secret to the deployments:
For the built-in PostgreSQL, configure vaults.postgresql-secrets-external in the same way and attach the Secret under statefulSets.save-postgresql.envSecrets.
Installation
From the unpacked chart directory, run:
To update an existing installation, use the same configured values.yaml version:
Verifying the installation
Check the resource status:
All Pods must reach the Running state and their containers must be ready. Use component logs for diagnostics:
After installation, open the Ingress URL, sign in with the password specified in AUTH_ADMIN_PASSWORD, and enter the activation key.
Activation key
Until the activation key is entered, Save responds with 403 and the No valid license message to API and repository requests. Without the key, only sign-in, health checks, Swagger, and the activation key page work.
The activation key is provided by the vendor. There are two ways to enter it.
In the web interface.
- Open
Settings -> Activation key. - Click Add new activation key in the top-right corner.
- Paste the key into the Enter new key here field and click Activate.
The new key replaces the current one. The Activation key page shows the key state (Status), the issue date (Issued at), the validity period (Expires in), and the number of artifacts (Artifacts).
Through the API:
The response contains the key state: is_valid, the owner (owner), the issued_at and expires_at dates, the remaining time expires_in (in milliseconds), and the message explanation.
To check the key state and the artifact counter:
Each pod remembers the result of the activation key check for up to 2 hours. With Redis, a new key is visible to all pods immediately. Without Redis, a pod that has already remembered that there is no key may respond with 403 for up to 2 hours — after entering the key, restart the backend pods: kubectl rollout restart deployment -n codescoring-save.
Next steps
After a successful installation:
