Containerizing AEM with Docker for Local Development

Australian teams building on Adobe Experience Manager have steadily adopted containerised workflows over the past few years, particularly as Sydney and Melbourne agencies expand distributed engineering squads. Running AEM locally has traditionally meant installing a QuickStart JAR, configuring a JDK, applying a licence file, and juggling OS-specific paths that often broke when a colleague on a different platform tried to reproduce the same setup. Docker shifts that responsibility away from individual laptops and into a portable image, where the operating system, Java version, and AEM runmode are pinned and shared across the team.

The interest is not purely cosmetic. Australian developers working across the NBN's variable connection speeds have learned that a self-contained image that boots predictably on a coffee shop Wi-Fi in Brisbane or a corporate VPN in Perth saves hours each sprint. A reproducible container also bridges the gap between the dev laptop and the staging cluster, which matters when offshore teams in Adelaide or Hobart pick up tickets mid-cycle. Adobe's own engineering blog and the wider Adobe developer ecosystem have leaned into containers since around 2020, and the patterns have stabilised enough to codify into a reliable local workflow.

For those attending developer-focused conferences like CIRCUIT, the practical questions come thick and fast: which base image to trust, how to seed author and publish instances, how to map CRX packages, and how to keep dispatcher configuration consistent. These are exactly the questions a container-based workflow answers cleanly. The circuit-app tooling discussion from the Chicago sessions pointed many attendees toward images that ship with the runmode already wired up, and the recorded talks remain a useful reference point for teams formalising their approach.

This piece walks through the moving parts of a Dockerised AEM setup aimed at local development. It assumes familiarity with AEM author and publish instances but no prior container experience, and it covers image construction, networking choices, persistent storage, and how the approach compares with a traditional local install.

Why a containerised AEM image changes daily work

AEM is a Java servlet-based platform that bundles an Apache Felix OSGi container, a JCR repository built on Jackrabbit Oak, and the CQ servlet engine. The legacy requirement of a JDK 8 or 11 installation on macOS, Windows, or a specific Linux distribution created a slow drift across teams, with patches applied locally and never quite synced. Containers collapse that drift into a single immutable layer that every developer pulls from the same registry.

A developer on a Perth-based engagement who switches between AEM 6.5 and the newer AEM as a Cloud Service SDK can keep both environments running side-by-side without port collisions or JDK juggling. The Australian Privacy Principles under the Privacy Act 1988 also nudge teams toward tighter control over local data, and a containerised CRX with explicit volume mounts makes it easier to demonstrate where repository content actually lives during an internal review.

The practical payoff appears early. Spin-up times drop because the image boots directly into a pre-warmed AEM process. Reproducibility becomes a property of the Dockerfile rather than tribal knowledge passed between engineers in a Brisbane Slack channel. New starters can clone a repository, run docker compose up, and have an author instance answering on port 4502 within minutes rather than spending half a day troubleshooting a QuickStart licence mismatch.

Building an AEM Docker image from scratch

The Dockerfile is the centrepiece. A sensible base is either eclipse-temurin:11-jdk or amazoncorretto:11, since AEM 6.5 LTS ships with Java 11 support and the Cloud Service SDK requires Java 11 or 17. The image copies the AEM QuickStart JAR, the licence properties file, and a startup script that unpacks the JAR on first run, then launches the instance with the appropriate runmode flag baked into JAVA_OPTS.

A minimal layer structure keeps image size manageable. The base JDK layer is shared across all AEM versions, the AEM binaries sit on top, and the licence plus any custom OSGi bundles form the smallest, most frequently rebuilt top layer. Multi-stage builds can keep the final image lean by separating the unpacked QuickStart directory from the runtime, which means the resulting image does not ship with development-only artefacts.

Environment variables deserve deliberate attention. AEM_HOST, AEM_PORT, AEM_RUNMODE, and JAVA_OPTS belong in the image's ENV declarations, with sensible defaults that match the demo standards discussed at CIRCUIT. The recorded speaker presentations include a few working examples of this pattern, particularly for teams that wanted publish instances on 4503 and a dispatcher on port 80 wired up by default.

Networking, ports, and dispatcher configuration

