Skip to content
CMO & CTO
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

  • Digital Experience
    • Experience Strategy
    • Experience-Driven Commerce
    • Multi-Channel Experience
    • Personalization & Targeting
    • SEO & Performance
    • User Journey & Behavior
  • Marketing Technologies
    • Analytics & Measurement
    • Content Management Systems
    • Customer Data Platforms
    • Digital Asset Management
    • Marketing Automation
    • MarTech Stack & Strategy
    • Technology Buying & ROI
  • Software Engineering
    • Software Engineering
    • Software Architecture
    • General Software
    • Development Practices
    • Productivity & Workflow
    • Code
    • Engineering Management
    • Business of Software
    • Code
    • Digital Transformation
    • Systems Thinking
    • Technical Implementation
  • About
CMO & CTO

Closing the Bridge Between Marketing and Technology, By Luis Fernandez

AEM Environments with Docker

Posted on October 1, 2018 By Luis Fernandez

Local AEM that starts fast, feels clean, and does not cling to your laptop is a dream many of us share.

I have been spinning Adobe Experience Manager on laptops for years and I keep coming back to the same wish list. I want a repeatable AEM environment, I want to kill it when I am done, and I want to keep my project files outside of the belly of the beast. Docker makes that possible with a few caveats. We do not get official images from Adobe due to licensing, so each team needs to build its own base images using the Quickstart jar and a license file. That is not a blocker. It is a chance to make the setup tidy, readable, and friendly to new devs who just want to run author and publish without a tour of your whole repo. The nice surprise is that you can run author, publish, and dispatcher together, map a couple of ports, and keep content on named volumes so your containers stay disposable. If you tried this a while back and hit slow file I O on mac laptops, do not bind mount the whole crx folder. Use volumes for the repo and bind mount only your code packages and configs. Your fan will thank you and your teammate on Windows will thank you too.

Before we jump into Compose, start with a small and boring author image. Keep the Dockerfile readable. Layer it so that the Quickstart install does not re run every time you tweak a config. Do not bake client code into the image. Put that in an install folder that is volume or bind mounted at runtime. Keep secrets like license files out of your repo and out of image layers by passing them at build time or mounting them at runtime. A tiny .dockerignore also helps. Here is a simple author build that stays close to the defaults and makes the run modes explicit.

# ./author/Dockerfile
FROM openjdk:8-jre

ENV AEM_HOME=/opt/aem \
    AEM_PORT=4502 \
    AEM_RUNMODES=author,crx3,crx3tar \
    JAVA_OPTS="-Xms2g -Xmx4g -XX:+UseG1GC"

WORKDIR ${AEM_HOME}

# Copy in the Quickstart jar but not the license
# Put license.properties next to the jar at build or mount at runtime
COPY aem-quickstart.jar ${AEM_HOME}/aem-quickstart.jar

# Unpack once so later layers do not redo it
RUN java -jar aem-quickstart.jar -unpack && \
    mkdir -p crx-quickstart/install crx-quickstart/packages crx-quickstart/logs

# This install folder is where you drop content packages at runtime
VOLUME ["${AEM_HOME}/crx-quickstart", "${AEM_HOME}/crx-quickstart/install"]

EXPOSE ${AEM_PORT}

HEALTHCHECK --interval=15s --timeout=5s --retries=20 \
  CMD curl -sf http://localhost:${AEM_PORT}/system/health || exit 1

CMD bash -lc "ls -l license.properties || echo 'missing license.properties'; \
  exec java ${JAVA_OPTS} -jar ${AEM_HOME}/aem-quickstart.jar -p ${AEM_PORT} -nofork -r ${AEM_RUNMODES}"

That image expects a license.properties file at runtime. Mount it as a read only file next to the jar or inject it at build time through a build arg that you never commit. I like to mount it. It is clearer and you do not leak it into an image layer. For publish you can reuse the same Dockerfile with a different run mode and port. Dispatcher sits in front with Apache httpd and the dispatcher module. There are several ways to ship that module. You can bake it into an image or download it on container start. I prefer a tiny build step that drops the right module for the platform into the image and keeps the config in the repo. The key for sanity is Compose readability. Short service names, clear ports, and named volumes tell the story without a doc. Here is a compact compose file that I have been using this month for a greenfield site.

# docker-compose.yml
version: "3.5"

volumes:
  author-crx: {}
  publish-crx: {}

services:
  author:
    build: ./author
    container_name: aem-author
    ports:
      - "4502:4502"
    environment:
      - AEM_RUNMODES=author,crx3,crx3tar
    volumes:
      - author-crx:/opt/aem/crx-quickstart
      - ./license/author/license.properties:/opt/aem/license.properties:ro
      - ./packages:/opt/aem/crx-quickstart/install:ro

  publish:
    build: ./publish
    container_name: aem-publish
    ports:
      - "4503:4503"
    environment:
      - AEM_RUNMODES=publish,crx3,crx3tar
    volumes:
      - publish-crx:/opt/aem/crx-quickstart
      - ./license/publish/license.properties:/opt/aem/license.properties:ro
      - ./packages:/opt/aem/crx-quickstart/install:ro
    depends_on:
      - author

  dispatcher:
    build: ./dispatcher
    container_name: aem-dispatcher
    ports:
      - "8080:80"
    volumes:
      - ./dispatcher/conf/httpd.conf:/usr/local/apache2/conf/httpd.conf:ro
      - ./dispatcher/conf/dispatcher.any:/etc/httpd/conf/dispatcher.any:ro
    depends_on:
      - publish

