• Home
  • About
    • Harshad Ranganathan photo

      Harshad Ranganathan

      Multi-Cloud ☁ | Kubernetes Certified

    • Learn More
    • Email
    • LinkedIn
    • Github
    • StackOverflow
  • Posts
    • All Posts
    • All Tags
  • Projects

Running Jenkins in your local as a Docker Container

04 Oct 2020

Reading time ~12 minutes

Table Of Contents

  • Project
  • Dockerfile
  • Docker Compose
  • Makefile
  • Configuration As Code
  • Plugins
  • Security
    • Access Control
      • Security Realm
      • Authorization Strategy
    • CrumbIssuer (CSRF Protection)
  • Location Configuration
  • Handling Secrets
    • Environment Variables
  • LDAP
    • slapd
      • LDIF Configuration
      • Dockerfile
      • Docker Compose
    • phpLDAPadmin
    • Access Control
      • Security Realm
      • Authorization Strategy
    • Login
  • References

Project

You can find the project with the required files in below repository.

HarshadRanganathan

jenkins-local-container

Run jenkins as a container in your local

  • 0
    Stars
  • 1
    Fork

We’ll will be setting up jenkins to run as a docker container along with below services:

   
slapd stand-alone LDAP daemon
phpldapadmin web-based LDAP client
nexus artifact repository
java agent build agent for running OpenJDK 11 workloads

Dockerfile

Let’s create the dockerfile with all the commands to assemble the Jenkins image.

jenkins/Dockerfile

FROM docker.io/jenkins/jenkins:lts
ARG JAVA_OPTS
ENV JAVA_OPTS "-Djenkins.install.runSetupWizard=false ${JAVA_OPTS:-}"
ENV JENKINS_HOME "/var/jenkins_home"
USER jenkins

Commands defined in the dockerfile are as follows:

  1. Pull the LTS version of jenkins image and use it as a base image
  2. Disable setup wizard on jenkins startup
  3. Set JENKINS_HOME path
  4. Set the username (UID) to use when running the image as jenkins

Docker Compose

Let’s create the docker compose file with the build context, volume mounts and port details.

docker-compose.yaml

version: '3.7'

services:
  jenkins:
    build:
      context: ./jenkins
    ports:
      - 8080:8080
      - 50000:50000
    environment:
      - TZ=Europe/Oslo
    volumes:
      - jenkins_home:/var/jenkins_home

volumes:
  jenkins_home:
Like the content ? 

Makefile

Now, since we have our docker build and compose file we can start up the jenkins container.

You need to run commands to basically first build your image and then to start the container.

Here, we define a Makefile with the set of tasks to be executed. So, it’s easier to just run the make command and it will have the commands to take care of cleaning, building and starting up the containers.

Makefile

.PHONY: all
all: compose-down-remove-local compose-up

.PHONY: compose-build
compose-build:
	docker-compose \
		--file docker-compose.yaml \
		build

.PHONY: compose-ps
compose-ps:
	docker-compose \
		--file docker-compose.yaml \
		ps

.PHONY: compose-up
compose-up:
	docker-compose \
		--file docker-compose.yaml \
		up \
		--build \
		--detach

.PHONY: compose-logs
compose-logs:
	docker-compose \
		--file docker-compose.yaml \
		logs \
		--follow \
		--timestamps

.PHONY: compose-up-logs
compose-up-logs:
	$(MAKE) compose-up \
	&& $(MAKE) compose-logs

.PHONY: compose-down-up
compose-down-up:
	$(MAKE) compose-down \
	&& $(MAKE) compose-up

.PHONY: compose-down
compose-down:
	docker-compose \
		--file docker-compose.yaml \
		down

.PHONY: compose-down-remove-local
compose-down-remove-local:
	docker-compose \
		--file docker-compose.yaml \
		down \
		--remove-orphans \
		--rmi local \
		--volumes

.PHONY: compose-down-remove-all
compose-down-remove-all:
	docker-compose \
		--file docker-compose.yaml \
		down \
		--remove-orphans \
		--rmi all \
		--volumes

Note: Ensure that this file is properly tab separated. Otherwise, it will result in errors. Also, make sure that it uses LF line endings.

Configuration As Code

Setting up Jenkins is a complex process, as both Jenkins and its plugins require some tuning and configuration, with dozens of parameters to set within the web UI manage section.

