Well.. setting up Continous Integration/ Continous Deployment (CI/CD) is a very huge chapter.. Generally you have to automize everything! And it’s hard to break it down to one example, because it really depends on your project setup. You have to

  • write your build scrips (maven, gradle, …),
  • define the runtine environment (Dockerfile),
  • add webhooks for automatic triggering the pipeline,
  • set up a deployment pipeline (Jenkinsfile),
  • archivate your libraries in a repository manager (Nexus),
  • archivate your docker-images in a docker registry,
  • automaticly detect new versions of the running images and deploy them (watchtower),
  • run and monitor the application (Portainer).

That’s why we’ll concentrate on the operations view for now. We’re gonna set up Jenkins, Nexus, Docker Registry and Portainer as well as watchtower via docker.

NOTE: The docker-compose files you do find below presuppose the server setup described in the previous post!

tl;dr

I’ve uploaded all files here. Refere the Readme.md!

Preperations

Let’s create our .env file first:

echo "COMPOSE_PROJECT_NAME=cicd" > .env
echo "DOMAIN=<YOUR_DOMAIN>" >> .env

Setting up Jenkins

Jenkins pipeline example

Jenkins is an automation server, which allows you to define tasks in steps. Those will be executed by triggering the pipeline.

Fixing permissions

By convention, Jenkins folders and files mounted in the container must have permissions set to 1000:1000. On the other hand, Docker will require all folders it writes to be owned by root while being completely opened nonetheless because of the different users used inside the build containers. [1]

Executing the following script via sh install.sh provides all needed jenkins folders and permissions:

mkdir -p /opt/data/jenkins/dev-ops
mkdir -p /opt/data/jenkins/volume-caches/build-cache/composer
mkdir -p /opt/data/jenkins/volume-caches/build-cache/node/npm
mkdir -p /opt/data/jenkins/volume-caches/build-cache/node/gyp
mkdir -p /opt/data/jenkins/volume-caches/build-cache/node/cache
mkdir -p /opt/data/jenkins/volume-caches/build-cache/node/config
mkdir -p /opt/data/jenkins/volume-caches/build-cache/bower/local
mkdir -p /opt/data/jenkins/persistent-cache
mkdir -p /opt/data/jenkins_home

chown -R 1000:1000 /opt/data/jenkins
chown -R root:root /opt/data/jenkins/volume-caches
chmod -R 777 /opt/data/jenkins_home
chmod -R 777 /opt/data/jenkins/volume-caches
chown root:root -R /opt/data/portainer
chown root:root -R /opt/data/traefik
chmod 777 /var/run/docker.sock

Installation

Our container will need additional apt-get packages which are not on the original container image [1]. That’s why we have to create an individual image using the following Dockerfile:

FROM jenkins/jenkins:lts