Notice the story the file tells. author and publish share the same packages folder as a read only install folder. Content packages you drop there will auto install on start. That keeps your image free of project code and lets you iterate on content packages without a rebuild. Each service uses a named volume for crx quickstart so the repository data survives container restarts while keeping your host file system out of the hot path. You can also add small health checks, then make dispatcher wait on publish by checking a friendly URL like a dispatcher health page or a publish ready check endpoint. Keep the ports simple. Keep names short. Keep secrets mounted. The drift from a clean compose file to a wall of env vars is real. Resist it by moving defaults into your Dockerfiles and only surfacing the bits that change per project.

A few practical tips save time. Run modes are your best friend. Put config in /apps/yourapp/config.author and /apps/yourapp/config.publish so the right OSGi values land in the right place without manual clicks. Do not click in dev. If you find yourself opening Felix to toggle a value, script it into a config file and add it to your content package. For replication, ship agent configs as code and bind them to environment driven endpoints. A small bash entrypoint that swaps hostnames based on env vars keeps configs portable.

# Example entrypoint snippet for publish agent host swap
#!/usr/bin/env bash
set -e

PUBLISH_HOST=${PUBLISH_HOST:-publish}
PUBLISH_PORT=${PUBLISH_PORT:-4503}

sed -i "s/host="publish"/host="${PUBLISH_HOST}"/g" /opt/aem/crx-quickstart/install/replication-agents/author/publish.xml
sed -i "s/port="4503"/port="${PUBLISH_PORT}"/g" /opt/aem/crx-quickstart/install/replication-agents/author/publish.xml

exec "$@"

On mac laptops Docker still runs inside a small virtual machine, so avoid heavy bind mounts inside crx quickstart. Use named volumes for that path and keep bind mounts for code and configs only. If you must inspect repo files, open a shell inside the container. Also keep logs outside of the repo to avoid noisy changes in your working tree. A log path inside the volume solves that. For new devs, add a tiny makefile or a script with two commands. One command to build base images with a check for the Quickstart jar and license. One command to bring the stack up. The fewer steps the better. The script can print friendly help with the exact URLs to click for author and dispatcher. Small things like that reduce the onboarding time and lower the number of slack pings.

The last bit is readability. Compose is a config file, not a novel. Keep comments brief and point to a docs folder if you need to explain choices. Name things like you would in code. No mystery abbreviations. Use the same env var names in all services when the meaning is the same. If you are using a local dispatcher, keep the dispatcher.any tidy. Split vhosts and farms into separate files and include them. Default deny, then allow what you need. Add a small status handler that shows you cache stats so you can verify your cache keys quickly. For AEM itself, turn off sample content right away and keep the Quickstart flags the same across the team. Small differences here lead to weird bugs later. If your team uses feature branches, tag your images with the branch name inside your own registry or cache a few tags locally. That keeps context switches snappy when you hop between tasks.

With this setup you can run an AEM author publish dispatcher stack on any machine your team carries, keep repo data persistent yet disposable, and keep project code in your repo rather than inside an opaque image layer. It also gives you a clean way to run integration tests against author by scripting package installs and smoke checks in a container that depends on author. I like a tiny test runner container that waits for author to be healthy, posts a content package through Package Manager, hits a couple of URLs, and prints a simple pass or fail. You can wire that as a step in your build server. When someone asks where to start, point them to one command and two ports. That is the vibe we aim for.

Bottom line keep AEM images boring, keep configs as code, keep Compose readable, and keep your teammates out of the weeds.

Ship the jar, not your machine.

Keep the containers boring, keep the code clear.

Code Development Practices Software Engineering coding-practicesjavaspring

Post navigation

Previous post
Next post
  • Digital Experience (94)
    • Experience Strategy (19)
    • Experience-Driven Commerce (5)
    • Multi-Channel Experience (9)
    • Personalization & Targeting (21)
    • SEO & Performance (10)
  • Marketing Technologies (92)
    • Analytics & Measurement (14)
    • Content Management Systems (45)
    • Customer Data Platforms (4)
    • Digital Asset Management (8)
    • Marketing Automation (6)
    • MarTech Stack & Strategy (10)
    • Technology Buying & ROI (3)
  • Software Engineering (310)
    • Business of Software (20)
    • Code (30)
    • Development Practices (52)
    • Digital Transformation (21)
    • Engineering Management (25)
    • General Software (82)
    • Productivity & Workflow (30)
    • Software Architecture (85)
    • Technical Implementation (23)
  • 2025 (12)
  • 2024 (8)
  • 2023 (18)
  • 2022 (13)
  • 2021 (3)
  • 2020 (8)
  • 2019 (8)
  • 2018 (23)
  • 2017 (17)
  • 2016 (40)
  • 2015 (37)
  • 2014 (25)
  • 2013 (28)
  • 2012 (24)
  • 2011 (30)
  • 2010 (42)
  • 2009 (25)
  • 2008 (13)
  • 2007 (33)
  • 2006 (26)

Ab Testing Adobe Adobe Analytics Adobe Target AEM agile-methodologies Analytics architecture-patterns CDP CMS coding-practices content-marketing Content Supply Chain Conversion Optimization Core Web Vitals customer-education Customer Data Platform Customer Experience Customer Journey DAM Data Layer Data Unification documentation DXP Individualization java Martech metrics mobile-development Mobile First Multichannel Omnichannel Personalization product-strategy project-management Responsive Design Search Engine Optimization Segmentation seo spring Targeting Tracking user-experience User Journey web-development

©2025 CMO & CTO | WordPress Theme by SuperbThemes