skip to content

How would you make a Jenkins controller's plugin set reproducible instead of installing plugins by clicking in the Plugin Manager?

level: seniorimportance: should knowfreq 42%

answer

  1. controller as a build output, not a pet
  2. pinned plugins.txt at image build
  3. reference directory copied into JENKINS_HOME
  4. JCasC YAML for the rest of the config
  5. block UI installs or the file lies

basics

~20 s

Describe the plugin set as a pinned list in a file, install it at image-build time with jenkins-plugin-cli, and keep controller configuration in Configuration as Code YAML. Upgrades become a reviewed diff; rollback is redeploying the previous image.

solid answer

~40 s

Treat the controller as a build output rather than a pet. Keep a `plugins.txt` listing every plugin with an explicit version, and in the Docker image build run `jenkins-plugin-cli --plugin-file` against it: the official `jenkins/jenkins` image installs into `/usr/share/jenkins/ref/plugins`, and the entrypoint copies that reference tree into `JENKINS_HOME` at startup. Pair it with the Configuration as Code plugin — a `jenkins.yaml` pointed at by `CASC_JENKINS_CONFIG` — so credentials wiring, security realm and tool locations are code too. Then an upgrade is a pull request that changes one version line, CI can build and boot the image, and rollback is redeploying the previous tag. Close the loop by restricting who holds Overall/Administer, otherwise someone installs from the UI and the file silently stops describing reality.

code

dockerfile · 8 lines
dockerfile
FROM jenkins/jenkins:lts-jdk21

# Pin an exact LTS tag or digest in production rather than a floating tag.
COPY --chown=jenkins:jenkins plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN jenkins-plugin-cli --plugin-file /usr/share/jenkins/ref/plugins.txt

COPY --chown=jenkins:jenkins jenkins.yaml /var/jenkins_home/casc.yaml
ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc.yaml

go deeper

for a junior

Know that plugin sets can be declared in a plugins.txt file and installed when a Jenkins image is built, instead of being clicked in one at a time.

for a middle

Explain the mechanics: jenkins-plugin-cli resolves dependencies at build time into the reference directory, the entrypoint copies it into JENKINS_HOME, and JCasC applies the rest of the configuration from YAML.

for a senior

Show the operational payoff and the limits — reviewed upgrades, boot-tested images, redeploy-to-rollback, and the state in JENKINS_HOME that still needs backups and a drift check.

for a principal

Own the policy: who may change the pinned file, whether the controller is disposable or long-lived, and whether an internal update mirror is worth running for supply-chain control.

## The problem with clicking Install A controller configured through the Plugin Manager has no record of *why* it looks the way it does. The plugin set exists only as files in `$JENKINS_HOME/plugins`, assembled over years by several administrators, with transitive dependencies mixed in among deliberate choices. Nobody can review it, nobody can diff it, and rebuilding it after a disaster means restoring a backup and hoping. The fix is to make the plugin set an artefact of a build you can read. ## The mechanism The official `jenkins/jenkins` image ships a tool called `jenkins-plugin-cli`. Given a file of plugin coordinates, it resolves dependencies and downloads everything into the image's reference directory: ``` configuration-as-code:<pinned-version> git:<pinned-version> workflow-aggregator:<pinned-version> kubernetes:<pinned-version> ``` Each line is `shortname:version`. Omitting the version means "latest at build time", which defeats the point — pin it. At container startup the image's entrypoint copies `/usr/share/jenkins/ref` into `JENKINS_HOME`, so a fresh volume comes up with exactly the set the image was built with. The plugin list is only half the controller. The **Configuration as Code** plugin reads a YAML document identified by the `CASC_JENKINS_CONFIG` environment variable and applies security realm, authorization strategy, credential providers, cloud/agent templates and tool installations from it. Together the two files describe a controller you can recreate from scratch. ## What this buys you - **Reviewability.** A plugin upgrade arrives as a one-line diff with an author, a reviewer and a reason in the commit message. - **Rehearsal.** CI can build the image and boot it headless to prove the plugin set loads before it reaches anyone's controller. - **Rollback in one move.** Redeploy the previous image tag. Compare that with recovering a broken upgrade by editing `.jpi` files on a controller that will not start. - **Honest inventory.** The file is the answer to "what do we run and why", including for an audit or an advisory sweep. ## What it does not fix This makes the controller's *configuration* reproducible, not its *state*. `JENKINS_HOME` still holds build history, fingerprints, queue state and — unless you source them externally — credentials. You still need a backup strategy for the volume, and job definitions should come from SCM (a `Jenkinsfile` per repository, multibranch discovery, or a seed job) rather than living only in `config.xml` files nobody generated. There is also a subtlety in the reference-directory copy: it populates a directory that does not yet have the file, so a long-lived `JENKINS_HOME` volume that already contains an *older* copy of a plugin may not be overwritten by a new image unless you force it. Teams that want strict image-defines-plugins semantics either treat the plugins directory as disposable or force the override explicitly. Know that the interaction exists — silently running last month's plugin from a stale volume is a genuinely confusing outcome. ## Closing the drift loop None of this holds if an administrator can still install from the UI. Options, roughly in order of strength: 1. Keep `Overall/Administer` to a very small group and make plugin changes go through the repository. 2. Point the update site at an internal mirror carrying only vetted plugins, so an ad-hoc install cannot reach arbitrary code. 3. Treat the controller as replaceable: if it is rebuilt from the image on a regular cadence, drift is erased rather than argued about. A useful detection step is a scheduled comparison of the running controller's `/pluginManager/api/json?depth=1` output against the pinned file; any difference is drift to investigate, and it is a five-line script. ## Answering it in an interview Name the two files (`plugins.txt`, `jenkins.yaml`), the tool (`jenkins-plugin-cli`), and the environment variable (`CASC_JENKINS_CONFIG`). Then say what changes about the *process*: upgrades become pull requests, rollback becomes a redeploy, and the controller stops being the only place its own configuration is recorded. Finish with the honest limits — build history and credentials still need a backup, and the approach only survives if UI installs are actually prevented.

  • What part of the controller does this approach still leave un-reproducible?
    Everything stateful in `JENKINS_HOME`: build history and artefacts, fingerprints, queue state, and credential material unless it comes from an external store at boot. Job definitions should also move to SCM — a Jenkinsfile per repository with multibranch discovery, or a seed job — otherwise the jobs remain hand-made even though the platform under them is not.
  • How do you stop an administrator from installing a plugin in the UI and drifting from the file?
    Limit Overall/Administer to a small group, point the update site at an internal mirror of vetted plugins, and rebuild the controller from the image often enough that drift is erased rather than negotiated. Detection is cheap too: compare `/pluginManager/api/json?depth=1` against the pinned list on a schedule and alert on any difference.
  • Is pinning every plugin version worth the maintenance cost of updating the file?
    Yes, because the cost is visible and the alternative's cost is not. Floating versions mean two builds of the same Dockerfile produce different controllers and you cannot reproduce yesterday's working set. Automate the toil instead: a scheduled job that proposes version bumps as a pull request keeps the file current without giving up determinism.

saying these in an interview costs you the question

  • Says the controller is documented by its own UI
  • Uses floating latest versions and calls it pinned
  • Assumes JCasC also captures build history
  • Believes an image rebuild alone prevents UI drift
  • Plans rollback by reinstalling plugins one at a time

context