Jenkins with HTTPS in a Docker Container

This post will be updated to reflect my new proxy setup at some point in the future. For now, please see this post for information about the new selection. The current proxy solution still works, it's just a bit stale.

Assumptions

  • You know what HTTPS and SSL certificates are/do and how the CA ecosystem works
  • You have a domain that you plan to use for the Jenkins instance and this domain either doesn't have a CAA record or has one that allows LetsEncrypt
  • You understand the general working of containers and have docker installed on your system. Docker/Docker Compose knowledge is a plus but not required, you can basically just copy/paste code to get this working. I'll also go over some common pitfalls at the end.
  • You know what Jenkins is and why you should be using it. If you don't, use the Google machine.

OK, let's get this show on the road.

You will need

  • A linux environment - because there's a special circle of hell reserved for you if you're doing this on Windows (just kidding...)
  • Docker
  • Docker-compose
  • A domain and the ability to add A records
  • Tea/Coffee because caffeine

1 - Checking Your Install

At the time of writing, I've got the docker and docker-compose versions below. If these become wildly out of date I'll try to update in the future. Or shoot me an angry email on the About Me page and I'll get right to it! If you can't be bothered to read the walkthrough and just want the files, they're pasted at the bottom of the post. I gotchu.

putty_o6DddMQAfc

As long as your docker --version and docker-compose --version outputs are these versions or later, this should work fine. If they're not, check if you've got installed via snap or pip or something as they sometimes don't update versions frequently. Official docker install process is here if you need it

You can also run docker run hello-world to make sure containers are running properly.

putty_jxJIkm3Y8a

2 - Adding Your A Record

Before we get all the nifty LetsEncrypt bits done, you need to have a domain that resolved to the IP of your server/raspberry pi/nan's old laptop. Hopefully you've got a VPS with a public IPv4 adress you can use, if not get comfortable with port forwarding. You'll need to forward TCP 80 and 443 inbound because LetsEncrypt still uses 80 for one of the validation challenges.

Create an A record however the Good Lord GoDaddy lets you (or use a respectable registrar/DNS provider ;)). This can be on the root domain or any subdomain, we're not getting fancy and doing wildcard certificates here, I'll probably cover that in another post.

chrome_87S0Xpuq4d

3 - Compose Like Vivaldi

OK so here's where we get into the docker-compose magic. There's another post explaining the proxy in detail so if you'd like to read up on that, check it out here. Therefore, we'll jump straight to the Jenkins YAML file. You can grab the correct proxy config from the bottom of this page and just docker-compose up -d that bad boy. Side note: if you're on a pi use the build found here instead. You're welcome

version: '3'

services:
  jenkins:
    container_name: jenkins
    image: jenkins/jenkins:lts
    restart: unless-stopped

We start with the basics, version and services because we're keeping to the standard like good, rule followers™. Next, we name the service and container something sensible and pull the jenkins/jenkins:lts image. You can use the :latest tag instead of :lts if you want the bleeding edge releases but I'd like my build server to be nice stable personally... restart: unless-stopped has the advantage of restarting the container automatically if you reboot the OS of the machine that it's running on and doesn't get too aggressive if there's a critical error that you need to stop the container for.

environment:
    - VIRTUAL_HOST=sub.domain.com
    - VIRTUAL_PORT=8080
    - LETSENCRYPT_HOST=sub.domain.com

Environment variables are next and these are important for the proxy and LetsEncrypt portions. Because Jenkins for some reason won't let you change the port that it's running on, you always have to proxy across to 8080. The VIRTUAL_PORT=8080 means that the proxy will take 443 traffic on whatever the VIRTUAL_HOST URL is and forward it to 8080 on the container so Jenkins is none the wiser. Neat! Match the LETSENCRYPT_HOST to the VIRTUAL_HOST because that's what LetsEncrypt will resolve to make sure the IP matches your server IP. Also, I can't think of a reason why you wouldn't want to have them matching... 10 points for whoever can send me a valid use case for this.

    volumes:
      - jenkins_home:/var/jenkins_home
    network_mode: "bridge"

volumes:
  jenkins_home:

Next there's the volume goodness. Fairly standard, nothing special here. If you want to get creative and map a specific local place in the filesystem you can change the named mapping to a bind mount, as detailed in the docker docs. We have to declare this named volume at the bottom by itself because we're being rule followers™, remember?

