• Global. Remote. Office-free.
  • Mon – Fri: 8:00 AM to 5:00 PM (Hong Kong Time)
English

Warning: foreach() argument must be of type array|object, bool given in /var/www/html/wp-content/plugins/wp-builder/core/Components/ShiftSaas/Global/topbar.php on line 50

Validate AEM Dispatcher Config Like Cloud Manager

By Vuong Nguyen September 21, 2025 19 min read

Most AEM projects keep a root-level dispatcher/ folder (as generated by the AEM Project Archetype). If your repo doesn’t have it yet, create one using the archetype or copy a starter from your team, and place it alongside your top-level pom.xml.

Then download the AEM as a Cloud Service SDK from Adobe Software Distribution to get the Dispatcher Tools (validate.shdocker_run.sh). For a step-by-step SDK setup, see: https://www.shiftsaas.com/adobe-experience-manager/setting-up-aem-sdk-local-environment/

Set Up Local Dispatcher Validation with the AEM SDK

Download the latest AEM SDK from Adobe’s Software Distribution portal to mirror Cloud Service locally and validate configurations with the most recent release.

Once extracted, the SDK provides both a .sh script for macOS/Linux and a .zip package for Windows. On macOS/Linux, you can make the script executable and run it, for example:

chmod +x aem-sdk-dispatcher-tools-2.0.256-unix.sh
./aem-sdk-dispatcher-tools-2.0.256-unix.sh

On Windows, simply use the .zip package—no extra commands are required.

The extracted dispatcher SDK includes bin/docs/lib/, and src/ folders—providing scripts, documentation, and dispatcher images needed to validate configs and simulate Cloud Manager checks locally.

dispatcher-sdk-2.0.256 % tree
.
├── README
├── bin
│   ├── docker_immutability_check.sh
│   ├── docker_run.sh
│   ├── docker_run_hot_reload.sh
│   ├── update_maven.sh
│   ├── validate.sh
│   ├── validator -> ./validator-darwin-arm64
│   ├── validator-darwin-arm64
│   └── validator-linux-arm64
├── docs
│   ├── README.html
│   ├── TransitionFromAMS.html
│   ├── TroubleShooting.html
│   └── Validator.html
├── lib
│   ├── configuration_reloading.sh
│   ├── dispatcher-publish-amd64.tar.gz
│   ├── dispatcher-publish-arm64.tar.gz
│   ├── dummy_gitinit_metadata.sh
│   ├── httpd-reload-monitor
│   ├── httpd-vhosts
│   ├── immutability_check.sh
│   ├── import_sdk_config.sh
│   └── overwrite_cache_invalidation.sh
└── src

Next, copy bindocs, and lib into the repo’s dispatcher folder—your project tree will then look like this.

With the SDK bits in place, run the validator from the dispatcher module itself to mirror Cloud Manager’s checks. This command executes three passes—static rules, a dockerized Apache/dispatcher syntax check, and immutable-file parity.

cd dispatcher            
./bin/validate.sh src

Use the AEM SDK’s dispatcher tools to check your config locally: run bin/validate.sh src to compile, then bin/docker_run.sh to test routing, filters, and caching. Next, we walk through Phases 1–3—the typical failures in each and how to fix them.

Validator Phases 1-3: Top Failures & How to Fix Them Quickly

Before you run the dispatcher validator, choose and configure the correct host pattern—use dedicated, host-specific vhosts for each environment and keep a strict 404 catch-all—so validation and runtime tests reflect the intended routing, headers, and caching.

  1. Catch-all vhostServerAlias * in default.vhost. Matches any Host header. Use only as a strict 404 fallback for unknown hosts.
  2. Dedicated vhost (recommended)ServerName portaldev.flagtick.com with optional ServerAlias portaldev.flagtick.local. Create one per environment/domain for predictable routing, headers, and cache rules.

For example, with option 1 we assume that flagtick.com points to another server, while portaldev.flagtick.com targets AEMaaCS. In this case, we would use flagtick.vhost (the name flagtick can be changed to custom.vhost or any other name when using option 1).

# Include customer defined variables
Include conf.d/variables/custom.vars

