How to Make Your Mac a Jenkins Server and Agent

You may be working on a personal project. Maybe your company has a different CI/CD system from what you are used to or it’s hard to be onboarded with the CI/CD system. I’ve come up with a way to create your own personal Jenkins server and agent all in one macOS. The following diagram shows what I am envisioning.

This whole things can easily fit on a laptop or a desktop. Obviously the architecture could be applied to Windows and Linux as well. I have Mac as my main development machine, so I will use Mac as an example here.

Install Docker on Mac

Installing Docker on macOS is very easy.

  • First, download Docker for Mac from here.
  • (Double-)click the downloaded Docker.dmg file.
  • Drag and drop the Docker application file to /Applications directory.
  • Double-click Docker.app file in /Applications directory.
  • You may have to reboot your machine to get the daemon to work correctly.
  • Open Terminal.
  • Execute docker run -d -p 80:80 docker/getting-started
  • Once it’s successful, it means your Docker installation was successful.

One thing to note is that you don’t have to add your account to docker group so you don’t have to sudo for every docker command to execute and docker-compose is bundled with Docker.app on Mac, which is nicer than the installation process on Linux.

Jenkins Master

We will spin up the Jenkins master using docker-compose. Create a directory such as ~/jenkins.

Copy and paste the following contents in docker-compose.yaml file in ~/jenkins.

version: '3.8'
services:
    jenkins:
        image: jenkins/jenkins:2.263.1
        container_name: jenkins
        user: '1000'
        volumes:
            - ./jenkins_home:/var/jenkins_home
        ports:
            - '8080:8080'
            - '50000:50000'
        networks:
            - docker_network
        restart: unless-stopped
        environment:
            - JENKINS_JAVA_OPTIONS=-Djava.awt.headless=true
            - JENKINS_LOG=/var/jenkins_home/logs/jenkins.log
        #command: --enable-future-java
networks:
    docker_network:
        external:
            name: custom_network

Execute the following command to create a network for your Docker containers.

docker network create custom_network

After that, execute the following command from at ~/jenkins

docker-compose up

When you see the output like the following, you have Jenkins up and running

jenkins    | This may also be found at: /var/jenkins_home/secrets/initialAdminPassword
jenkins    |
jenkins    | *************************************************************
jenkins    | *************************************************************
jenkins    | *************************************************************
jenkins    |
jenkins    | 2021-01-08 07:21:22.224+0000 [id=44]	INFO	h.m.DownloadService$Downloadable#load: Obtained the updated data file for hudson.tasks.Maven.MavenInstaller
jenkins    | 2021-01-08 07:21:22.231+0000 [id=44]	INFO	hudson.util.Retrier#start: Performed the action check updates server successfully at the attempt #1
jenkins    | 2021-01-08 07:21:22.248+0000 [id=44]	INFO	hudson.model.AsyncPeriodicWork#lambda$doRun$0: Finished Download metadata. 9,056 ms
jenkins    | 2021-01-08 07:21:23.526+0000 [id=31]	INFO	jenkins.InitReactorRunner$1#onAttained: Completed initialization
jenkins    | 2021-01-08 07:21:23.554+0000 [id=20]	INFO	hudson.WebAppMain$3#run: Jenkins is fully up and running

At this point, you could hit the Jenkins master by navigating your browser to http://localhost:8080 but this is not what I want. I would like to have Dockerized NGINX in front of it and route the traffic to the Jenkins so that it is easy to implement SSL certs for it.

You can Ctrl+C to stop Jenkins and then…

docker-compose down

to completely remove the container.

NGINX as Reverse Proxy

As the diagram indicates, my plan is to have NGINX accept HTTP(S) traffic and route the traffic to the Jenkins instance via the Docker’s virtual network.

Please follow the steps below.

Create a directory like ~/nginx

mkdir ~/nginx

Copy and paste the following YAML to ~/nginx/docker-compose.yaml file and save the file.

