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#

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

One-time setup#

  1. Buy an ESA plan in the Alibaba Cloud console. One plan serves one root domain (one ESA site). Luma never buys plans.
  2. 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": "*"
    }
  ]
}
  1. 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#

  1. 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 _esaauth TXT 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 its cache paths. It then writes the _acme-challenge CNAME 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.
  2. Sync DNS points the domain at the ESA CNAME, which already serves HTTPS. Switching between A and CNAME replaces the record in place.
  3. The Nomad rollout runs as before. redirectFrom hosts move to the edge right after it, once their redirect routes are live; a DNS failure for them is a warning. Hosts dropped from redirectFrom get their DNS record removed.
  4. 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.
  5. Purge CDN refreshes the cache after the new version is live: the whole hostname by default, or the purge paths. A refresh failure is a warning, never a failed deploy; run luma cdn purge to 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:

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:

/_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

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

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

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.