<VirtualHost *:80>
    ServerName	"publish"
    # Put names of which domains are used for your published site/content here
    ServerAlias	 "*"
    # Use a document root that matches the one in conf.dispatcher.d/default.farm
    DocumentRoot "${DOCROOT}"
    # URI dereferencing algorithm is applied at Sling's level, do not decode parameters here
    AllowEncodedSlashes NoDecode
    # Add header breadcrumbs for help in troubleshooting
    <IfModule mod_headers.c>
        Header add X-Vhost "publish"
    </IfModule>
    <Directory />
        <IfModule disp_apache2.c>
            # Some items cache with the wrong mime type
            # Use this option to use the name to auto-detect mime types when cached improperly
            ModMimeUsePathInfo On
            # Use this option to avoid cache poisioning
            # Sling will return /content/image.jpg as well as /content/image.jpg/ but apache can't search /content/image.jpg/ as a file
            # Apache will treat that like a directory.  This assures the last slash is never stored in cache
            DirectorySlash Off
            # Enable the dispatcher file handler for apache to fetch files from AEM
            SetHandler dispatcher-handler
        </IfModule>
        Options FollowSymLinks
        AllowOverride None
        # Insert filter
        SetOutputFilter DEFLATE
        # Don't compress images
        SetEnvIfNoCase Request_URI \.(?:gif|jpe?g|png)$ no-gzip dont-vary
        # Prevent clickjacking
        Header always append X-Frame-Options SAMEORIGIN
    </Directory>
    <Directory "${DOCROOT}">
        AllowOverride None
        Require all granted
    </Directory>
    <IfModule disp_apache2.c>
        # Enabled to allow rewrites to take affect and not be ignored by the dispatcher module
        DispatcherUseProcessedURL	On
        # Default setting to allow all errors to come from the aem instance
        DispatcherPassError		0
    </IfModule>
    <IfModule mod_rewrite.c>
        RewriteEngine	on
        Include conf.d/rewrites/rewrite.rules
        
        RewriteCond %{HTTP:X-Forwarded-Proto} !https
        RewriteCond %{REQUEST_URI} !^/dispatcher/invalidate.cache
        RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]

        # Rewrite index page internally, pass through (PT)
        RewriteRule "^(/?)$" "/index.html" [PT]

    </IfModule>
</VirtualHost>

After adding flagtick.vhost do these steps in order:

# 1. Enable the vhost (if not already)
ln -s ../available_vhosts/flagtick.vhost src/conf.d/enabled_vhosts/flagtick.vhost

# 2. Run validator
./bin/validate.sh src

On the other hand, if you follow option 2, the ServerName and ServerAlias should point to portaldev.flagtick.com in the Cloud Manager domain mapping, with portaldev.flagtick.local as an optional local alias, defined inside portal_dev_publish.vhost.

## FlagTick DEV Publish virtual host
<VirtualHost *:80>
    AllowEncodedSlashes On

    ## Primary hostname for FlagTick DEV publish
    ServerName    portaldev.flagtick.com
    ServerAlias   portaldev.flagtick.local
    ServerAlias   flagtick.cloud

    ## Dispatcher document root
    DocumentRoot ${DOCROOT}

    <IfModule mod_headers.c>
        Header always add X-Vhost "flagtick-publish-dev"
        Header merge X-Frame-Options SAMEORIGIN "expr=%{resp:X-Frame-Options}!='SAMEORIGIN'"
        Header merge X-Content-Type-Options nosniff "expr=%{resp:X-Content-Type-Options}!='nosniff'"
        Header set Permissions-Policy "browsing-topics=()"
    </IfModule>

    <Directory />
        <IfModule disp_apache2.c>
            ModMimeUsePathInfo On
            DirectorySlash Off
            SetHandler dispatcher-handler
        </IfModule>
        Options FollowSymLinks
        AllowOverride None
    </Directory>

    <Directory "${DOCROOT}">
        AllowOverride None
        Require all granted
    </Directory>

    <IfModule disp_apache2.c>
        DispatcherUseProcessedURL 1
        DispatcherPassError  0
    </IfModule>

    <IfModule mod_rewrite.c>
        RewriteEngine on

        ## Enforce HTTPS
        RewriteCond %{HTTP:X-Forwarded-Proto} !https
        RewriteCond %{REQUEST_URI} !^/dispatcher/invalidate.cache
        RewriteRule (.*) https://%{SERVER_NAME}%{REQUEST_URI} [L,R=301]

        ## Project-specific rewrites
        Include conf.d/rewrites/flagtick_301_rewrite.rules
        Include conf.d/rewrites/portal_dev_rewrite.rules
        Include conf.d/rewrites/rewrite.rules
    </IfModule>
</VirtualHost>

Next, enable your custom vhost and farm by creating symlinks from available_* into enabled_*. This ensures Apache picks up the vhost and Dispatcher routes requests through the correct farm.

