• 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 20 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.sh, docker_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 Manager locally and validate configurations with the most recent release.

After extracting the SDK you’ll find scripts for macOS/Linux and a zip for Windows, plus docs, lib and example sources.

macOS / Linux (example):

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

Windows: unzip the package and follow the included README — no shell commands required.

Inside the Dispatcher SDK you will find the bin/, docs/, lib/, and src/ folders. These give you the scripts, documentation, validator tools, and images you need to validate your configs and run Cloud Manager-style checks locally.

Example tree (for orientation):

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

Copy the bin/, docs/, and lib/ folders into your project dispatcher/ folder. After copying, your project tree should include dispatcher/bin, dispatcher/docs, dispatcher/lib, and dispatcher/src.

From your dispatcher module run the validator to mirror Cloud Manager’s checks. The typical command sequence:

cd dispatcher            
./bin/validate.sh src

What the validator checks

  1. Static rule checks
  2. Apache/Dispatcher syntax checks (inside Docker)
  3. Immutable-file parity checks

When prompted: “Do you want to update the immutable files?”

  • yes → updates baseline immutable files (use when upgrading SDK)
  • no → keeps your current baseline (use for regular validation)

Tip: Use yes only during planned upgrades. For normal day-to-day validation, choose no.

Domain Mapping Models: Cloud Manager vs Dispatcher

Model 1: Domain Mapped in Adobe Cloud Manager (AEMaaCS)

When Cloud Manager handles domain mapping, the Dispatcher does not receive the real domain.
Adobe’s CDN normalizes the incoming Host header before passing the request to Dispatcher.

Therefore, your vhost only needs a generic “accept everything” configuration:

ServerName  "publish"
ServerAlias "*"
DocumentRoot "${DOCROOT}"

Explanation (short & clear):

  • ServerName “publish” → internal identifier only — never matched against real domains.
  • ServerAlias “*” → accept all domains because Cloud Manager/CDN has already normalized the traffic.
  • DocumentRoot “${DOCROOT}” → must point to your standard publish DocumentRoot.

Supported automatically (no extra vhosts needed):

  • portaldev.flagtick.com
  • portalstg.flagtick.com
  • portal.flagtick.com

Use this pattern when Cloud Manager manages your domain.
It keeps Dispatcher configuration simple and avoids Phase-1 validation errors.

Model 2: Domain Mapped Directly to Dispatcher (DNS → Dispatcher)

In this model, Dispatcher — not Cloud Manager — receives the real domain, because DNS resolves the domain straight to the Dispatcher’s IP.

This means the Dispatcher must match the domain explicitly in its vhost:

ServerName portaldev.flagtick.com
ServerAlias portaldev.flagtick.local
ServerAlias flagtick.cloud

DocumentRoot "${DOCROOT}"

Why explicit domain matching is required

  • Dispatcher receives the raw Host header
  • No CDN or Cloud Manager to normalize domains
  • Dispatcher must decide which vhost/farm to use based on the exact hostname
  • Any host not listed here → falls back to default vhost or returns 404

This ensures correct:

  • environment separation (DEV/STG/PROD)
  • farm selection
  • filter rules
  • caching behavior

Using table below which highlights the operational differences between Cloud Manager–mapped and Dispatcher-mapped domains:

FeatureCloud Manager (AEMaaCS)Dispatcher-mapped (On-prem/AMS/Local)
Who maps the domain?Adobe CDNDispatcher
vhost patternServerAlias “*”ServerName real-domain.com
Dispatcher matches Host header?NoYes
CDN present?AlwaysNot required
Used forDEV/STAGE/PROD AEM CloudOn-prem/AMS/Local Dispatcher/AEM Cloud

    Important: Adobe CDN ≠ Dispatcher.
    CDN handles global edge routing, TLS, domain mapping, WAF, and caching.
    Dispatcher handles Apache routing, filtering, and AEM publish caching.

    Understanding Validator Phases and How to Resolve Errors

    Before running the validator, first verify which domain-mapping model your project uses
    (see Domain Mapping Models: Cloud Manager vs Dispatcher).
    This determines which vhost pattern is correct:

    • Cloud Manager/CDN model → use wildcard vhost (ServerAlias "*")
    • DNS → Dispatcher model → use explicit domain vhosts (ServerName real-domain.com)

    This section now focuses purely on validation behavior, common failures, and how to fix them quickly.

    Quick Pre-Validation Checklist — run these steps before validate.sh to avoid common Phase-1 failures.

    cd dispatcher
    
    # vhost symlinks must exist
    ls -la src/conf.d/enabled_vhosts
    
    # farm symlinks must exist
    ls -la src/conf.dispatcher.d/enabled_farms
    
    # check Apache syntax before Docker runs it
    httpd -t   # or apachectl -t
    
    # confirm AEM Publish is reachable
    curl -I http://host.docker.internal:4503 || curl -I http://localhost:4503

    Example: Cloud Manager–mapped vhost (Case 1)

    (You can name the file flagtick.vhost, default.vhost, wknd.vhost, etc.—the filename doesn’t matter in Case 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>

    Example: Dispatcher-mapped vhost (Case 2 — DNS → Dispatcher)

    ## 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>

    Enable and validate the vhost

    # 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

    Enable both vhost and 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

    Add a hosts entry (local testing):

    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 
    
    or 
    ./bin/validator full -d out src
    ./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/validator full -d out src
    %./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"

    Dispatcher-Driven Request Routing

    Think of the Dispatcher as the gateway in front of AEM Publish. Every request passes through it first. From here, it can serve cached content, rewrite URLs internally, send users to a new URL with a redirect, or pass the request through to another backend.
    In this section, we will look at which behavior to use, where to configure it, and how to test each one.

    Reverse Proxy Routing (using RewriteRule + [P])

    Redirect Handling (301/302)

    Internal URL Rewrites

    AEM vs Backend Origin Decision Logic

    Allowlist vs Proxy Routes

    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