Configuration as code (JCasC)
A reproducible, reviewable Jenkins.
A hand-built Jenkins is a house with no blueprints. You set it up by clicking through Manage Jenkins: pick a security realm on one screen, tick an authorization box on another, paste the instance URL somewhere else, then open each plugin and fill in its settings by hand. That is a hundred small decisions, none of them written down anywhere. Six months later a disk dies, and nobody can say which of those two hundred checkboxes were changed, or why. What you had was a snowflake: unique, hand-made, undocumented, impossible to rebuild. JCasC (Jenkins Configuration as Code, a plugin) ends that. Two terms before the file. A security realm is the identity source, the thing that decides who is allowed to log in at all. An authorization strategy is the rulebook for what each logged-in person may then do. JCasC writes down both of those, plus global tools, agent clouds and every plugin's settings, in one YAML (a plain-text configuration format) file you keep in version control. Jenkins then assembles itself from that file. Standing up a fresh instance stops being a day of remembering and becomes one command that reads a file.
jenkins:systemMessage: "Managed by Configuration as Code — do not edit via the UI"numExecutors: 0 # controller runs no builds (agents only)securityRealm:local: # Jenkins' own user databaseallowsSignup: false # no public self-registrationusers:- id: "admin"password: "${ADMIN_PASSWORD}" # resolved from env at boot, never inlineauthorizationStrategy:globalMatrix: # matrix authorization: a permission gridentries:- group:name: "authenticated" # every logged-in userpermissions:- "Overall/Read"- "Job/Build"- user:name: "admin" # one real administratorpermissions:- "Overall/Administer"security:scriptApproval:approvedSignatures: [] # no pre-approved Groovy — start closedunclassified:location:url: "https://jenkins.acme.internal/"adminAddress: "[email protected]"
The file is organised under a handful of top-level roots, like labelled drawers in a filing cabinet. jenkins: configures the core node. The controller is Jenkins' central server, the machine that schedules work, serves the web interface and stores every secret. numExecutors: 0 gives that machine zero build slots. (An executor is one build slot, one place a single build can run.) So the controller still hands work out, but it runs none of it; the actual work goes to agents, the separate worker machines connected to it. That is a deliberate security decision. Build code should never run on the box that holds the credentials store. security: configures the security subsystem plugins, script approval among them. unclassified: is the junk drawer, the catch-all for global settings that plugins register but that do not fit a fixed section, which is where the public URL lives. Under authorizationStrategy you see globalMatrix, matrix authorization: a grid with permissions along one axis and identities along the other. Here the built-in authenticated group gets read and build access, and the admin user alone gets Overall/Administer. Naming users and groups as their own explicit entries, rather than using the older one-line short form, avoids the ambiguity warning that newer matrix-auth versions print.
Notice that the password is written as ${ADMIN_PASSWORD} and not as the real thing. This YAML lives in Git, where everyone with repository access can read it, so a literal secret in there is a leak waiting to happen. JCasC resolves ${VAR} at load time, pulling the value from an environment variable or from a mounted secrets file. The file describes the shape of your Jenkins. The actual values are injected at boot and never committed. That separation is the whole credential story for JCasC: the credentials store still holds the secrets, and the YAML only points at them by name.
Apply it at boot
You tell Jenkins where the file is with one environment variable, CASC_JENKINS_CONFIG. It can name a single file, a directory of YAML files that JCasC merges together, or even an HTTP URL. On startup the plugin reads it and applies every setting before Jenkins finishes coming up, so a brand-new container is fully configured the first moment you can reach it. Pass -Djenkins.install.runSetupWizard=false and there is no setup wizard either. No clicking at all. A clean apply is quiet, which catches people out: JCasC logs its per-source detail at FINE, so at the default INFO level you see nothing but Jenkins reporting that it is ready. To find out what actually landed, you export the live config, shown further down. Running the same file twice is safe. JCasC is idempotent, meaning it drives each value to exactly what the YAML says regardless of the current state, so a boot and a reload behave identically. Pointing at a directory is the usual production choice, because you can split security, clouds and tooling into separate files that different people review.
# CASC_JENKINS_CONFIG can be a file, a directory of YAML, or an HTTP URL.$ docker run -d --name jenkins \-e CASC_JENKINS_CONFIG=/var/jenkins.d \-e JAVA_OPTS="-Djenkins.install.runSetupWizard=false" \-e ADMIN_PASSWORD="$(openssl rand -base64 24)" \-e CASC_RELOAD_TOKEN="$(openssl rand -hex 24)" \-v "$PWD/casc:/var/jenkins.d:ro" \-v jenkins_home:/var/jenkins_home \jenkins/jenkins:lts-jdk21$ docker logs jenkins 2>&1 | grep -i 'fully up'
9f3c1a2b7e4d0a6c8b5f2d1e... # container id printed by `docker run -d`# CASC_JENKINS_CONFIG applied silently (its detail logs at FINE); INFO shows only:INFO hudson.WebAppMain$3#run: Jenkins is fully up and running# no "Please use the following password to proceed to installation" banner — the# wizard is off and JCasC already created the admin user.
Reload without a restart
Configuration also applies without downtime. Once a change to jenkins.yaml lands through a pull request, you have three ways to re-apply it. Click Manage Jenkins, then Configuration as Code, then Reload existing configuration. Run the reload-jcasc-configuration command over the Jenkins CLI (command line interface). Or send an HTTP POST, the kind of request that submits data rather than fetching it, to the /reload-configuration-as-code/ endpoint with a shared secret token set through the CASC_RELOAD_TOKEN environment variable, which is what a deploy pipeline usually does. Hold on to this mental model: JCasC does not watch Git and does not poll it. It reads the file at boot, or when you explicitly ask for a reload, and at no other moment. A merged change does nothing to the running instance until one of those happens. A reload also stamps the file back over any drift, so a setting somebody quietly flipped in the web interface snaps back to whatever Git says.
# jenkins.yaml changed via a merged pull request — re-apply WITHOUT a restart.# 1) token endpoint (what a deploy pipeline calls; POST, no login, just the shared token):$ curl -sS -X POST -o /dev/null -w 'HTTP %{http_code}\n' \"https://jenkins.acme.internal/reload-configuration-as-code/?casc-reload-token=$CASC_RELOAD_TOKEN"# 2) or authenticated over the Jenkins CLI (prints nothing, exit 0 on success):$ java -jar jenkins-cli.jar -s https://jenkins.acme.internal/ \-auth admin:$ADMIN_API_TOKEN reload-jcasc-configuration
HTTP 200# curl printed only the status line above. A successful reload is silent in the# controller log at the default level; a failure logs at SEVERE# ("Failed to reload configuration"), so a clean HTTP 200 with no error line# means the new YAML is now live.
Because the file is the source of truth, every configuration change turns into a code review. Loosening authorization, adding a credentials provider, trusting a new agent cloud: each one shows up as a diff that a named person approved at a recorded time. The audit trail falls out of the workflow instead of being extra paperwork. This pairs directly with the next lesson on backup and recovery. JCasC captures the configuration, which is safe to commit because the secrets in it are only interpolated references, while a JENKINS_HOME backup captures the state and the encrypted secret values. Recovery is then two moves: replay the YAML to rebuild the shape of the instance, and restore JENKINS_HOME for its history and its secrets.
Verify what actually applied
Applying is not the same as verifying, so confirm that the live state matches what you intended. The plugin can dump the running configuration back out as YAML: the Export configuration button at /configuration-as-code/, or a POST to /configuration-as-code/export when you are scripting it. That endpoint accepts POST only, and an API token in the request is exempt from the CSRF crumb (cross-site request forgery token, the anti-forgery check Jenkins normally demands on writes). Diffing that export against your committed file is how you catch a setting some plugin ignored or quietly rewrote. Expect two things. The export is fully expanded, so it includes defaults you never wrote and sorts keys alphabetically, which makes it read noisier than your source. And an invalid key never fails silently. A mistyped attribute throws a ConfiguratorException that names the offending element, and it shows up right in the boot log as your first troubleshooting clue.
# dump the LIVE config (export is POST-only; the API token skips the CSRF crumb),# then pull out just the section you care about$ curl -sS -u admin:$ADMIN_API_TOKEN -X POST \https://jenkins.acme.internal/configuration-as-code/export -o live.yaml$ yq '.jenkins.authorizationStrategy' live.yaml
globalMatrix:entries:- group:name: authenticatedpermissions:- Job/Build # exporter sorts permissions alphabetically,- Overall/Read # so authoring order is not preserved- user:name: adminpermissions:- Overall/Administer
One boundary is worth learning early. JCasC manages global configuration, not content. It sets up security, clouds and tools, but it does not create your pipeline jobs. Those come from Job DSL or, better, from Multibranch pipelines that go and find Jenkinsfiles in your repositories. And it configures only plugins that are already present. It does not install them. Keeping that line clear heads off the two most common beginner failures: waiting for a job to appear out of jenkins.yaml, and expecting the file to fetch a plugin it names.
Try this
Work through “Verify what actually applied” yourself on a sandbox you can throw away, following the commands above in order. Then break one step deliberately and re-run, so you have seen the failure before it finds you.
Takeaway
The trap worth remembering here: jCasC applies configuration. It does not install the plugins that configuration needs. Check that on your own systems before you need to, because it is cheaper to find on a quiet afternoon than during an incident.