Stop the error-prone manual UI configuration, manage the entire controller through a jenkins.yaml file, and provision an identical Jenkins instantly using Docker and JCasC.

In episode 12 we eliminated static credentials with OIDC. Now let's look at another equally big problem: how is the Jenkins configuration itself managed?
Most Jenkins instances in the world are configured manually through the admin UI — click here, click there, fill a form, save. This approach, often called Configuration ClickOps, is fragile. There is no audit trail of who changed what, one wrong choice in a form can break the controller, and when a server dies or an environment must be rebuilt, all configuration has to be redone from scratch based on memory alone. Imagine having to reinstall a laptop from zero without any record of which applications were installed — that is ClickOps during disaster recovery.
The solution is Jenkins Configuration as Code (JCasC): the entire controller configuration is written as a declarative file named jenkins.yaml, stored in a Git repository, and applied automatically when Jenkins starts. In this episode we will build a Jenkins that is fully defined by code — complete with security, credentials, tools, and plugins.
Manual UI configuration has three fundamental weaknesses:
JCasC turns the configuration file into the single source of truth. Changes are made through pull requests, can be reviewed, can be tested, and produce an identical Jenkins in any environment.
The Configuration as Code plugin lets Jenkins accept the entire configuration in a single YAML file, covering: system settings, security realm, authorization strategy, credentials, nodes, tools such as JDK and Maven, and even plugin settings. The file's location is given through the environment variable CASC_JENKINS_CONFIG — it can be a single file or a directory containing many YAML files that get merged.
When Jenkins boots for the first time, JCasC reads that file and applies all the settings automatically. There is one important limitation to understand:
Note
JCasC manages controller configuration — not job definitions. Job pipelines still live in their respective repositories (Pipeline as Code). To declare jobs centrally, combine it with the Job DSL plugin or jenkins-jobs.yaml.
Here is an example jenkins.yaml that declares security, credentials, and tools in one file:
jenkins:
systemMessage: "Jenkins dikelola dengan JCasC - jangan ubah manual!"
numExecutors: 0
authorizationStrategy:
globalMatrix:
permissions:
- "Overall/Administer:admin"
- "Overall/Read:authenticated"
- "Job/Read:developer"
securityRealm:
local:
allowsSignup: false
credentials:
system:
domainCredentials:
- credentials:
- usernamePassword:
scope: GLOBAL
id: docker-hub
username: "${DOCKERHUB_USERNAME}"
password: "${DOCKERHUB_PASSWORD}"
- basicSSHUserPrivateKey:
scope: GLOBAL
id: deploy-key
username: deploy
privateKeySource:
directEntry:
privateKey: "${DEPLOY_PRIVATE_KEY}"
tools:
jdk:
installations:
- name: jdk17
properties:
- installSource:
installers:
- jdkInstaller:
version: "17"
unclassified:
location:
url: "https://jenkins.example.com"
gitSCM:
globalConfigName: "jenkins-bot"
globalConfigEmail: "jenkins@example.com"Note values such as numExecutors: 0 — the controller does not run any builds; all work is directed to agents, following the controller security principle. Credential values are taken from environment variables — JCasC substitutes environment variable references in the YAML file when the controller boots — so secrets are never written in the repository.
Warning
Do not write secret values directly in a jenkins.yaml that goes into Git. Always reference environment variables, and set their values when the container is started. Secrets in a repository are a permanent security hole.
Plugin installation can also be declared through a plugins.txt file — one line per plugin along with its version:
configuration-as-code:1817.vf64c293ec5c1
role-strategy:753.v9ee33920c17a
openid-connect-plugin:2.11.0-1
git:5.6.0
workflow-aggregator:600.vb_57cdd26fdd7
junit:1301.v7396a_4306d48This list becomes a version contract: every Jenkins image is built with the exact same plugin combination, so behavior does not drift between environments.
Combine JCasC with Docker to create an immutable controller: an image containing the entire configuration and plugins, ready to be re-run at any time. Example Dockerfile:
FROM jenkins/jenkins:lts-jdk17
COPY --chown=jenkins:jenkins jenkins.yaml /var/jenkins_home/jenkins.yaml
ENV CASC_JENKINS_CONFIG=/var/jenkins_home/jenkins.yaml
COPY --chown=jenkins:jenkins plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli -f /usr/share/jenkins/ref/plugins.txtBuild the image, then run the container while supplying secrets through environment variables:
docker build -t my-jenkins:1.0 .
docker run -d -p 8080:8080 \
-e CASC_JENKINS_CONFIG=/var/jenkins_home/jenkins.yaml \
-e DOCKERHUB_USERNAME=arman \
-e DOCKERHUB_PASSWORD=rahasia \
-e DEPLOY_PRIVATE_KEY="$(cat ~/.ssh/id_ed25519)" \
-v jenkins-home:/var/jenkins_home \
my-jenkins:1.0In seconds, a fresh Jenkins boots a controller with identical configuration, security, credentials, and plugins. This is the core power of JCasC: reproducibility.
Because configuration lives in Git, every change can be reviewed and serves as an audit trail. If the controller breaks, recovery is simple: rebuild the image, restore the JENKINS_HOME backup, and the controller returns to the same state. No more guessing at settings when the production server goes down.
Tip
If you already have a manually configured Jenkins, use the
Manage Jenkins > Configuration as Code page to see the YAML
representation of the current configuration as a starting point for
writing jenkins.yaml. Once JCasC is active, avoid changing settings
through the UI because the results will be overwritten on the next
reload.
In episode 13 you have understood why Configuration ClickOps is dangerous, the JCasC concept that manages the entire controller through jenkins.yaml, plugin declaration with plugins.txt, and how to combine Docker with JCasC to create an identical immutable controller in seconds. You also know that configuration can now be audited via Git and easily restored in a disaster.
At this point, your CI/CD infrastructure is secure and automated. But one important question remains: is the code being built actually high quality?
In episode 14 we will discuss Automated Testing, Code Quality & SonarQube Integration — publishing test results, measuring coverage, and automatically stopping the pipeline if code fails to meet quality standards. See you there!