The most important bit about this is the network_mode: "bridge" bit. This is because, if you're running it like I am, you've got your proxy docker-compose magic in a different file. If you leave them as default networking, they will only be able to see containers in the same docker-compose.yml file. This means you'll have Error 500 all up in your NGINX proxy and have a bad time in general. It definitely didn't take me a good half an hour to figure this out the first time... You could also make a custom network for your proxy and connect it up to that. I'll cover docker networking in another post and link back here when it's done. For now, unless you've got something else super complex running then bridge mode will be fine.

4 - Run Forrest, Run

Now, it's time to put the butler to work. Worth mentioning that you should have your proxy up and running by this point, docker-compose up -d if you dont. docker-compose up -d && docker-compose logs -f will do the trick. It'll grab the relevant images, start like magic on the first attempt and then show you the logs of a fledgling jetty server doing it's thing. You'll also, crucially, see the below little chunk of text.

putty_LPHrWb089c

Grab that password and browse to the domain that you've pointed at this server. If you get some weird browser error, give it 30 seconds, dump the cache and refresh. The proxy container probably just took a second to get all the certs in order. If all went well, you should be greeted with the below screen.

chrome_MGZMUnVkVA

You guessed it, stick that admin password in and hit Continue!

chrome_ROFWJqDtow

This one depends on if you know what you're doing, I normally just hit the 'Install suggested plugins' options. You can always pick other specific ones later on.

chrome_p71HRp8S36

Then some nifty plugin installing will commence. This will take a minute so grab a cup of tea. Containerising without tea is uncivilised, you should know that already!

chrome_FGkU6gwGW2

Now you'll create an admin user. Bob the builder is really the only user that makes sense in the context of creating a build server but if you have some other cool name like Jeffrey or Ferercio then go ahead and enter that instead. I can't stop you from making the wrong decisions, I can only try and advise you against them... Also we know Bob The Builder is the type of dude to still have an AOL email account, don't @ me

chrome_A6RCHkIec6

This next page should have picked up your install URL because you've browsed to it but if not, pop the FQDN in here to avoid any wrongly genearted links in the future.

chrome_OgXoQRdcf1

Congratuwelldone! The butler is at your service.

chrome_SJ5a3K4EP7

Can you smell that? That new server smell? Smells like hope with notes of long nights figuring out why it builds locally but Jenkins hates you. That's all yet to come, friends. I'll be writing more Jenkins posts as I set up build pipelines in various differnet environments so keep your eyes peeled for those scintillating pieces of writing.

Proxy docker-compose.yml

version: '3'

services:
  proxy:
    container_name: proxy
    image: jwilder/nginx-proxy
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    labels:
      com.github.jrcs.letsencrypt_nginx_proxy_companion.nginx_proxy: "true"
    environment:
      - DEFAULT_HOST=proxy.local
    volumes:
      - certs:/etc/nginx/certs:ro
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - /var/run/docker.sock:/tmp/docker.sock:ro
    network_mode: "bridge"

  letsencrypt-companion:
    container_name: letsencrypt
    image: jrcs/letsencrypt-nginx-proxy-companion
    restart: unless-stopped
    environment:
      - DEFAULT_EMAIL=you@adomain.com
    volumes:
      - conf:/etc/nginx/conf.d
      - vhost:/etc/nginx/vhost.d
      - html:/usr/share/nginx/html
      - dhparam:/etc/nginx/dhparam
      - certs:/etc/nginx/certs:rw
      - /var/run/docker.sock:/var/run/docker.sock:ro
    network_mode: "bridge"
    depends_on:
      - proxy

volumes:
  certs:
  conf:
  vhost:
  html:
  dhparam:

Jenkins docker-compose.yml

version: '3'

services:
  jenkins:
    container_name: jenkins
    image: jenkins/jenkins:lts
    restart: unless-stopped
    environment:
    - VIRTUAL_HOST=jenkins.init.tools
    - VIRTUAL_PORT=8080
    - LETSENCRYPT_HOST=jenkins.init.tools
    volumes:
      - jenkins_home:/var/jenkins_home
    network_mode: "bridge"

volumes:
  jenkins_home:
Song of the post: new Tool goodness!