The Configuration as Code plugin has been designed as an opinionated way to configure Jenkins based on human-readable declarative configuration files. Writing such a file should be feasible without being a Jenkins expert, just translating into code a configuration process one is used to executing in the web UI.

Let’s create a jenkins.yaml file where we define our jenkins configuration options.

jenkins/jenkins.yaml

configuration-as-code:
  version: 1
  deprecated: warn
  restricted: reject

jenkins:
  systemMessage: |-
    Welcome to Jenkins!    ٩(◕‿◕)۶

Here, we are just defining the version and system message to be displayed in the home page after login.

You can get the reference documentation for each of the plugins that you need to configure in jenkins.yaml file from http://localhost:8080/configuration-as-code/reference.

Let’s update our Dockerfile to add this configuration file to our container.

jenkins/Dockerfile

FROM docker.io/jenkins/jenkins:lts
ARG JAVA_OPTS
ENV JAVA_OPTS "-Djenkins.install.runSetupWizard=false ${JAVA_OPTS:-}"
ENV JENKINS_HOME "/var/jenkins_home"
USER jenkins

COPY jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml

Note: This configuration as code approach won't work unless you install the configuration-as-code plugin which we will do in our plugins section

Like the content ? 

Plugins

You can rely on the install-plugins.sh script to pass a set of plugins to download with their dependencies. This script will perform downloads from update centers, and internet access is required for the default update centers.

Let’s create a plugins.txt file with the list of plugins that we need to be pre-installed.

jenkins/plugins.txt

ace-editor:1.1
ant:1.11
antisamy-markup-formatter:2.1
apache-httpcomponents-client-4-api:4.5.10-2.0
bouncycastle-api:2.18
branch-api:2.5.8
build-timeout:1.20
cloudbees-folder:6.14
command-launcher:1.4
configuration-as-code:1.43
credentials:2.3.12
credentials-binding:1.23
display-url-api:2.3.3
durable-task:1.34
echarts-api:4.8.0-2
email-ext:2.73
git:4.3.0
git-client:3.3.2
git-server:1.9
github:1.31.0
github-api:1.115
github-branch-source:2.8.3
gradle:1.36
handlebars:1.1.1
jackson2-api:2.11.2
jdk-tool:1.4
jquery-detached:1.2.1
jquery3-api:3.5.1-1
jsch:0.1.55.2
junit:1.30
ldap:1.24
lockable-resources:2.8
mailer:1.32
mapdb-api:1.0.9.0
matrix-auth:2.6.2
matrix-project:1.17
momentjs:1.1.1
okhttp-api:3.14.9
pam-auth:1.6
pipeline-build-step:2.13
pipeline-github-lib:1.0
pipeline-graph-analysis:1.10
pipeline-input-step:2.11
pipeline-milestone-step:1.3.1
pipeline-model-api:1.7.1
pipeline-model-definition:1.7.1
pipeline-model-extensions:1.7.1
pipeline-rest-api:2.13
pipeline-stage-step:2.5
pipeline-stage-tags-metadata:1.7.1
pipeline-stage-view:2.13
plain-credentials:1.7
plugin-util-api:1.2.2
resource-disposer:0.14
scm-api:2.6.3
script-security:1.74
snakeyaml-api:1.26.4
ssh-credentials:1.18.1
ssh-slaves:1.31.2
structs:1.20
subversion:2.13.1
timestamper:1.11.5
token-macro:2.12
trilead-api:1.0.8
workflow-aggregator:2.6
workflow-api:2.40
workflow-basic-steps:2.20
workflow-cps:2.82
workflow-cps-global-lib:2.17
workflow-durable-task-step:2.35
workflow-job:2.39
workflow-multibranch:2.22
workflow-scm-step:2.11
workflow-step-api:2.22
workflow-support:3.5
ws-cleanup:0.38

Next, we need to pass this file to install-plugins.sh script to install the plugins.

Note: Make sure the plugins.txt file has LF line endings otherwise the plugin downloads will fail

Let’s update our dockerfile with the required commands.

jenkins/Dockerfile

FROM docker.io/jenkins/jenkins:lts
ARG JAVA_OPTS
ENV JAVA_OPTS "-Djenkins.install.runSetupWizard=false ${JAVA_OPTS:-}"
ENV JENKINS_HOME "/var/jenkins_home"
USER jenkins

