CDN#
Luma can put a service behind Alibaba Cloud ESA (Edge Security Acceleration). One line in the manifest enables it; Luma then manages the ESA record and its rules for the life of the service. Services without cdn are unchanged and never touch Alibaba Cloud.
User -> Alibaba Cloud ESA node -> (cache miss) -> CN Traefik -> service
When to use it#
- Public HTTP services on
cn-edge,external-edgeortailscale-relay. Other exposures are rejected. - Domains with an ICP filing use
scope: global: one CNAME serves users in mainland China and abroad.scope: overseasneeds no filing. - DNS stays in Cloudflare. Luma writes a DNS-only CNAME to ESA; do not turn on the Cloudflare proxy for these names.
Root domains (apex)#
Serve the CDN on www and let the root domain redirect to it:
name: web
image: ghcr.io/example/web:1.4.0
region: cn
exposure: cn-edge
domain: www.example.com
redirectFrom:
hosts: [example.com]
except: ["/api/*"] # optional: served directly on example.com, never redirected
port: 3000
cdn: esa
- A root domain cannot be a CNAME, so Cloudflare must "flatten" it: Cloudflare resolves ESA itself and returns IP addresses. ESA then schedules every visitor by Cloudflare's location instead of the visitor's. Measured on itool.tech: a visitor in mainland China was served from US nodes and a page took 10 seconds. Luma therefore refuses
cdnon the zone apex and explains this fix. www.example.comis a plain CNAME, so each visitor's own DNS resolver asks ESA and gets a nearby node.example.comstays a direct A record to the edge. GET and HEAD get a permanent redirect tohttps://www.example.comwith the path and query kept; browsers cache it. Other methods reach the application unchanged, so payment callbacks and webhooks sent to the old address keep working.exceptlists paths served directly on the old host for every method. Use it for APIs other clients call with anAuthorizationheader: browsers and most HTTP libraries drop that header when a redirect changes host, so the request would arrive unauthenticated. The short list formredirectFrom: [example.com]redirects every GET and HEAD.- Point the application's canonical URL, sitemap and OAuth callbacks at
www. Passkeys keep working when the WebAuthn RP ID stays the root domain.
One-time setup#
- Buy an ESA plan in the Alibaba Cloud console. One plan serves one root domain (one ESA site). Luma never buys plans.
- Create a RAM user with programmatic access only and attach this policy, which grants ESA and nothing else:
{
"Version": "1",
"Statement": [
{
"Effect": "Allow",
"Action": "esa:*",
"Resource": "*"
}
]
}
- Store the RAM user's AccessKey in Luma Control and check the setup:
luma secret set LUMA_ALIBABA_CLOUD_ACCESS_KEY_ID
luma secret set LUMA_ALIBABA_CLOUD_ACCESS_KEY_SECRET
luma cdn check
The variable names carry a LUMA_ prefix so they never clash with Alibaba Cloud keys your applications use. luma cdn check without a domain verifies the key, the ESA plan, the Cloudflare zone and the ESA site.
Enable it for a service#
Add cdn: esa and deploy as usual:
name: web
image: ghcr.io/example/web:1.4.0
region: cn
exposure: cn-edge
domain: www.example.com
port: 3000
cdn: esa
All fields:
cdn:
provider: esa # only esa today
scope: global # global (default) | domestic | overseas
purge: ["/", "/sitemap.xml"] # omit: refresh the whole hostname; []: no refresh; "/blog/*": a directory
cache: # paths that are the same for every visitor
- path: /blog/*
ttl: 300
- path: /opengraph-image*
ttl: 86400
List under cache only paths whose response never depends on who is signed in. A response the application marks private or no-store is still not cached.
What a deploy does#
- Prepare CDN, before DNS changes. The first time for a Luma zone, Luma creates the ESA site for the zone with CNAME access, proves ownership with the
_esaauthTXT record in Cloudflare and applies the site-wide settings below. For each service it creates a proxied ESA record pointing at the edge, updates it when the edge address changed, and reconciles the rules for itscachepaths. It then writes the_acme-challengeCNAME that delegates certificate validation to ESA and waits, up to 15 minutes, until ESA has issued a free certificate that covers the domain and its edge completes a TLS handshake for it. A failure here, including a certificate that is still pending, stops the deploy before DNS changes; deploy again later. ESA renews the certificate through the same delegation. - Sync DNS points the domain at the ESA CNAME, which already serves HTTPS. Switching between A and CNAME replaces the record in place.
- The Nomad rollout runs as before.
redirectFromhosts move to the edge right after it, once their redirect routes are live; a DNS failure for them is a warning. Hosts dropped fromredirectFromget their DNS record removed. - The public route probe connects to the edge directly with the domain as SNI, so a CDN error cannot trigger a recovery of a healthy application.
- Purge CDN refreshes the cache after the new version is live: the whole hostname by default, or the
purgepaths. A refresh failure is a warning, never a failed deploy; runluma cdn purgeto retry. A newly created record is not refreshed because it has no cache yet.
Site-wide settings, shared by every Luma domain on the ESA site:
- Origin: always fetch over HTTPS with the request's
Host, and verify Traefik's certificate. ESA redirects plain HTTP to HTTPS at the edge. - Cache: one site-wide rule makes every path eligible for caching and honours the application's cache headers, HTML included; a response without cache headers is not cached. ESA's global cache setting alone only covers static file extensions, so HTML would stay uncached.
- Gzip, Brotli and Zstd compression, HTTP/2 and HTTP/3 to visitors, HTTP/2 to the origin, WebSocket, and the
ali-real-client-ipheader with the real client address.
Each cache path gets one rule for its domain: honour the application's headers, and without them cache for ttl seconds. Path rules run after the site-wide rule, so their TTL applies, and cache paths cannot overlap. Cache rules count against a per-site quota shared by every application on the root domain: 5 on Free, 10 on Basic, 50 on Standard. The site-wide rule takes one. A deploy that would exceed it fails at Prepare CDN with that explanation.
ESA defaults cover the rest: TLS 1.0 off, basic DDoS protection and the managed WAF rules. Luma leaves paid options such as smart routing off.
Cache as much as possible#
Without its own configuration ESA caches only static files: HTML and paths without an extension are never cached, whatever the application returns. Luma's site-wide cache rule makes the application's headers count on every path, so caching more comes down to two levers:
- Return cache headers from the application:
/_next/static/*, /assets/*.<hash>.js Cache-Control: public, max-age=31536000, immutable
HTML pages Cache-Control: public, max-age=0, s-maxage=60
API, signed-in pages Cache-Control: private, no-store
- Or list the paths that are the same for every visitor under
cdn.cache, without changing the application.
Audit a domain to see where you stand. The check is read-only: it fetches the home page, the purge and cache paths, a few assets referenced by the home page, /sitemap.xml and /robots.txt, twice each, anonymously:
luma cdn check www.example.com
luma cdn check www.example.com --path /pricing --format json
- DANGER: a cacheable response that sets a cookie, or an account-like path marked public. Fix these first; the command exits non-zero. The
acw_tc,cdn_sec_tcandacw_sc__v2cookies are added by the ESA edge with a new value on every response and are never cached, so they are not counted. - IMPROVE: a page without cache headers, a page revalidated on every request, a static file without cache headers, or large text sent uncompressed.
- OK: cacheable (and served from the edge when the second request hit), or explicitly not cached.
Files with a content hash in the name never need a refresh: a new version has a new URL. The default refresh after a deploy covers the whole hostname and only fetches changed resources again. ESA limits hostname and directory refreshes to about 100 per day per plan; URL refreshes allow far more. Applications that deploy many times a day should list fixed URLs in purge.
Operations#
luma cdn status
luma cdn purge www.example.com
luma cdn purge www.example.com / /pricing
luma cdn remove www.example.com
statuslists each domain with its site, CNAME, last refresh and last error.purgerefreshes the whole hostname, or the given paths.removedeletes an ESA record and itscacherules once DNS no longer points at it.
To turn ESA off for a service, remove cdn from the manifest and deploy. DNS points at the edge again and the ESA record stays, so clients with a cached DNS answer keep working; delete it later with luma cdn remove. Removing the application deletes its ESA record and rules. The ESA site stays.