Core design patterns
Going straight to the point: AEM isn't exactly the best kind of application to put in a Docker container. According to its homepage, Docker can "Build, Ship, and Run Any App, Anywhere". That promise shouldn't be trusted blindly though. At its core, Docker is suited best for stateless microservices, not stateful monoliths like AEM.
Microservice vs Monolith
AEM with its modular design based on OSGi is technically a collection of microservices but seen as a self-contained application. It's what you call a monolith. Docker containers show their full potential when used with lightweight services that instantiate quickly. Only then can you properly benefit from fast starting times of containers, compared to virtual machines that need to boot an entire OS first. But AEM usually takes minutes to start anyway, and a fresh installation without any content already uses over 540 MB of disk space before the first boot and 1.9 GB after the first instantiation (AEM 6.3). AEM in Docker can, therefore, neither profit much from the light footprint nor from the scalability through fast instantiation that you gain with containers over virtual machines.
Stateless vs Stateful
Everything is content. A core notion so important that it's probably the first thing every novice AEM developer gets taught. AEM as an application is tightly integrated with its underlying content repository, which in turn makes the application itself content. And that is exactly the reason why it is problematic to put AEM in a Docker container.
Unlike a virtual machine, a container can't be stopped without destroying all files created by the application in the container itself. Of course, the system state of the container could be committed as a new image layer, but the main idea behind not putting a container in a persistent state is that the deployment of a new version is as easy as starting the new instance and deleting the old one, which in the case of AEM would also delete all the added content along with it.
There's another way in Docker to persist data and that is through data volumes. They are initialised when a container is created and don't depend on the container's lifecycle, i.e. they persist even if the container is deleted. This also makes the data volumes shareable and reusable among other containers.
However, this doesn't exactly solve our problem with AEM. If we move the repository to a persistent volume, we don't lose our content anymore. However, it would defeat the purpose of what Docker is meant for, since old application code would be persistent in the volume as well.
Let's put AEM in a container anyway
Let's put aside for a moment that our preconditions to utilize Docker on the whole software lifecycle aren’t optimal. How easy is it to get AEM to run inside of Docker in the first place? Would it really give us an advantage over using proven setups with a virtualization solution like Vagrant? To evaluate that, let's have a look at two equivalent, very basic setups where we start an unconfigured AEM author via the quickstart jar. For both options we'll perform the following steps:
Install a Java SDK in the VM / container
Copy quickstart jar and license to VM / container
Unpack the jar file to /opt/cq
Execute the quickstart bash script
Expose port 4502 of the VM / container to the host machine to access the running AEM instance
(If you want to try the following code snippets yourself, please make sure to place the quickstart jar along with the license file in the same directory as your Vagrantfile and Dockerfile. For a more sophisticated setup with images for author, publish and load balancer instances, I recommend having a look at some of the public images on Docker Hub)
First, let's see how a basic Vagrant setup could look like. We need to define an OS for the virtual machine and allocate the system resources. We'll then execute a bootstrap shell script to perform the steps mentioned above.
# -*- mode: ruby -*-
# vi: set ft=ruby :
Vagrant.configure("2") do |config|
config.vm.box = "centos/7"
config.vm.provider "virtualbox" do |vb|
vb.customize ["modifyvm", :id,
"--memory", "2048", "--cpus", "2",
config.vm.provision :shell, path: "bootstrap.sh"
config.vm.network :forwarded_port, guest: 4502, host: 4502
sudo yum -y update
sudo yum -y install java-1.8.0-openjdk
cp /vagrant/cq-author-4502.jar /opt/cq/
cp /vagrant/license.properties /opt/cq/
java -XX:MaxPermSize=256m -Xmx1024M -jar cq-author-4502.jar -unpack -r nosamplecontent
To create our VM and start AEM, we simply run the command vagrant up in the directory where our Vagrantfile resides.
Now let's do the same in Docker. Since containers don't simulate a machine but rather share the host's kernel on the process level, we don't need to allocate system resources. The container will just take as many resources as it needs and at most, what the host's kernel scheduler will allow. Technically for Docker, we could have saved the step to install Java by just choosing the official OpenJDK Docker image as a base. But to make our examples a bit more comparable, we will use the CentOS 7 image as a base instead and install Java manually.
RUN yum -y update
RUN yum -y install java-1.8.0-openjdk
# Copies required build media
ADD cq-author-4502.jar /opt/cq/cq-author-4502.jar
ADD license.properties /opt/cq/license.properties
# Extracts AEM
RUN java -XX:MaxPermSize=256m -Xmx1024M -jar cq-author-4502.jar -unpack -r nosamplecontent
To build the Docker image and add it to the local image registry, we execute the following command in the Folder where the Dockerfile resides:
docker build -t aem-test .
-t aem-test defines a tag for our image. The following command is then used to start a container from our newly created image:
docker run --name AEM_AUTHOR_6.3 -p 4502:4502 -d aem/author:6.3
With --name AEM_AUTHOR_6.3 we define a name for our container, -p 4502:4502 maps the internal AEM port that we used to start AEM to an external port on the host machine and -d aem/author:6.3 tells Docker to use the aem/author image version 6.3 and run it in detached mode (otherwise the container would exit when the root process used to run the container exits).