COPY plugins.txt /usr/share/jenkins/ref/plugins.txt
RUN xargs /usr/local/bin/install-plugins.sh </usr/share/jenkins/ref/plugins.txt

COPY jenkins.yaml /usr/share/jenkins/ref/jenkins.yaml
Like the content ? 

Security

Access Control

You should lock down the access to Jenkins UI so that users are authenticated and appropriate set of permissions are given to them. This setting is controlled mainly by two axes:

  • Security Realm, which determines users and their passwords, as well as what groups the users belong to.

  • Authorization Strategy, which determines who has access to what.

Security Realm

In our jenkins.yaml file, we will define the security realm to use default username and password for authentication.

If we don’t define the security realm, then there will be no login required.

Possible values:

  • legacy
  • local
  • ldap
  • pam
  • none
jenkins:
  securityRealm:
    local:
      allowsSignup: false
      enableCaptcha: false
      users:
        - id: ${JENKINS_ADMINISTRATOR_USERNAME:-administrator}
          password: ${JENKINS_ADMINISTRATOR_PASSWORD:-changeit}

Here, we had utilized variable substitution feature available in configuration file to define the username and password.

For example, id: "${JENKINS_ADMINISTRATOR_USERNAME:-administrator}" will evaluate to administrator if $JENKINS_ADMINISTRATOR_USERNAME is unset in the environment variables.

Authorization Strategy

With respect to authorization strategy, we choose the simplest option which is to allow logged in users to perform any actions.

Possible values:

  • unsecured
  • legacy
  • loggedInUsersCanDoAnything
  • globalMatrix
  • projectMatrix
jenkins:
  authorizationStrategy: "loggedInUsersCanDoAnything"

Here is the complete jenkins.yaml file -

configuration-as-code:
  version: 1
  deprecated: warn
  restricted: reject

jenkins:
  systemMessage: |-
    Welcome to Jenkins!    ٩(◕‿◕)۶
  
  authorizationStrategy: "loggedInUsersCanDoAnything"

  securityRealm:
    local:
      allowsSignup: false
      enableCaptcha: false
      users:
        - id: ${JENKINS_ADMINISTRATOR_USERNAME:-administrator}
          password: ${JENKINS_ADMINISTRATOR_PASSWORD:-changeit}
Like the content ? 

CrumbIssuer (CSRF Protection)

A CrumbIssuer represents an algorithm to generate a nonce value, known as a crumb, to counter cross site request forgery exploits.

Crumbs are typically hashes incorporating information that uniquely identifies an agent that sends a request, along with a guarded secret so that the crumb value cannot be forged by a third party.

Let’s update jenkins.yaml to use the standard crumb issuer.

jenkins:
  crumbIssuer: "standard"

Here is the complete jenkins.yaml file -

configuration-as-code:
  version: 1
  deprecated: warn
  restricted: reject

jenkins:
  systemMessage: |-
    Welcome to Jenkins!    ٩(◕‿◕)۶
  
  authorizationStrategy: "loggedInUsersCanDoAnything"

  securityRealm:
    local:
      allowsSignup: false
      enableCaptcha: false
      users:
        - id: ${JENKINS_ADMINISTRATOR_USERNAME:-administrator}
          password: ${JENKINS_ADMINISTRATOR_PASSWORD:-changeit}

  crumbIssuer: "standard"
Like the content ? 

Location Configuration

We need to configure the location settings to resolve some of the errors shown after jenkins startup.

adminAddress - Notification e-mails from Jenkins to project owners will be sent with this address in the from header.

url - This value is used to let Jenkins know how to refer to itself

unclassified:
  location:
    adminAddress: "Harshad Ranganathan <rharshad93@gmail.com>"
    url: "http://localhost:8080" 

Here is the complete jenkins.yaml file -

configuration-as-code:
  version: 1
  deprecated: warn
  restricted: reject

jenkins:
  systemMessage: |-
    Welcome to Jenkins!    ٩(◕‿◕)۶
  
  authorizationStrategy: "loggedInUsersCanDoAnything"

  securityRealm:
    local:
      allowsSignup: false
      enableCaptcha: false
      users:
        - id: ${JENKINS_ADMINISTRATOR_USERNAME:-administrator}
          password: ${JENKINS_ADMINISTRATOR_PASSWORD:-changeit}

  crumbIssuer: "standard"