ln -s ../available_vhosts/portal_dev_publish.vhost 
src/conf.d/enabled_vhosts/portal_dev_publish.vhost

ln -s ../available_farms/portal_dev_publish.farm src/conf.dispatcher.d/enabled_farms/portal_dev_publish.farm

In /etc/hosts, make sure you have:

127.0.0.1 portaldev.flagtick.local

Then:

  • Local Author (4502): http://portaldev.flagtick.local:4502/
  • Local Publish (4503): http://portaldev.flagtick.local:4503/
  • Dispatcher SDK (8080/8081): http://portaldev.flagtick.local:8081/

Below folder structure keeps the configuration organized and makes validation easier. Each environment’s vhost and farm can be tested separately with the Dispatcher SDK or Docker before deploying to Adobe Cloud Manager.

One of the most common Phase 1 screw-ups is when the validator says it can’t find any .vhost files in conf.d/enabled_vhosts/.

./bin/validate.sh src
Phase 1: Dispatcher validator
Error: no .vhost files found in conf.d/enabled_vhosts/
Warning: ignoreUrlParameters not set in farm (marketing params recommended)
Phase 1 failed

That usually means you copied the vhost files instead of linking them, or the symlinks got wiped. Cloud Manager only cares about symlinks in enabled_vhosts/, so the fix is easy: keep the real files in available_vhosts/ and drop links back in with ln -s.

ln -s ../available_vhosts/default.vhost src/conf.d/enabled_vhosts/default.vhost

After fixing the symlinks, it is a good idea to double-check that Apache can actually parse your configs before re-running the full validator. The simplest way is by running the built-in syntax check:

httpd -t

On macOS, the main config file is typically located at /etc/apache2/httpd.conf. If you see the warning about “Could not reliably determine the server’s fully qualified domain name”, you can fix it by editing that file and setting:

ServerName localhost

Validate routing, caching, and filter rules in the same runtime as Cloud Manager. Install and run Docker locally to avoid errors:

Next, move into the dispatcher lib directory, locate the file (e.g., dispatcher-publish-arm64.tar.gz), and load it into Docker Desktop:

cd ../dispatcher/lib  
docker load -i dispatcher-publish-arm64.tar.gz

Once loaded, open Docker Desktop and confirm the Dispatcher Adobe image is available by running:

% docker images
REPOSITORY                        TAG       IMAGE ID       CREATED       SIZE
adobe/aem-cs/dispatcher-publish   2.0.256   86f18dd3966f   6 weeks ago   149MB

With the image confirmed, you can now start the dispatcher container. First, prepare an out folder with your config, then launch using the provided script:

