Creating a Dockerized Magnolia CMS Environment using Docker Compose and Vagrant


Introduction

This article describes how to create a complete Magnolia environment that contains an Author environment and two Public environments which are served via a HAProxy load balancer. The purpose of this setup is to be able to do a full system test from a local development machine.

Developers tend to use a single local virtual machine that contains all of the components of a system. This is often architecturally very different to how the system is eventually deployed to production (live) servers, where the system is most likely spread across multiple servers, such as a database server, backend server, services layer, load balancer, etc. Spinning up multiple local virtual machines to create a more accurate representation of the production environment can be a complicated and resource intensive process. Most developers will shy away from this and instead use some form of pre-production environment for their integration testing, rather than their local environment. But it’s always beneficial, and sometimes even necessary, to test early and test often using a more accurate local representation of the production environment.

Containerisation engines, such as Docker, can help here as they allow developers to define containers for each logical component of a system. Containers make efficient use of the capacity of a host machine by separating the system architecture from the underlying operating system. This makes it feasible to model all the components of a system using multiple containers running within a single virtual machine instance.

Recreating a Typical Magnolia Setup

The following diagram (borrowed from Magnolia’s documentation) describes a typical Magnolia implementation.

A typical Magnolia setup
A typical Magnolia setup

This is the target setup to model using Docker containers, with the slight variation that the database nodes (labelled “JCR” in the diagram for the Java Content Repository) are configured as standalone MySQL containers.

The following sever containers can be created:

  • Author web server running Tomcat
  • Author database running MySQL
  • Two public web servers running Tomcat
  • Two public database servers running MySQL
  • HAProxy load balancer

Docker Compose files

Docker compose allows developers to define a system with multiple containers using a YAML file. The Docker compose file for the Magnolia setup described above is as follows:

version: '3'

services:

    publicDB1:
        container_name: mysql-public1-container
        image: mysql:5.7
        environment:
            MYSQL_ROOT_PASSWORD: xxxxxxx
            MYSQL_DATABASE: magnoliaPublic
            MYSQL_USER: magnoliaPublic
            MYSQL_PASSWORD: xxxxxxx
        ports:
            - "3307:3307"
        volumes:
            - ./mysql-config/public1/:/etc/mysql/conf.d
            - ./db:/docker-entrypoint-initdb.d
            
    public1:
        container_name: tomcat-public1-container
        hostname: public1
        build: .
        ports:
            - "8081:8080"
        expose:
            - "8081"
        volumes:
            - ./war/ROOT.war:/opt/apache-tomcat-9.0.10/webapps/ROOT.war
        links:
            - publicDB1:mysql-public1-container
    
    publicDB2:
        image: mysql:5.7
        environment:
            MYSQL_ROOT_PASSWORD: xxxxxxx
            MYSQL_DATABASE: magnoliaPublic
            MYSQL_USER: magnoliaPublic
            MYSQL_PASSWORD: xxxxxxx
        ports:
            - "3308:3308"
        volumes:
            - ./mysql-config/public2/:/etc/mysql/conf.d
            - ./db:/docker-entrypoint-initdb.d
            
    public2:
        container_name: tomcat-public2-container
        hostname: public2
        build: .
        ports:
            - "8082:8080"
        expose:
            - "8082"
        volumes:
            - ./war/ROOT.war:/opt/apache-tomcat-9.0.10/webapps/ROOT.war
        links:
            - publicDB2:mysql-public2-container   

    authorDB:
        container_name: mysql-author-container
        image: mysql:5.7
        environment:
            MYSQL_ROOT_PASSWORD: xxxxxxx
            MYSQL_DATABASE: magnoliaAuthor
            MYSQL_USER: magnoliaAuthor
            MYSQL_PASSWORD: xxxxxxx
        ports:
            - "3306:3306"
        volumes:
            - ./authorDB:/docker-entrypoint-initdb.d
            
    author:
        container_name: tomcat-author-container
        build: .
        ports:
            - "8080:8080"
        volumes:
            - ./war/ROOT.war:/opt/apache-tomcat-9.0.10/webapps/ROOT.war
        links:
            - authorDB:mysql-author-container
        links:
            - public1
            - public2
    
    haproxy:
        container_name: haproxy
        image: haproxy:latest
        volumes:
            - ./haproxy:/haproxy-override
            - ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro
        links:
            - public1
            - public2
        ports:
            - "8000:8000"
            - "70:70"
        expose:
            - "8000"
            - "70"

The Tomcat instances are built from a Dockerfile. This file also takes care of deploying the application file (WAR file) to the Tomcat root. The application used for this demonstration was the Magnolia Travel Demo.