AEM author, publish, and dispatcher services need to be reachable from both the host machine and other containers. A Docker bridge network with named services lets the dispatcher container resolve aem-author and aem-publish by hostname rather than IP, which keeps the configuration stable across restarts. Port mappings then expose 4502, 4503, and 80 to the host so a developer can browse the sites from a local browser without extra tooling.

The Apache HTTP Server-based dispatcher requires a slightly different approach. Its configuration relies on the renderer hostname, and that hostname should match the Docker service name to avoid rendering errors after container restarts. Mounting the dispatcher.conf, vhosts, and farms directories as bind mounts during development lets engineers tweak rules without rebuilding the image, while production-style builds copy those files into the image layer for reproducibility.

Time zone alignment is a small but common gotcha for distributed Australian squads. Setting TZ=Australia/Sydney or TZ=Australia/Perth in the container ensures that JCR timestamps, log entries, and scheduled jobs line up with the developer's wall clock. Forgetting this is a frequent source of confusion during handover between teams in NSW and WA, where a two- or three-hour shift can obscure the order of events in a log when everyone reads it the next morning.

Persistent storage, volumes, and content sync

AEM stores its content repository, segment store, and datastore under crx-quickstart inside the working directory. Without persistent storage, every container restart wipes local changes and forces a fresh unpack from the QuickStart JAR. Named volumes solve this cleanly, with one volume for the segment store and a separate one for the file datastore, so a crashed JVM never costs the team a day's worth of authored content.

Bind mounts are an alternative when developers want to inspect or back up the repository directly from the host filesystem. A common pattern is to mount ./aem-data/author to /opt/aem/crx-quickstart, which keeps the content alongside the project source tree in version control. For teams concerned about Australian data sovereignty under the Privacy Act 1988, this explicit mount makes it easier to reason about where repository files physically reside on the engineer's laptop.

Seeding reproducible content is a related concern. CRX packages exported from a reference environment can be mounted at a known path, and a startup script can install them via the Package Manager HTTP endpoint. The combination of persistent volumes for author state and ephemeral packages for seeding gives teams a fast reset button without losing genuine local edits made during the working day.

Comparing Dockerised AEM with a traditional local install

A side-by-side view clarifies where the container approach earns its keep and where a bare-metal install still has merit for a single-developer shop.

Aspect Docker-based AEM Traditional local install
Setup time for new developer Under 30 minutes with a prebuilt image Half a day to a full day
JDK version control Pinned in the Dockerfile Managed per machine, often drifts
Parallel AEM versions Multiple containers, isolated ports Difficult, frequent conflicts
Operating system parity Linux container regardless of host OS Varies by developer laptop
Resource isolation Cgroups limit CPU and memory Competes with other apps
Rollback after broken config Re-pull previous image tag Manual uninstall and reinstall
Network access for new starters Works offline once image is local Often requires repeated downloads

The trade-off is the upfront investment in authoring and maintaining the Dockerfile and compose files. Teams that rarely change AEM versions or rarely onboard new engineers may not see a strong return, while larger Australian shops with rotating contractors and offshore partners usually recover the cost within a single onboarding cycle.

The habits and pitfalls below make the difference between a setup that survives a six-month project and one that breaks after the first JDK upgrade. Embed them in your repository documentation and review them whenever the base image changes.

Practical habits that make the setup stick

  • Document the docker compose up sequence alongside the README so a developer in Melbourne or a contractor in Manila can follow the same steps without tribal knowledge.
  • Pin every base image and AEM version with a digest, not just a tag, so that upstream changes do not silently break the local build.
  • Keep the licence properties file out of version control and inject it through an untracked .env file or a Docker secret.
  • Schedule a quarterly rebuild of the base image to pull in JDK security patches and any updated AEM service packs.

Common pitfalls to watch for

  • Letting the segment store volume fill the host disk by forgetting to prune dangling images and stopped containers.
  • Binding to port 4502 directly when another local AEM already uses it, leading to silent failures on first boot.
  • Forgetting to map the JVM debug port, which then makes remote debugging from an IDE impossible.
  • Treating the Docker image as production-ready when it is in fact only suitable for local development, with relaxed security settings.

Teams ready to formalise their approach can review the recorded sessions and slide decks on the event homepage to see how presenters structured their reference images. Bringing that structure into your own repository is the shortest path from a flaky QuickStart workflow to a dependable local environment that any developer, anywhere, can launch with a single command.