How to install Docker and Docker Compose on Oracle Linux 7

I have a need to install Docker and Docker Compose on Oracle Linux 7. Here is my note for future reference.

sudo yum -y update
sudo yum install -y docker-engine
sudo systemctl start docker
sudo systemctl enable docker
sudo usermod -aG docker $USER

Now, logout and log back in and execute a docker command to see if you don’t need sudo to execute it.

docker ps -a

Now install Docker Compose.

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

Now check if docker-compose was successfully installed.

docker-compose -v

New Server

I’ve migrated my blog to yet another host on OCI‘s free tier host. It was much easier this time because I had all my contents into one zip file and expanded it on the new host. By Dockerizing the whole site, it completely separates data and server and makes the migration so much easier.

As a blogger (though a mediocre one) who maintains the whole thing by himself, I cannot live without this anymore. It’s really convenient and much more maintainable.

Moved to OCI’s Free Tier

I decided to move to OCI’s free tier because the charge was going up faster than I expected. If you see this blog post, it’s on the free tier host.

I had already gone through the migration process so it wasn’t too hard to do it but still it was some work.

I was starting to get charged 67 cents a day. My ads on this blog doesn’t earn that much, so I was starting to be negative on the budget.

So far, since June 3 to June 17, only $3.13 but you can see the cost was starting to get higher in the last few days. The Shape of the host was VM.Stardard.E3.Flex with 2 AMD CPUs and 2GB of memory. $3.13 is nothing for the technical knowledge I gained migrating my WordPress site to my own Cloud but I wanted to make it economical to make it more sustainable. I thought 1 AMD CPU and 1GB of memory would do, so I moved everything to the free tier host. For more details about OCI’s free resources, click here.

As far as I know, it is possible for anyone who has OCI account to have a free tier host in us-ashburn-1 region in Availability Domain 3 with an AMD CPU. I saw a free tier host with ARM CPU just recently but I will wait and see if docker-compose releases bits for ARM processor. I don’t know if they will charge me for anything, but we will see. Once DNS propagates to this new host, I am going to terminate the old host.

Edit: I was just digging the OCI’s UI and found the combination is free. Just need to have docker-compose for ARM processor…

Configuring Ubuntu for Containerized Blog Engine

There are some prerequisites on the OS to support Dockerized WordPress.

  • Update OS (packges) with sudo apt-get update && sudo apt-get upgrade
  • Install Docker Engine by executing the following commands.
sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER
  • Install docker-compose by executing the following commands.
sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose
  • Install VIM by executing sudo apt-get install vim (This is necessary to edit config files while working on configurations.
  • Install net-tools by executing sudo apt-get install net-tools. This is not a must but it helps to check if your host is listening to the expected ports. sudo netstat -tulp is useful to check the listening ports.
  • Install ufw to manage firewall on Ubuntu. sudo apt-get install ufw
  • Open ports 80, 443 and 8080 by executing the following command. Adding 22 for ssh access.
    sudo ufw allow 80 && sudo ufw allow 443 && sudo ufw allow 8080 && sudo ufw allow 22 && sudo ufw enable

That’s pretty much what you need to get ready your own Dockerized self hosted WordPress on your own Cloud.


What Consists of This Blog

Like I said in my previous blog, I have moved my blog from iPage.com to Oracle Cloud Infrastructure. There were 2 motivation to move my blog to my own Cloud. One was because iPage.com was very slow. It’s a cheap service so I was getting what I was paying for. My second motivation was I wanted to Dockerize everything just because I was very curious about what I could do with the technical knowledge I learned over the years. So, what consists of this blog?

  • A Ubuntu VM
  • VM.Standard.E3.Flex Shape
  • 1 CPU with 2GB Memory
  • Docker CE
  • MySQL Container
  • phpMyAdmin Container
  • WordPress Container
  • NGINX Container as a Reverse Proxy
  • All Containers are in docker-compose YAML file
  • Internet Gateway
  • Security List to accept traffic from the Internet to the VM.
  • Change to A Record in iPage.com’s DNS to the public IP address.

I have used Terraform to provision the VM so that I can consistently reprovision the same VM over and over.

I will write about the details more in the coming weeks as I organize what I have done to accomplish the migration.

Moved to OCI

I have completely migrated my blog from iPage.com to Oracle Cloud Infrastructure. There were some struggles over the last few weeks but I have learned how to solve the problems.

I will start to blog about the struggles I had to migrate my blog. One thing that I have accomplished is Dockerize WordPress + NGINX. This is something I have always wanted to do. My struggles paid off and my blog runs so much faster. It runs on a VM with 1 CPU with 2 GB memory on AMD. I will see how much it costs and will talk about it here.

My site runs on Ubuntu. I was able to clean up some mess that accumulated over the years in the database too.

Will blog again.

How to Install Docker on Ubuntu

This is just for my own note so that I don’t have to Google around for every time I do this for my own.

Execute the following commands.

sudo apt-get update
sudo apt-get install apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io
sudo usermod -aG docker $USER

This way I just have to copy and paste all the commands listed here to have a running Docker on a Ubuntu host.

To test the Docker installation, execute and make sure the following command executes successfully.

$ docker run hello-world

I am going to take another note below for installing docker compose as well.

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose
sudo chmod +x /usr/local/bin/docker-compose

To test docker compose installation, make sure the following command executes successfully.

docker-compose -v

New Ubuntu Server

I’ve downloaded Ubuntu Server from here and installed as a virtual machine on my Virtual Box. Both of them are available for free, which I really appreciate.

The reason why I wanted to install Ubuntu Server is because I want to try Docker on it. CentOS used to be my choice of Linux OS but with the recent event of CentOS Stream, my choice of Linux server OS is Ubuntu.

I’ve installed the Ubuntu server on my virtual box. Here is the information.

I chose an option to install Docker during the installation process, so Docker is already installed.

Now, I want to be able to execute docker command without having to do sudo, so I am adding the user the the docker group.

Adding docker group.

sudo groupadd docker

And then add the current user to the group.

sudo usermod -aG docker $USER

Logout and log back in for it to be effective. Or if it still doesn’t work, you might have to restart the server.

Try to execute the following command to see if it really works.

amaterasu48@ubuntu-svr:~$ docker run hello-world
Unable to find image 'hello-world:latest' locally
latest: Pulling from library/hello-world
b8dfde127a29: Pull complete
Digest: sha256:308866a43596e83578c7dfa15e27a73011bdd402185a84c5cd7f32a88b501a24
Status: Downloaded newer image for hello-world:latest

Hello from Docker!
This message shows that your installation appears to be working correctly.

To generate this message, Docker took the following steps:
 1. The Docker client contacted the Docker daemon.
 2. The Docker daemon pulled the "hello-world" image from the Docker Hub.
    (amd64)
 3. The Docker daemon created a new container from that image which runs the
    executable that produces the output you are currently reading.
 4. The Docker daemon streamed that output to the Docker client, which sent it
    to your terminal.

To try something more ambitious, you can run an Ubuntu container with:
 $ docker run -it ubuntu bash

Share images, automate workflows, and more with a free Docker ID:
 https://hub.docker.com/

For more examples and ideas, visit:
 https://docs.docker.com/get-started/

The next thing I would like to do is to have WordPress running within containers. I’ve always wanted to try it but I didn’t spend my time to do it. I know WordPress needs database behind it, so this is going to be an interesting challenge.

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!

How to Provision Jenkins on NGINX Reverse Proxy on Docker Container

I’ve wanted to provision Jenkins on NGINX reverse proxy for a long time. I would hit walls here and there, but I finally got things sorted out at least to the point where I can hit Jenkins master on NGINX reverse proxy on my browser both running on Docker container.

There is so much to cover, so I think this blog post is going to be quite long.

First of all, I need to make clear what I want to accomplish.

I want to have multiple instances of Jenkins hosted on a single Docker host. Depending on the host name the HTTP request came, NGINX reverse proxy routes the traffic to the appropriate Jenkins instance.

The diagram below represents the logical architecture.

Here is the list of things that will be covered in this blog entry.

  • Docker Network
  • Jenkins on Docker
  • NGINX on Docker as Reverse Proxy
  • Putting NGINX and Jenkins Together

Docker Network

As you can see, there are 3 containers in the diagram above. NGINX reverse proxy routes traffic to Jenkins instances. It means that NGINX needs to be able to communicate with Jenkins instance. The containers in the diagram exist within Docker network.

By default, Docker containers use bridge network meaning that Docker uses its own network though it is configurable. If you execute ip addr on your terminal, you’d see network interfaces like this.

4: docker0: <NO-CARRIER,BROADCAST,MULTICAST,UP> mtu 1500 qdisc noqueue state DOWN group default
    link/ether 02:42:67:fe:f4:d5 brd ff:ff:ff:ff:ff:ff
    inet 172.17.0.1/16 brd 172.17.255.255 scope global docker0
       valid_lft forever preferred_lft forever
103: br-1418967f1f10: <BROADCAST,MULTICAST,UP,LOWER_UP> mtu 1500 qdisc noqueue state UP group default
    link/ether 02:42:63:60:8f:d1 brd ff:ff:ff:ff:ff:ff
    inet 172.25.0.1/16 brd 172.25.255.255 scope global br-1418967f1f10
       valid_lft forever preferred_lft forever
    inet6 fe80::42:63ff:fe60:8fd1/64 scope link
       valid_lft forever preferred_lft forever

When you spin up a Docker container, each container has its own IP address within the Docker network. I provisioned a Jenkins container and I can check its IP address with the steps below.

  1. Check the container ID.
    docker ps -a
  2. Run the following command to get the IP address.
    docker inspect [container ID] | grep IPAddress
  3. I got 172.25.0.2 as the IP address of the container. This means that it’s within the range of the CIDR 172.25.0.1/16 listed in the br-1418967f1f10 bridge network above. Based on the CIDR, the network can host up to 65,536 containers which is way more than enough for normal operations.

Why is this important in this article. It’s because I am planning to provision NGINX as a Docker container and I want the NGINX container to communicate with Jenkins container within the Docker network without using the host network. It will increase the network security level to the next level. It is important to understand the network aspect of containerization.

Jenkins on Docker

First of all, I need to provision a Jenkins container. Here is the initial Docker Compose file (YAML) I created.

version: '3.8'
services:
    jenkins:
        image: jenkins/jenkins:lts
        container_name: jenkins
        user: '1000'
        volumes:
            - ./jenkins_home:/var/jenkins_home
        ports:
            - '8080:8080'
            - '2000:2000'
        restart: unless-stopped
        environment:
            - JENKINS_SLAVE_AGENT_PORT=2000
            - JENKINS_JAVA_OPTIONS=-Djava.awt.headless=true
            - JENKINS_LOG=/var/jenkins_home/logs/jenkins.log

Please note that you should run mkdir jenkins_home (create the directory) beforehand to make sure you won’t get an error because as you can see it in docker-compose.yaml, /var/jenkins_home in the container is mapped to the host’s ./jenkins_home directory where all the data is persisted.

If you save this file as docker-compose.yaml and run docker-compose up -d and then access it on your browser http://foobar:8080/, you should see the initial set up UI. If you want to stop the container, you can just run docker-compose down.

It would be operational enough to use the Jenkins container as the way it is. However, it’s limited in a sense that if you want to have another instance of Jenkins on the same machine, you would have to use another port like http://foobar:8081 and http://foobar:8082 if you need another instance. It would mean you would have to open more ports as you have more instances of Jenkins on the same Docker host. Also, it’s kind of ugly. By having NGINX reverse proxy in place in front of the Jenkins instances on the Docker host, it solves the problem.

Moreover, NGINX can be the front man to service SSL communication. It’s the best practice nowadays that even internal web applications are protected by encrypted communication by SSL. You may want to have URLs that looks like the following.

https://jenkins1.foobar and https://jenkins2.foobar

This way the URL is easier to remember and also protected by SSL communication.

NGINX on Docker as Reverse Proxy

Here is the NGINX’s docker-compose.yaml file that I’ve got.

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

For the NGINX container, I am mapping the /etc/nginx/nginx.conf to the local ./data/nginx.conf file and /etc/nginx/conf.d directory to ./data/conf.d directory. Make sure that you have ./data and ./data/conf.d directories before you run docker-compose up -d.

I am also creating networks -> proxynet -> custom_network here in docker-compose.yaml because I want to have the Docker network where NGINX and Jenkins can communicate with each other.

Putting NGINX and Jenkins Containers Together

Here is the docker-compose.yaml file that can spin up NGINX and Jenkins within the same subnet within Docker. Notice that I commented out - 8080:8080 line. It means I stopped exposing the port 8080 of the Jenkins container to the host side because it is not necessary anymore. NGINX and Jenkins will communicate with each other within the Docker network subnet.

ersion: '3.8'
services:
    jenkins:
        image: jenkins/jenkins:lts
        container_name: jenkins
        user: '1000'
        volumes:
            - ./jenkins_home:/var/jenkins_home
        ports:
            #- '8080:8080'
            - '2000:2000'
        networks:
            proxynet:
        restart: unless-stopped
        environment:
            - JENKINS_SLAVE_AGENT_PORT=2000
            - JENKINS_JAVA_OPTIONS=-Djava.awt.headless=true
            - JENKINS_LOG=/var/jenkins_home/logs/jenkins.log
        #command: --enable-future-java
    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

Also make sure you have the nginx.conf file under ./data directory.

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;
}