The two public ports are being forwarded from 8081 and 8082 to 8080. And the three MySQL instances (2 public databases and 1 author) are running on ports 3306, 3307, and 3308. This is because we are running the application on one virtual machine and therefore must use different ports to expose the services outside of the containers.

In order to run MySQL on different ports requires the port to be changed in the MySQL configuration file. This line in the Docker Compose file copies the my.cnf file to the right place:
/mysql-config/public1/:/etc/mysql/conf.d

The my.cnf file simply contains:
[mysqld]
port=3307

The “links” definition connects containers to each other. For example: publicDB1:mysql-public1-container links the Public 1 web servers to the PublicDB1 MySQL instance.

The web servers have their hostnames explicitly defined, e.g. hostname: public1. This is so that the correct Magnolia initialisation file (magnolia.properties) will be read. By default, Magnolia reads the magnolia.properties file from: WEB-INF/config/${servername}/magnolia.properties, where ${servername} is the hostname.

In the Jackrabbit configuration, the database URL is set to the Docker instance name, e.g.:
<param name=”url” value=”jdbc:mysql://publicDB1:3307/magnoliaPublic” />

HAProxy configuration

The following line maps the HAProxy configuration file:
– ./haproxy/haproxy.cfg:/usr/local/etc/haproxy/haproxy.cfg:ro

The haproxy.cfg is as follows:

global
  log 127.0.0.1 local0
  log 127.0.0.1 local1 notice
  maxconn 4096

defaults
  log global
  mode http
  option httplog
  option dontlognull
  timeout connect 10000ms
  timeout client 50000ms
  timeout server 50000ms

listen stats
  bind 0.0.0.0:70
  mode http
  stats enable
  stats hide-version
  stats scope .
  stats realm Haproxy\ Statistics
  stats uri /
  stats auth admin:admin

frontend balancer
  bind *:8000
  mode http	
  default_backend web_backends

backend web_backends
  mode http
  option forwardfor
  balance roundrobin
  server public1 public1:8080 check id 1
  server public2 public2:8080 check id 2

bind *:8000 maps the load balancer to port 8000.

The last few lines configure the two public servers into the load, to be served using a round robin process:
balance roundrobin
server public1 public1:8080 check id 1
server public2 public2:8080 check id 2

Vagrant machine

To wrap the multiple Docker containers into a single virtual machine that can be created, destroyed and recreated at will, Vagrant was used to orchestrate the creation the VM, the installation of Docker and Docker Compose, and to start the Docker containers.

The Vagrantfile that does all this is as follows:

Vagrant.configure("2") do |config|

  config.vm.box = "centos/7"
  config.vm.provider "virtualbox" do |vb|
	  vb.memory = 8048
  end
  config.ssh.insert_key = 'true'
  
  # Open Ports
  config.vm.network "forwarded_port", guest: 70, host: 70
  config.vm.network "forwarded_port", guest: 8000, host: 8000
  config.vm.network "forwarded_port", guest: 8080, host: 1111
  config.vm.network "forwarded_port", guest: 8081, host: 1112
  config.vm.network "forwarded_port", guest: 8082, host: 1113
    
  # Install Docker using Ansible script
  config.vm.provision "ansible_local" do |ansible|
    ansible.playbook = "docker-install.yml"
	ansible.limit = "all,localhost"
	ansible.become_user = "root"
  end
  
  # Start Docker
  config.vm.provision "shell", inline: "docker-compose -f /vagrant/docker/docker-compose.yml up"
  
end

Multiple ports are forwarded on the virtual machine. These map to the Magnolia, MySQL and HAProxy servers that are configured in the Docker Compose file.

To install Docker, Ansible is first installed, and this Ansible playbook is then called.

The following line starts the Docker Compose process:
config.vm.provision “shell”, inline: “docker-compose -f /vagrant/docker/docker-compose.yml up”

The output of all of this is a process to start up the seven containers that make up the full Magnolia environment with a single command:

vagrant up

After a few minutes the following instances are available:

Author Server
Author Server
HAProxy Endpoint
HAProxy Endpoint
Public Server 1
Public Server 1
Public Server 2
Public Server 2
HAProxy Admin
HAProxy Admin
 

Conclusion

This framework provides an easy way for a developer to set up and test a complete Magnolia solution in a local development environment. With a simple command they can spin up a complete replica of a production environment within their local machine. By running the application locally, developers are able to debug issues with their standard tools, unencumbered by network connectivity, security policies, or other issues that may effect real-world infrastructure, and can replicate and resolve common issues that can occur when running multiple public servers behind a load balancer.

Leave a Comment

(required)