USER root
RUN apt-get update \
      && apt-get upgrade -y \
      && apt-get install -y sudo libltdl-dev ssh rsync \
      && rm -rf /var/lib/apt/lists/*
      
RUN echo "jenkins ALL=NOPASSWD: ALL" >> /etc/sudoers

# Fixes socket permissions within build containers
RUN groupadd -for -g 998 docker # echo $(stat -c '%g' /var/run/docker.sock)
RUN usermod -aG docker jenkins

USER jenkins

Finally we can build our custom Jenkins image via:

docker build -t custom-jenkins:latest .

Up and running

The easiest way to run this image is using this docker-compose file:

version: '3.7'

services:

  jenkins:
    image: custom-jenkins:latest
    container_name: jenkins
    labels:
      - traefik.http.routers.jenkinsRouter.rule=Host("jenkins.${DOMAIN}")
      - traefik.http.routers.jenkinsRouter.entrypoints=https
      - traefik.http.routers.jenkinsRouter.tls=true
      - traefik.http.routers.jenkinsRouter.tls.certresolver=mytlschallenge
    volumes:
      - /opt/data/jenkins/dev-ops:/dev-ops
      - /opt/data/jenkins/persistent-cache:/persistent-cache
      - /opt/data/jenkins/volume-caches/build-cache/composer:/.composer
      - /opt/data/jenkins/volume-caches/build-cache/composer:/home/docker/.composer
      - /opt/data/jenkins/volume-caches/build-cache/node/npm:/.npm
      - /opt/data/jenkins/volume-caches/build-cache/node/npm:/home/docker/.npm  
      - /opt/data/jenkins/volume-caches/build-cache/node/gyp:/.node-gyp
      - /opt/data/jenkins/volume-caches/build-cache/node/cache:/.cache
      - /opt/data/jenkins/volume-caches/build-cache/node/config:/.config
      - /opt/data/jenkins/volume-caches/build-cache/bower/local:/.local
      - /opt/data/jenkins/volume-caches/build-cache/bower/local:/home/docker/.local
      - /opt/data/jenkins:/var/jenkins_home
      - /var/run/docker.sock:/var/run/docker.sock
      - /usr/local/bin/docker-compose:/usr/bin/docker-compose
      - /usr/bin/docker:/usr/bin/docker
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    networks:
      - proxy

networks:
  proxy:
    external: true

To finish the installation follow these steps:

  1. Run the service:
    docker-compose up -d
  2. Look up the admin password and open jenkins.<YOUR_DOMAIN> to access the application:
    docker exec -it jenkins /bin/bash
    cat /var/jenkins_home/secrets/initialAdminPassword
    exit
  3. IMPORTANT: Unselect Folders in ‘Custom Packages’ during installation

Setting up Nexus

In case you’d like to have a core functionality in more than one of your projects, it’s state of the art to swap that code in a library and include it in all those projects.

Now you can use Nexus to store that library in a self-hosted repository to make it available for your build tools like maven or gradle.

Installation

At first you have to crate a volume for nexus to store all libraries permanently on your disc:

mkdir -p /opt/data/nexus
chown -R 200:200 /opt/data/nexus/

Now you’re can append the docker-compose file with the nexus image:

version: '3.7'

services:

  jenkins:
  ...

  nexus:
    image: sonatype/nexus3
    container_name: nexus
    expose:
      - 8081
    volumes:
      - /opt/data/nexus/nexus-data:/nexus-data:rw
      - /opt/data/nexus/sonatype-work/:/sonatype-work:rw
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    labels:
      - traefik.http.routers.nexusRouter.rule=Host("nexus.${DOMAIN}")
      - traefik.http.services.nexus.loadbalancer.server.port=8081
      - traefik.http.routers.nexusRouter.entrypoints=https
      - traefik.http.routers.nexusRouter.tls=true
      - traefik.http.routers.nexusRouter.tls.certresolver=mytlschallenge
    networks:
      - proxy

networks:
  proxy:
    external: true

Up and running

Run the service with docker-compose up -d, visit nexus.<YOUR_DOMAIN> and paste the admin password you’ve looked up via:

docker exec -it nexus /bin/bash
cat /nexus-data/admin.password
exit

Setting up Portainer

Portainer is a container management service which provides its users an UI for nearby all docker functionalities. Once it’s installed, developers can watch their containers without connecting to the server via shh.

Installation

The only thing you have to do is creating a volume on your disc (mkdir /opt/data/portainer) and appending your docker-compose file with the portainer image:

version: '3.7'

services:

  jenkins:
    ...

  nexus:
    ...

  portainer:
    image: portainer/portainer
    container_name: portainer
    command: -H unix:///var/run/docker.sock
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
      - /opt/data/portainer:/data
      - /etc/timezone:/etc/timezone:ro
      - /etc/localtime:/etc/localtime:ro
    labels:
      - traefik.http.routers.portainerRouter.rule=Host("portainer.${DOMAIN}")
      - traefik.http.routers.portainerRouter.tls=true
      - traefik.http.routers.portainerRouter.tls.certresolver=mytlschallenge
      - traefik.http.routers.portainerRouter.entrypoints=https
    restart: always
    networks:
      - proxy

networks:
  proxy:
    external: true

Up and running

Run the service with:

docker-compose up -d 

Setting up Watchtower

If you like your images to be updated automaticly, watchtower is your tool of choice! You can install it via appending your docker-compose file with:

version: '3.7'

services:

  jenkins:
    ...

  nexus:
    ...

  portainer:
    ...

  watchtower:
    image: containrrr/watchtower
    container_name: watchtower
    volumes:
      - /var/run/docker.sock:/var/run/docker.sock
    command: --interval 900 --cleanup
    labels:
      - com.centurylinklabs.watchtower.enable=true
    restart: unless-stopped
    networks:
      - proxy

networks:
  proxy:
    external: true

Finally restart the stack:

docker-compose up -d

NOTE: USE WITH CAUTION!
There may be some breaking changes from one version to another! Watchtower crashed my server a few month ago as treafik released v2.

Leave a Reply

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