unclassified:
  location:
    adminAddress: "Harshad Ranganathan <rharshad93@gmail.com>"
    url: "http://localhost:8080"

Handling Secrets

Environment Variables

Environment variables can be directly read by JCasC when loading configurations.

Secrets can be also be injected using environment variables.

In our configuration file, we had previously defined variables for username/password.

jenkins:
  securityRealm:
    local:
      allowsSignup: false
      enableCaptcha: false
      users:
        - id: ${JENKINS_ADMINISTRATOR_USERNAME:-administrator}
          password: ${JENKINS_ADMINISTRATOR_PASSWORD:-changeit}

Let’s create a new file secrets.env which will contain all our secrets which needs to be injected as environment variables.

jenkins/secrets.env

JENKINS_ADMINISTRATOR_USERNAME=admin
JENKINS_ADMINISTRATOR_PASSWORD=admin123

Then in our docker compose file, we must configure to use this env file.

docker-compose.yaml

version: '3.7'

services:
  jenkins:
    build:
      context: ./jenkins
    ports:
      - 8080:8080
      - 50000:50000
    environment:
      - TZ=Europe/Oslo
    volumes:
      - jenkins_home:/var/jenkins_home
    
    env_file: ./jenkins/secrets.env

volumes:
  jenkins_home:
Like the content ? 

LDAP

Previously, we had configured the login to use default username/password.

We will change it to use LDAP with users and groups configured in the active directory.

slapd

Slapd is a stand-alone LDAP daemon. It listens for LDAP connections on any number of ports (default 389), responding to the LDAP operations it receives over these connections

LDIF Configuration

We can define the data for LDAP database in a LDIF file.

Let’s define some sample organization, users and groups in three LDIF files.

slapd/configurations/ou.ldif

dn: ou=Users,dc=acme,dc=local
objectClass: organizationalUnit
objectClass: top
ou: Users

dn: ou=Groups,dc=acme,dc=local
objectClass: organizationalUnit
ou: Groups

slapd/configurations/users.ldif

dn: cn=user,ou=Users,dc=acme,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: user
sn: Lastname
givenName: Firstname
cn: Acme User
displayName: Acme User
uidNumber: 10003
gidNumber: 8000
userPassword: changeit
homeDirectory: /home/user
mail: user@acme.local

dn: cn=manager,ou=Users,dc=acme,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
uid: manager
sn: Lastname
givenName: Firstname
cn: Acme Manager
displayName: Acme Manager
uidNumber: 10002
gidNumber: 8000
userPassword: changeit
homeDirectory: /home/manager
mail: manager@acme.local

dn: cn=service,ou=Users,dc=acme,dc=local
cn: service
displayName: Acme Service
gidnumber: 8000
givenName: Firstname
homedirectory: /home/service
loginshell: /bin/bash
objectclass: inetOrgPerson
objectclass: posixAccount
objectclass: simpleSecurityObject
sn: Lastname
uid: service
uidnumber: 10001
userPassword: changeit
mail: service@acme.local

dn: cn=superuser,ou=Users,dc=acme,dc=local
objectClass: inetOrgPerson
objectClass: posixAccount
objectClass: shadowAccount
objectclass: simpleSecurityObject
uid: superuser
sn: Lastname
givenName: Firstname
cn: Acme Superuser
displayName: Acme Superuser
uidNumber: 10000
gidNumber: 8000
userPassword: changeit
homeDirectory: /home/superuser
mail: superuser@acme.local

slapd/configurations/groups.ldif

dn: cn=Acme Superusers,ou=Groups,dc=acme,dc=local
objectClass: posixGroup
cn: Acme Superusers
gidNumber: 5000
memberUid: superuser

dn: cn=Acme Servicers,ou=Groups,dc=acme,dc=local
objectclass: posixGroup
cn: Acme Servicers
gidnumber: 5001
memberUid: service

dn: cn=Acme Managers,ou=Groups,dc=acme,dc=local
objectClass: posixGroup
cn: Acme Managers
gidNumber: 5002
memberUid: manager

dn: cn=Acme Users,ou=Groups,dc=acme,dc=local
objectClass: posixGroup
cn: Acme Users
gidNumber: 5003
memberUid: user

