Running in production
The container from Getting started is already the production server - there is no other build of it. Running it for a team is a matter of four decisions: where the store lives, how many servers serve it, how it is reached over TLS, and how people sign in.
Where the store lives
Everything the server holds is in one store, chosen at startup with JENREPO_:
JENREPO_ |
Store | Required setting |
|---|---|---|
(unset) or filesystem |
A directory - a Docker volume, a disk, a network share | JENREPO_ |
s3 |
AWS S3, or any S3-compatible store such as MinIO or Ceph | JENREPO_ |
gcs |
Google Cloud Storage | JENREPO_ |
azure-blob |
Azure Blob Storage | JENREPO_ |
A store that is named but missing a required setting stops the server at startup with a message naming what is missing - and so does a second store that is fully configured beside the selected one, because a deployment writing to a store nobody reads is the one mistake that loses data quietly.
S3. Credentials come from the usual AWS chain - an instance or task role, a profile, environment variables - so a server on AWS usually needs none in its configuration:
JENREPO_STORE=s3
JENREPO_S3_BUCKET=my-artifacts
JENREPO_S3_REGION=eu-central-1 # us-east-1 by default
JENREPO_S3_ENDPOINT=https://minio.internal:9000 # only for an S3-compatible store
JENREPO_ and JENREPO_ supply keys explicitly, and
JENREPO_ encrypts with a KMS key instead of the default server-side encryption.
Google Cloud Storage authenticates with Application Default Credentials - Workload Identity on GKE and Cloud
Run - or with a service-account key file named in JENREPO_.
Azure Blob takes the storage account's connection string, and JENREPO_ names the
container (jenesis-repository by default).
Every object store must be reached over https, and at startup the server checks that the store honours the
conditional writes it relies on - some S3-compatible services do not, and are refused with the reason.
Several servers
On an object store the server keeps no state of its own, so any number of them can serve one bucket behind a load balancer: they coordinate through the store alone, with no lock service and no database. A shared filesystem works the same way, provided it honours file locks - NFS without its lock daemon does not, and must not be shared. Background work, such as the scheduled walks, is shared out between the servers rather than repeated by each.
Set JENREPO_ on every server to have them compare notes: each then records a small
fingerprint of what it has seen, and a server that has fallen behind, runs with different settings, or answers a
path differently from its peers is reported on the Security posture page. Give each server a stable name with
JENREPO_ - the host name is used otherwise - so a restarted server is recognised as itself.
TLS and a reverse proxy
Clients should reach the repository over https - some, such as the Go command, refuse to send a credential
otherwise. Put the server behind the load balancer or reverse proxy you already run, terminate TLS there, and tell
the server about it:
JENREPO_PUBLIC_URL=https://repo.example.com # the address clients use, for the links the server generates
JENREPO_TRUSTED_PROXIES=10.0.0.0/8 # whose X-Forwarded-* headers to believe
The console's session cookie is only ever sent over https, so the console, too, is used through the proxy.
Signing people in
Replace the administrator key from Getting started with your identity provider - OpenID Connect, GitHub or LDAP, as Access describes - and name your administrators:
JENREPO_UI_OIDC_ISSUER_URI=https://login.example.com/realms/main
JENREPO_UI_OIDC_CLIENT_ID=jenesis
JENREPO_UI_OIDC_CLIENT_SECRET=…
JENREPO_UI_ADMINS=oidc/8f3c1a…
Then remove JENREPO_ if you set one, set JENREPO_ and restart: every person now
signs in as themselves, and every change they make is attributed to them.
The Helm chart
On Kubernetes, the jenesis chart deploys the same image with a service, probes and a volume or an object store.
It is published beside the image, one chart version per release, and each version deploys the image released with
it - so pin the release you want with --version:
helm install jenesis oci://registry-1.docker.io/jenesisbuild/jenesis --version 1.0.0 \
--set store.backend=s3 --set store.s3.bucket=my-artifacts \
--set ui.oidc.issuerUri=https://login.example.com/realms/main \
--set ui.oidc.clientId=jenesis \
--set secrets.oidcClientSecret=…
| Value | Meaning |
|---|---|
store. |
filesystem (a 20 GiB volume by default), s3, gcs or azure-blob, with the backend's own values beside it |
ui., ui., ui. |
Sign-in, as above |
secrets. |
Credentials - store keys, client secrets - rendered into a Secret, or secrets. to use your own |
secrets. |
On by default: generates JENREPO_, the key settings holding a secret are stored encrypted with, into a Secret of its own on install, and keeps it across upgrades and on uninstall. A key in secrets. takes precedence |
repository. |
Any other setting, as JENREPO_ - for example repository. |
ingress. |
An ingress in front of the service |
The server listens on 8080, and the chart points its liveness and readiness probes at /
and /.
Checking the image
The published image and chart are signed by digest, keylessly, with the identity of the workflow that publishes
them, and the image's CycloneDX SBOM is attested to the same digest. cosign checks both against Sigstore's public
transparency log, with no key to fetch first:
cosign verify \
--certificate-identity-regexp '^https://github\.com/jenesis/jenesis-repository/\.github/workflows/publish-images\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
docker.io/jenesisbuild/jenesis-repository:<version>
cosign verify-attestation --type cyclonedx \
--certificate-identity-regexp '^https://github\.com/jenesis/jenesis-repository/\.github/workflows/publish-images\.yml@' \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
docker.io/jenesisbuild/jenesis-repository:<version>
The chart verifies with the same two flags, as docker..
On a cloud
For a managed container service there is a template per cloud in the repository's
deploy/ folder. Each one provisions that
cloud's object store, selects it, and runs the published image over it with the store's credential wired in, so a
first deployment is one command:
| Cloud | Template | Runs on | Store |
|---|---|---|---|
| Google Cloud | deploy/ (Terraform) |
Cloud Run | gcs, as the service's own account |
| AWS | deploy/ (CloudFormation) |
ECS Fargate behind a load balancer | s3, as the task role |
| Azure | deploy/ (Bicep) |
Container Apps | azure-blob |
| Scaleway | deploy/ (Terraform) |
Serverless Containers | s3, as an IAM application's key |
cd deploy/gcp
terraform init
terraform apply -var project_id=my-project -var bucket_name=my-artifacts \
-var 'secrets={JENREPO_BOOTSTRAP_KEY="jenk_…", JENREPO_UI_ADMIN_KEY="…"}'
Every template takes the image as a parameter that defaults to latest; pin a release for a deployment that should
not move on its own. Because authentication is on, each takes the two starter credentials as secrets - the API's
bootstrap key and the console's starter key - and any other setting as an environment variable by its JENREPO_
name. The Google Cloud, Azure and Scaleway services start private, reachable only through the cloud's own access
control, until a parameter publishes them; the AWS load balancer is public from the start and answers on plain
HTTP until you give it a certificate. The folder's README says what every template takes.
Backups
The store is the only state, so a backup is a copy of it: the volume or directory, or the bucket, with the
snapshot or replication tooling you already use. A copy restores to any backend - copy the objects with their
names unchanged, point JENREPO_ at the new place, and start the server. Settings → Settings also exports
the runtime settings alone, as one file, which is worth keeping beside a large configuration change.