Also you need to have the following ssl.conf file under ./data/conf.d directory file. Notice that this file does 2 important things. One is to enable SSL communication between NGINX and the client side. If you wonder how to create SSL certificate, I have written how to just do it in my previous blog post. The second thing is to route the HTTPS traffic to the server jenkins:8080. This communication with done within the Docker network subnet, so no traffic goes out to the host’s network. You can also see that the SSL certificate files are under conf.d/ssl directory.

upstream jenkins_upstream {
    server jenkins:8080;
}

server {
    server_name jenkins.linux-mint.local;
    listen 443 ssl;
    ssl_certificate /etc/nginx/conf.d/ssl/jenkins.linux-mint.local.crt;
    ssl_certificate_key /etc/nginx/conf.d/ssl/jenkins.linux-mint.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.linux-mint.local:50022' always;
    }
}

Once it’s all done, just execute the command docker-compose up -d in the directory where you have docker-compose.yaml. If it’s successful and you access the URL like https://jenkins.linux-mint.local, you should see the initial screen where you can start to manage the instance of Jenkins.

If you execute cat jenkins_home/secrets/initialAdminPassword, you should see the initial password to enter on the screen. And you just have to click Continue button, you are on to setting up Jenkins.

Recap

I have covered the steps to spin up Jenkins behind NGINX as a reverse proxy. Based on my experience, this is the most flexible and safest way to provision Jenkins. There are steps to provision agents (slaves) for it. I will cover the topic some other time.