Dockerfile

Let’s define the dockerfile which will have the commands to install slapd daemon, required utilities and copy the ldif data files.

slapd/Dockerfile

FROM docker.io/library/debian:10-slim

# References
# https://github.com/rackerlabs/dockerstack/blob/master/keystone/openldap/Dockerfile
# https://github.com/acme/docker-openldap/blob/master/memberUid/Dockerfile
# https://github.com/larrycai/docker-openldap/blob/master/Dockerfile

# https://github.com/hadolint/hadolint/wiki/DL4006#correct-code
SHELL ["/bin/bash", "-o", "pipefail", "-c"]

# install slapd in noninteractive mode
RUN apt-get update \
    && echo "slapd slapd/root_password password changeit" | debconf-set-selections \
    && echo "slapd slapd/root_password_again password changeit" | debconf-set-selections \
    && echo "slapd slapd/internal/adminpw password changeit" | debconf-set-selections \
    && echo "slapd slapd/internal/generated_adminpw password changeit" | debconf-set-selections \
    && echo "slapd slapd/password2 password changeit" | debconf-set-selections \
    && echo "slapd slapd/password1 password changeit" | debconf-set-selections \
    && echo "slapd slapd/domain string acme.local" | debconf-set-selections \
    && echo "slapd shared/organization string Acme" | debconf-set-selections \
    && echo "slapd slapd/backend string HDB" | debconf-set-selections \
    && echo "slapd slapd/purge_database boolean true" | debconf-set-selections \
    && echo "slapd slapd/move_old_database boolean true" | debconf-set-selections \
    && echo "slapd slapd/allow_ldap_v2 boolean false" | debconf-set-selections \
    && echo "slapd slapd/no_configuration boolean false" | debconf-set-selections \
    && DEBIAN_FRONTEND=noninteractive apt-get install \
        --assume-yes \
        --no-install-recommends \
        ldap-utils \
        slapd \
    && rm --force --recursive /var/lib/apt/lists/*

# ca-certificates is already the newest version (20190110).
# openssl is already the newest version (1.1.1c-1).

COPY configurations/*.ldif /tmp/

# TODO: Use initialization scripts if available.
# TODO: Deprecate this Dockerfile when initialization scripts are used.
# https://github.com/osixia/docker-openldap/issues/20

# Workaround DL3001 for `service slapd start` Command
# https://github.com/hadolint/hadolint/wiki/DL3001

RUN mkdir -p /var/ldap/acme \
    && chown --recursive openldap /var/ldap \
    && /etc/init.d/slapd start \
    && ldapadd -H ldapi:/// -f /tmp/ou.ldif -x -D "cn=admin,dc=acme,dc=local" -w changeit -v \
    && ldapadd -H ldapi:/// -f /tmp/groups.ldif -x -D "cn=admin,dc=acme,dc=local" -w changeit -v \
    && ldapadd -H ldapi:/// -f /tmp/users.ldif -x -D "cn=admin,dc=acme,dc=local" -w changeit -v \
    && rm --verbose /tmp/*.ldif

EXPOSE 389

CMD ["slapd", "-h", "ldap:///" ,"-g", "openldap", "-u", "openldap", "-d", "256"]

Docker Compose

Let’s update our root docker compose file to create the slapd container first before the jenkins container.

docker-compose.yaml

version: '3.7'

services:
  jenkins:
    build:
      context: ./jenkins
    ports:
      - 8080:8080
      - 50000:50000
    environment:
      - TZ=Europe/Oslo
    volumes:
      - jenkins_home:/var/jenkins_home
    env_file: ./jenkins/secrets.env
    depends_on:
      - slapd

  slapd:
    build:
      context: ./slapd
    ports:
      - 389:389
      - 636:636

volumes:
  jenkins_home:
Like the content ? 

phpLDAPadmin

phpLDAPadmin (also known as PLA) is a web-based LDAP client. It provides easy, anywhere-accessible, multi-language administration for your LDAP server.

Its hierarchical tree-viewer and advanced search functionality make it intuitive to browse and administer your LDAP directory.

Let’s update the docker compose file to run phpLDAPadmin container.

docker-compose.yaml

version: '3.7'

services:
  jenkins:
    build:
      context: ./jenkins
    ports:
      - 8080:8080
      - 50000:50000
    environment:
      - TZ=Europe/Oslo
    volumes:
      - jenkins_home:/var/jenkins_home
    env_file: ./jenkins/secrets.env
    depends_on:
      - slapd

  slapd:
    build:
      context: ./slapd
    ports:
      - 389:389
      - 636:636

  phpldapadmin:
    image: docker.io/osixia/phpldapadmin:0.9.0
    environment:
      PHPLDAPADMIN_LDAP_HOSTS: slapd
      PHPLDAPADMIN_HTTPS: 'false'
    ports:
      - 8090:80
    depends_on:
      - slapd

volumes:
  jenkins_home:

If you run the docker compose file then you should be able to access the LDAP web client at http://localhost:8090/.

You can login using below credentials which we had earlier configured in our slapd docker file.

Login DN: cn=admin,dc=acme,dc=local
Password: changeit
Like the content ? 

Access Control

Security Realm

We had earlier configured to use local security realm. Since, now we have ldap set up we can update our jenkins configuration to use LDAP for authentication.

jenkins/jenkins.yaml

jenkins:
  securityRealm:
    # local:
    #   allowsSignup: false
    #   enableCaptcha: false
    #   users:
    #     - id: ${JENKINS_ADMINISTRATOR_USERNAME:-administrator}
    #       password: ${JENKINS_ADMINISTRATOR_PASSWORD:-changeit}
    ldap:
      configurations:
      - groupSearchFilter: "(& (cn={0}) (objectclass=posixGroup) )"
        inhibitInferRootDN: false
        managerDN: "cn=service,ou=Users,dc=acme,dc=local"
        managerPasswordSecret: ${LDAP_SERVICE_PASSWORD}
        rootDN: "dc=acme,dc=local"
        server: "ldap://slapd:389"
      disableMailAddressResolver: false
      disableRolePrefixing: true
      groupIdStrategy: "caseInsensitive"
      userIdStrategy: "caseInsensitive"

Update secrets.env file specifying the LDAP password which will be substituted for variable ${LDAP_SERVICE_PASSWORD} in the jenkins configuration file.

jenkins/secrets.env

JENKINS_ADMINISTRATOR_USERNAME=admin
JENKINS_ADMINISTRATOR_PASSWORD=admin123
LDAP_SERVICE_USERNAME=
LDAP_SERVICE_PASSWORD=changeit

Authorization Strategy

Earlier, we had used loggedInUsersCanDoAnything authorization strategy.

Now, we can use globalMatrix strategy to define different authorization levels for the users/groups configured in LDAP.

jenkins:
  #authorizationStrategy: "loggedInUsersCanDoAnything"
  authorizationStrategy: 
    globalMatrix:
      grantedPermissions:
        - "Job/Build:authenticated"
        - "Job/Cancel:authenticated"
        - "Job/Read:authenticated"
        - "Overall/Administer:Acme Superusers"
        - "Overall/Read:authenticated"
        - "View/Read:authenticated"

Here, we have defined that only Acme Superusers can create new jobs and manage jenkins configurations.

Other authenticated users can view, build and cancel jobs.

Login

You can now login into Jenkins using any of the users/password configured previously in LDIF files.

e.g.

username: superuser
password: changeit
Like the content ? 

References

https://github.com/jenkinsci/configuration-as-code-plugin

https://github.com/jenkinsci/docker#preinstalling-plugins

https://www.jenkins.io/doc/book/system-administration/security/

https://github.com/jenkinsci/configuration-as-code-plugin/blob/master/docs/features/secrets.adoc



how to start jenkinsjenkins downloadjenkins dockerjenkins configurationjenkins pipeline docker mount volumepersist jenkins data dockerlocal jenkins dockerjenkins can be configured to connect to ldaprun jenkins pipeline locallyrun jenkins locally dockerjenkins docker localhostjenkins pipeline local repositoryInstallation of Jenkins in a Docker containerRunning job on local jenkins with local repositoryLocal Development Using Jenkins Pipelinesjenkins docker composedocker jenkins tutorialjenkins dockerfiledockerfile to create jenkins imagejenkins global tool configuration dockermigrate jenkins to dockerjenkins docker imagerunning jenkins locallyStarting Jenkins in Docker ContainerLocal Continuous Delivery Environment With Docker Share Tweet +1