cd dispatcher  
mkdir out  
cp -R src/* out/  
./bin/docker_run.sh out host.docker.internal:4503 8080  

Note: This mounts your dispatcher configuration into the container and connects it to your local AEM Publish instance running on port 4503.

There is common issue which that Dispatcher starts but can’t reach AEM Publish on port 4503, so the script keeps retrying with the ‘Sleeping for 5s…’ message.

%./bin/docker_run.sh ../out host.docker.internal:4503 8080  
Darwin MacBook-Pro-cua-Vuong.local 24.6.0 Darwin Kernel Version 24.6.0: Mon Jul 14 11:30:40 PDT 2025; root:xnu-11417.140.69~1/RELEASE_ARM64_T8132 arm64
/bin/zsh
Running script /docker_entrypoint.d/05-display-image-version.sh
...
...
Waiting until port 4503 on host.docker.internal is available (with timeout of 1s)
Sleeping for 5s to wait until port 4503 on host.docker.internal is available
Sleeping for 5s to wait until port 4503 on host.docker.internal is available

If 8080 is in use, run docker ps, stop the blocker with docker kill <id> (or docker rm -f <id>), or start Dispatcher on a different port.

Which diagram below shows requests hitting Dispatcher (8080); cache misses proxy to Publish (4503). Author (4502) activates content to Publish and triggers cache invalidation back to Dispatcher.

Before moving further, when we add the SDK folders from AEMaaCS such as lib, docs, and bin into our local setup for validation, we should also update our .gitignore. This ensures these large SDK artifacts are not accidentally committed and pushed to AEMaaCS Cloud.

# dispatcher-sdk
dispatcher/lib
dispatcher/docs
dispatcher/bin
dispatcher/cache
dispatcher/out

Validate Using WKND Prior to Your Project

With Dispatcher running, the next step is to add real content — Adobe’s WKND site provides a complete AEM structure out of the box.

Note: If you are running Java 11 locally, use the WKND 3.2.0 release (for example, aem-guides-wknd.all-3.2.0.zip). For Java 21, you can install the latest WKND from Adobe’s GitHub: aem-guides-wknd

[INFO] ------------------------------------------------------------------------
[INFO] Reactor Summary for WKND Sites Project - Reactor Project 3.2.0:
[INFO] 
[INFO] WKND Sites Project - Reactor Project ............... SUCCESS [  0.726 s]
[INFO] WKND Sites Project - Core .......................... SUCCESS [ 38.908 s]
[INFO] WKND Sites Project - UI Frontend ................... SUCCESS [ 38.375 s]
[INFO] WKND Sites Project - UI apps structure ............. SUCCESS [  1.040 s]
[INFO] WKND Sites Project - UI apps ....................... SUCCESS [  4.041 s]
[INFO] WKND Sites Project - UI content .................... SUCCESS [  1.411 s]
[INFO] WKND Sites Project - UI config ..................... SUCCESS [  0.162 s]
[INFO] WKND Sites Project - UI sample content ............. SUCCESS [  0.816 s]
[INFO] WKND Sites Project - All ........................... SUCCESS [ 42.472 s]
[INFO] WKND Sites Project - Integration Tests ............. SUCCESS [ 26.077 s]
[INFO] WKND Sites Project - Dispatcher .................... SUCCESS [  0.564 s]
[INFO] WKND Sites Project - UI Tests ...................... SUCCESS [  0.347 s]
[INFO] ------------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] ------------------------------------------------------------------------
[INFO] Total time:  02:47 min
[INFO] Finished at: 2025-09-27T21:19:01+07:00

Next, copy the Dispatcher configuration from your custom project into the WKND setup, then update rewrite.rules to point the default page to portaldev.flagtick.local.

On AEM Publish (4503), WKND installs a root mapping through RootMappingServlet.cfg.json so that / resolves directly to /content/wknd/us/en.html. However, Dispatcher (8080) does not use Sling mappings.

// RootMappingServlet.cfg.json
{
    "rootmapping.target": "/content/wknd/us/en.html"
}

// rewrite.rules
# rewrite for root redirect
RewriteRule ^/?$ /content/${CONTENT_FOLDER_NAME}/us/en.html [PT,L]

For redirecting to login.html, the configuration should be set up like this

// RootMappingServlet.cfg.json
{
    "rootmapping.target": "/content/<project>/us/login.html"
}

// rewrite.rules
# Map the root folder to the login page
RewriteRule ^/?$ /content/<project>/us/login.html [PT,L]

That is why we copy our project’s Dispatcher configuration into WKND and update rewrite.rules — ensuring that hitting http://portaldev.flagtick.local:8080/ also goes to the intended homepage.

It is important to understand where different types of redirects should live. AEM itself is responsible for content-driven routing (like site root or language mapping), while Dispatcher is better suited for infrastructure-level redirects (like forcing HTTPS or handling domain aliases).

If redirect is business/content logic → do it in AEM

  • /etc/map.publish
  • Sling Resource Resolver
  • Redirect Map Manager (ACS Commons, etc.)

If redirect is infra/domain routing → do it in Dispatcher

  • e.g., force HTTPS, remove www, vanity domain to homepage

Once your Dispatcher is up on 8080, you can test a few common checks to confirm it’s wired correctly with AEM Publish (4503).

First, hit the Dispatcher root — a 200 OK confirms it’s proxying to Publish.

curl -v http://portaldev.flagtick.local:8080/

* Host portaldev.flagtick.local:8080 was resolved.
* IPv6: (none)
* IPv4: 127.0.0.1, 127.0.0.1
*   Trying 127.0.0.1:8080...
* Connected to portaldev.flagtick.local (127.0.0.1) port 8080
> GET / HTTP/1.1
> Host: portaldev.flagtick.local:8080
> User-Agent: curl/8.7.1
> Accept: */*
> 
* Request completely sent off
< HTTP/1.1 200 OK
< Date: Sun, 28 Sep 2025 03:34:06 GMT
< Server: Apache
< X-Frame-Options: SAMEORIGIN
< X-Content-Type-Options: nosniff
< Last-Modified: Sun, 28 Sep 2025 01:58:38 GMT
< ETag: "1dd2d-63fd2db7ebc91"
< Accept-Ranges: bytes
< Content-Length: 122157
< Cache-Control: max-age=300
< Expires: Sun, 28 Sep 2025 03:39:06 GMT
< Vary: Accept-Encoding
< X-Vhost: publish
< Content-Type: text/html;charset=utf-8
< 
<!DOCTYPE HTML>