version: '3.8'
services:
    reverse:
        image: nginx:latest
        volumes:
            - ./data/nginx.conf:/etc/nginx/nginx.conf
            - ./data/conf.d:/etc/nginx/conf.d
        ports:
            - "80:80"
            - "443:443"
        networks:
            proxynet:
        restart: always
networks:
    proxynet:
        name: custom_network

Next, create ~/nginx/data directory. While you are there, let’s create conf.d directory.

mkdir data
cd data
mkdir conf.d

Copy and paste the following contents to ~/nginx/data/nginx.conf file and save it.

user  nginx;
worker_processes  2;

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;

events {
   worker_connections  1024;
   use epoll;
   accept_mutex off;
}

http {
   include       /etc/nginx/mime.types;
   proxy_set_header X-Real-IP $remote_addr;
   proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

   default_type  application/octet-stream;

   log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                     '$status $body_bytes_sent "$http_referer" '
                     '"$http_user_agent" "$http_x_forwarded_for"';

   access_log  /var/log/nginx/access.log  main;

   sendfile        on;
   #tcp_nopush     on;

   keepalive_timeout  65;

   client_max_body_size 300m;
   client_body_buffer_size 128k;

   gzip  on;
   gzip_http_version 1.0;
   gzip_comp_level 6;
   gzip_min_length 0;
   gzip_buffers 16 8k;
   gzip_proxied any;
   gzip_types text/plain text/css text/xml text/javascript application/xml application/xml+rss application/javascript application/json;
   gzip_disable "MSIE [1-6]\.";
   gzip_vary on;

   server {
       listen       80 default_server;
       listen       [::]:80 default_server;
       server_name  _;
       sendfile        on;
       #tcp_nopush     on;

       keepalive_timeout  65;

      client_max_body_size 300m;
      client_body_buffer_size 128k;

      gzip  on;
      gzip_http_version 1.0;
      gzip_comp_level 6;
      gzip_min_length 0;
      gzip_buffers 16 8k;
      gzip_proxied any;
      gzip_types text/plain text/css text/xml text/javascript application/xml application/xml+rss application/javascript application/json;
      gzip_disable "MSIE [1-6]\.";
      gzip_vary on;
   }
    include /etc/nginx/conf.d/*.conf;
}

Next, paste the following configuration to ~/nginx/conf.d/ssl.conf file.

upstream jenkins_upstream {
    server jenkins:8080;
}

server {
    server_name jenkins.local;
    listen 443 ssl;
    ssl_certificate /etc/nginx/conf.d/ssl/jenkins.local.crt;
    ssl_certificate_key /etc/nginx/conf.d/ssl/jenkins.local.key;

    location / {
        proxy_set_header        Host $host:$server_port;
        proxy_set_header        X-Real-IP $remote_addr;
        proxy_set_header        X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header        X-Forwarded-Proto $scheme;
        resolver 127.0.0.11;
        #proxy_redirect http:// https://;
        proxy_pass http://jenkins_upstream;
        # Required for new HTTP-based CLI
        proxy_http_version 1.1;
        proxy_request_buffering off;
        proxy_buffering off; # Required for HTTP-based CLI to work over SSL
        # workaround for https://issues.jenkins-ci.org/browse/JENKINS-45651
        add_header 'X-SSH-Endpoint' 'jenkins.local:50022' always;
    }
}

SSL Support

Since I’m just planning to run this locally, there isn’t a need for SSL but I would like to implement this for a good practice. And you never know, you may want to create a Jenkins instance in a real environment eventually.

I am planning to make the URL https://jenkins.local

Place the following files for SSL support. If you need to know how to create self-signed cert, please take a look my previous article.

To summarize, here are the files you have to create for NGINX to work in the nginx root directory.

├── data
│   ├── conf.d
│   │   ├── ssl
│   │   │   ├── jenkins.local.crt
│   │   │   └── jenkins.local.key
│   │   └── ssl.conf
│   └── nginx.conf
└── docker-compose.yml

Next, let’s edit /etc/hosts file so that https://jenkins.local will resolve to your local NGINX.

sudo vim /etc/hosts

Add the following line in the file.

127.0.0.1       jenkins.local

Now navigate to ~/jenkins and execute the following command.

docker-compose up -d

This starts the instance of Jenkins. Now navigate to ~/nginx and execute the following command.

docker-compose up -d

This starts the instance of NGINX as a reverse proxy that routes the traffic to the containerized Jenkins.

Access Jenkins UI and Initial Configuration

If you open your browser, and navigate it to https://jenkins.local/ you should see the following screen.

Beware that if you try to get the Administrator password from /var/jenkins_home/secrets/initialAdminPassword, your host side does not have the file at that location. Instead, you should cat the file at the following location.

~/jenkins/jenkins_home/secrets/initialAdminPassword

Once you get the admin password from the location, enter it to pass the initial screen to start the containerized Jenkins master.

Connect Jenkins Slave

If you take a look at the UI, you see that the Jenkins master has 2 executors by default. If you want your Mac to run builds, you can do so. Here are the steps.

Navigate to Manage Jenkins -> Manage Nodes and Clouds and then click New Node.

Enter mac for Node name and click Permanent Agent and then click OK.

And click Save on the next screen.

When you go back to Manage Nodes and Clouds, you will see the following screen.

Click mac (or whatever you created) and you will see the following screen.

You can definitely follow the instruction on the screen to connect your mac as a Jenkins slave. First download the agent.jar file from the same page and store it in ~/jenkins-slave directory.

Next execute the following command.

echo 15ef91ec4499d7be20f64fdca93995ab37cd23182982d8814ccb35b6df8254e7 > secret-file

This writes the secret to the secret-file in the directory. And then try to execute the following command to get your mac connected to Jenkins master as a slave.

java -jar agent.jar -jnlpUrl https://jenkins.local/computer/mac/slave-agent.jnlp -secret @secret-file -workDir "~/jenkins-slave"

You would expect that this should be the last step but you would get an error like the following.

ava -jar agent.jar -jnlpUrl https://jenkins.local/computer/mac/slave-agent.jnlp -secret @secret-file -workDir "~/dev/jenkins-slave"
Jan 13, 2021 10:31:43 PM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir
INFO: Using ~/dev/jenkins-slave/remoting as a remoting work directory
Jan 13, 2021 10:31:43 PM org.jenkinsci.remoting.engine.WorkDirManager setupLogging
INFO: Both error and output logs will be printed to ~/dev/jenkins-slave/remoting
Exception in thread "main" java.io.IOException: Failed to validate a server certificate. If you are using a self-signed certificate, you can use the -noCertificateCheck option to bypass this check.
	at hudson.remoting.Launcher.parseJnlpArguments(Launcher.java:571)
	at hudson.remoting.Launcher.run(Launcher.java:347)
	at hudson.remoting.Launcher.main(Launcher.java:298)

This means that Java does not trust the self signed SSL certificate you created. I blogged about this in the past, so please refer to this blog article to resolve this issue.

Once the SSL cert issue is resolved, it connects to Jenkins master and it’s ready like the image below.

INFO: Agent discovery successful
  Agent address: jenkins.local
  Agent port:    50000
  Identity:      5c:25:1c:e8:1d:35:f1:e8:d1:34:b6:52:ac:e9:e5:ca
Jan 13, 2021 11:13:33 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Handshaking
Jan 13, 2021 11:13:33 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connecting to jenkins.local:50000
Jan 13, 2021 11:13:33 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Trying protocol: JNLP4-connect
Jan 13, 2021 11:13:33 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Remote identity confirmed: 5c:25:1c:e8:1d:35:f1:e8:d1:34:b6:52:ac:e9:e5:ca
Jan 13, 2021 11:13:33 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Connected

Recap

It took me a few nights to write up this article. To recap…

  • Dockerizing Jenkins master
  • Using NGINX as a reverse proxy
  • Implementing SSL cert
  • Jenkins slave

This give you an environment where you can manage your own Jenkins in Docker and do whatever you want!

Author: admin

A software engineer in greater Seattle area

Leave a Reply

Your email address will not be published. Required fields are marked *