Add a header to show whether requests are served from cache (HIT, MISS, REFRESH).

<IfModule mod_headers.c>
    Header always add X-Vhost "flagtick-publish-dev"
    Header merge X-Frame-Options SAMEORIGIN "expr=%{resp:X-Frame-Options}!='SAMEORIGIN'"
    Header merge X-Content-Type-Options nosniff "expr=%{resp:X-Content-Type-Options}!='nosniff'"
    Header set Permissions-Policy "browsing-topics=()"
    
    # Show Dispatcher cache state (HIT, MISS, REFRESH)
    Header add X-Cache-Status "%{DISP_CACHE_STATE}e"
</IfModule>

If you want to check cache HIT or MISS in HTTP headers, note that the Local Dispatcher SDK doesn’t add this header by default — you have to configure it in Apache. Otherwise, the easiest way is to tail the log file with:

docker ps
docker exec -it <container_id> ls -l /var/log/apache2
docker exec -it <container_id> tail -f /var/log/apache2/dispatcher.log

curl -I http://portaldev.flagtick.local:8080/content/wknd/us/en.html

If the header isn’t available, you can always confirm caching status in the dispatcher log, which reports whether a request was served from cache (hit), fetched fresh (miss), or revalidated (refresh).

[28/Sep/2025:06:45:09 +0000] "HEAD /content/wknd/us/en.html HTTP/1.1" - hit [publishfarm/-] 8ms "portaldev.flagtick.local:8080"

Running the Local Dispatcher over HTTPS

Now that caching is verified, the next step is to enable HTTPS support for your local Dispatcher. This requires configuring SSL certificates in AEM’s Security tools.

Open Tools → Security → SSL Configuration in AEM. Here you will create a Key Store and Trust Store by setting passwords, as shown in the screenshot. These secure the certificates AEM uses to serve content over HTTPS.

In AEM’s SSL wizard:

  • Key Store password → you set it yourself. It protects the private keys and certificates inside the keystore.
  • Trust Store password → you also set it yourself. It secures the trusted CA certificates AEM will use.

Reference link: https://experienceleague.adobe.com/en/docs/experience-manager-learn/foundation/security/use-the-ssl-wizard

At the Key & Certificate step, upload your private key and certificate to finish SSL setup. For local use, Adobe offers a ZIP with a self-signed key (.DER) and cert (.CRT), valid until July 2028 with passphrase admin. As shown, just select the files to proceed.

Next, bind the certificate by setting your local hostname (e.g., portaldev.flagtick.local) and an HTTPS port like 8443in the SSL Connector screen. This enables AEM to serve content securely over HTTPS.

⚠️ Just make sure:

  1. You have mapped portaldev.flagtick.local in /etc/hosts.
  2. Your Dispatcher vhost is updated to listen on *:8443 (or proxied accordingly).

In your case (from earlier screenshots), you were doing the setup in Author’s Tools → Security → SSL Configurations, so https://portaldev.flagtick.local:8443/ will point to Author.

Wrapping up

You now have a Dispatcher configuration validated just like Cloud Manager. You built and deployed it, ran the container locally, checked logs (dispatcher.log), and confirmed cache/invalidation against Publish. Key takeaways: validate rules in Docker before pushing, always watch for HIT/MISS in headers, and keep vhost/filter rules as lean as possible.

Side note: Cloud Manager validation is stricter than local SDK defaults. Always sync your local dispatcher SDK version with the Cloud Manager version to avoid surprises.

What’s next

In the next part, we’ll move from the back-end Dispatcher to the front-end workflow in AEM. You’ll:

  • Break down the ui.frontend module and its folder/package.json structure.
  • Refine Webpack configs (webpack.common.jswebpack.plugins.jsmain.ts) for cleaner builds.
  • Use tools like webpack-bundle-analyzerESLint, and Stylelint for optimization and code quality.
  • Automate clientlibs generation with npm run clientlibs, integrating output into ui.apps.
  • Apply best practices for a lightweight and efficient AEM front-end development workflow.

Prereqs for the next article

  • Docker Desktop installed.
  • AEM Publish running locally (e.g., http://localhost:4503).
  • The Adobe Dispatcher Tools/SDK or AMS Dispatcher Docker image available.
  • A hosts entry for your test domain (e.g., 127.0.0.1 flagtick.local).

When you are ready, jump to: “Validate AEM Dispatcher Config Like Cloud Manager”.

#Adobe Experience Manager