Skip to content

Setup

This guide covers local development, dashboard development, production-oriented Compose deployment, validation, and GitHub Pages docs rendering.

Prerequisites

AreaRequirement
OSLinux or macOS with Docker support. Windows works best through WSL2.
ContainersDocker Engine and Docker Compose v2.
BackendPHP 8.3 with pdo_pgsql for host-side lint/tests.
TestsPython 3.12 and pytest.
FrontendNode.js 22 and npm.
Optional docs renderNode.js 22 and npm for VitePress local preview/build.

Local Stack

bash
cp .env.example .env
docker compose up -d --build
docker compose ps

Default URLs:

ServiceURL
Core APIhttp://localhost:8080
Edge proxyhttp://localhost:8081
Edge TLS proxyhttps://localhost:8443
Dashboardhttp://localhost:8082
PostgreSQLlocalhost:5432
PowerDNS API, loopback onlyhttp://localhost:8089
PowerDNS authoritative DNS127.0.0.1:5353
Poweradmin, loopback onlyhttp://localhost:8084

Health checks:

bash
curl -fsS http://localhost:8080/health
curl -fsS http://localhost:8080/cdn-health
curl -fsS http://localhost:8080/ready
curl -fsS http://localhost:8081/health

Dashboard Login

The local .env.example enables admin bootstrap:

text
CDNLITE_BOOTSTRAP_ADMIN_USER=1
CDNLITE_BOOTSTRAP_ADMIN_USERNAME=admin
CDNLITE_BOOTSTRAP_ADMIN_PASSWORD=admin

Create a deliberate admin account when bootstrap is disabled:

bash
docker compose exec core php artisan cdn:admin:create \
  --username=admin \
  --password='replace-with-a-long-password'

Admin maintenance commands:

bash
docker compose exec core php artisan cdn:admin:list --format=table
docker compose exec core php artisan cdn:admin:password --username=admin --password='replace-with-a-new-long-password'
docker compose exec core php artisan cdn:admin:delete --username=old-admin

Backend Setup

The core image runs PHP from core/public_index.php and CLI commands from core/artisan. Database upgrades run through ordered PostgreSQL migrations in core/database/migrations/. core/database/schema.sql is a development snapshot for inspection and fresh local rebuilds, not the production upgrade path. Core containers run migrations at startup when CDNLITE_AUTO_MIGRATE is true (the local default). Set it to false for controlled production rollouts and run migrations manually after taking a backup.

Useful commands:

bash
docker compose exec core php artisan cdn:dns:reconcile
docker compose exec core php artisan cdn:domain:list
docker compose exec core php artisan cdn:db:migrate --dry-run
docker compose exec core php artisan cdn:db:migrate
docker compose exec core php artisan cdn:db:status
docker compose exec core php artisan cdn:readiness:check
docker compose exec core php artisan cdn:edge:list
docker compose exec core php artisan cdn:usage:prune --dry-run

Migrations are applied automatically before the Core web process starts when auto-migration is enabled. Durable DNS state is reconciled after mutations and by the dns-reconciler service every CDNLITE_SYNC_INTERVAL_SECONDS seconds (default 30).

If an existing development volume logs a missing powerdns_zone_serials table or Activity shows request country but an unknown client IP, run docker compose exec core php artisan cdn:db:migrate. That applies the runtime schema reconciliation migration that restores durable PowerDNS SOA serial state and the Activity client_ip diagnostics column.

The nameserver-scheduler runs php artisan cdn:domains:verify-all every CDNLITE_NAMESERVER_CHECK_INTERVAL_SECONDS seconds (default 86400). A verified domain activates automatically and queues managed ACME DNS-01 SSL for the apex hostname and wildcard hostname. If its authoritative nameservers later move away, CDNLite marks it pending and withdraws its DNS records and edge config. The PowerDNS zone itself is created as soon as the domain is saved and remains present with only platform NS and SOA records while nameserver verification is pending or lost. User DNS records remain stored in Core and are republished after nameserver verification succeeds again. Domain mutations return after saving Core state and the scheduled reconciler converges PowerDNS; run php artisan cdn:dns:reconcile when you need to force immediate convergence. The dashboard domain detail page also has Refresh nameservers now, which does not wait for the scheduler and shows expected, observed, matched, missing, and resolver error details. Operators logged in with an admin session can use Force verify as admin with a reason; the override is audited, activates the domain, invalidates edge config, and reconciles DNS. If you update platform.nameservers after domains already exist, use Re-seed expected NS on each affected domain or call POST /api/v1/domains/{domainId}/nameservers/reseed-expected with an admin session token. This updates expected delegation rows from current settings, preserves already observed overlaps, invalidates edge config, reconciles DNS, and writes an audit event without deleting the domain.

Fresh local reset:

bash
docker compose down -v
docker compose up -d --build

For upgrade and backup details, see Database Migrations.

The root Compose topology does not assign registry tags to locally built CDNLite services. Core, schedulers, Edge, and the Edge agent are built from the currently checked-out branch and use Compose project-scoped image names.

Frontend Setup

For dashboard-only development:

bash
cd dash
npm ci
npm run dev

Open http://localhost:5173. The dashboard reads Vite build-time variables such as VITE_CDNLITE_CORE_URL and VITE_CDNLITE_EDGE_URL; use browser-reachable URLs, not internal Compose names.

Production dashboard image builds happen through the root docker-compose.yml:

bash
docker compose build dashboard

Environment Variables

Core settings:

VariablePurpose
DB_HOST, DB_PORT, DB_DATABASE, DB_USERNAME, DB_PASSWORDPostgreSQL connection.
APP_ENV, APP_DEBUG, APP_LOG_ENABLED, APP_LOG_LEVELRuntime and logging behavior.
CDNLITE_API_TOKENOptional bearer token for non-edge /api/v1/* endpoints.
CDNLITE_CORS_ALLOWED_ORIGINSBrowser origins allowed to call the API.
CDNLITE_SSL_SECRET_KEYSecret used for stored SSL material handling.
CDNLITE_ORIGIN_SHIELD_SECRETDefault origin shield secret.
CDNLITE_CONFIG_SNAPSHOT_KEEP_LASTCount of latest published config snapshots to retain in addition to the active version; default 2.
CDNLITE_CONFIG_SNAPSHOT_PRUNE_BATCH_SIZEBounded delete batch used after publish and by cdn:config-snapshots:prune; default 5000.
CDNLITE_CONFIG_SNAPSHOT_HISTORY_ENABLEDEnables legacy snapshot payload, diff, and rollback endpoints for development; default false.
CDNLITE_ANALYTICS_RETENTION_DAYSDetailed edge request/activity retention window for cdn:usage:prune; default 30.
CDNLITE_SECURITY_EVENT_RETENTION_DAYSHigh-volume WAF, rate-limit, bot, and Geo security-event retention for cdn:usage:prune --all; default 90.
CDNLITE_DNS_EVENT_RETENTION_DAYSSuccessful DNS sync event retention for cdn:usage:prune --all; default 30. Failed DNS sync events are retained for troubleshooting.
CDNLITE_SSL_JOB_RETENTION_DAYSTerminal SSL job retention for cdn:usage:prune --all; default 180. Active jobs are never pruned.
CDNLITE_INGEST_KEY_RETENTION_DAYSEdge ingest idempotency-key retention for cdn:usage:prune --all; default 7.
CDNLITE_RETENTION_PRUNE_ENABLEDEnables the retention-scheduler service when set to true; default false for upgrade safety.
CDNLITE_RETENTION_INTERVAL_SECONDS, CDNLITE_RETENTION_BATCH_SIZERetention scheduler interval and bounded delete batch size; defaults 86400 and 5000.
CDNLITE_STORE_FULL_CLIENT_IPStore full client IPs in security-event audit details only when explicitly true; default stores a SHA-256 hash.
CDNLITE_ACME_*ACME directory, contact email, DNS propagation delay, optional public DNS TXT precheck, and polling for automatic apex and wildcard certificates.
CDNLITE_SSL_JOB_STALE_RETRY_SECONDSAge after which an in-progress SSL job can be reclaimed by the scheduler and retried.
CDNLITE_SSL_SCHEDULER_INTERVAL_SECONDSSeconds between SSL scheduler loops for queued issuance and renewals; default 30.
CDNLITE_BOOTSTRAP_ADMIN_*Local/admin bootstrap behavior.
CDNLITE_BOOTSTRAP_EDGE_*, CDNLITE_BOOTSTRAP_EDGE_EXTRA_TOKENS, EDGE_ID, EDGE_TOKEN, EDGE_2_*Local edge token bootstrap and the bundled two-edge test topology.
CDNLITE_EDGE_*, CDNLITE_GEO_*, CDNLITE_NS*Edge DNS, health, anycast, and Geo DNS defaults.
PDNS_REPLICATION_PASSWORDPassword for the TLS-protected PowerDNS PostgreSQL streaming-replication role.
CDNLITE_CDN_ZONEAuthoritative zone containing stable site targets and the shared proxy record.
CDNLITE_CDN_PROXY_HOSTShared proxy hostname, and must be inside CDNLITE_CDN_ZONE.

The recommendation engine has no dedicated environment variables. Run php artisan cdn:recommendations:generate manually or from your scheduler to refresh proactive security, performance, reliability, and SSL suggestions from recent telemetry.

Static proxy anycast IPs are configured in the admin dashboard under Settings -> Edge DNS / Anycast as anycast_ipv4 and anycast_ipv6. These are database-backed settings, not environment variables. Enter one or more IPs per family separated by commas, spaces, or new lines. When set, the shared proxy host publishes plain A/AAAA records containing all configured addresses for the configured families and bypasses DNSGeo Lua, country routing, and continent routing for those families.

The DNS initializer creates only the PowerDNS/Poweradmin schemas and service roles. It does not create sample zones. GeoIP bootstrap uses only the reserved geoip-bootstrap.invalid backend-initialization zone, so Core remains the owner of all routable authoritative DNS data. See DNSGeo and PowerDNS.

Edge and agent settings:

VariablePurpose
CORE_URLAgent target core URL, normally http://core:8080 in Compose.
EDGE_CONFIG_DIRHost directory mounted into /var/lib/cdnlite.
EDGE_CONFIG_PATHRuntime config path, default /var/lib/cdnlite/config.json.
EDGE_CONFIG_MAX_STALE_SECONDSMaximum acceptable config staleness before edge readiness fails.
METRIC_PATHMetrics queue file for the agent.
SECURITY_EVENT_PATHSecurity event queue file for the agent.
CDNLITE_CACHE_DEFAULT_TTLDefault OpenResty cache TTL.
CDNLITE_EDGE_DEBUG_HEADERSSet to 1 to expose sanitized cache key and bypass reason headers during diagnosis.
CDNLITE_EDGE_WORKER_PROCESSES, CDNLITE_EDGE_WORKER_CONNECTIONSOpenResty worker capacity. Defaults are auto and 4096.
CDNLITE_EDGE_LIMITS_DICT_SIZE, CDNLITE_EDGE_REQUEST_CONTEXT_DICT_SIZE, CDNLITE_EDGE_METRIC_QUEUE_DICT_SIZE, CDNLITE_EDGE_SECURITY_EVENT_QUEUE_DICT_SIZEShared memory budgets for rate limits, request context, metrics, and security-event queues.
CDNLITE_EDGE_WAITING_ROOM_DICT_SIZE, CDNLITE_EDGE_WAITING_ROOM_SECRETShared-memory budget and signing secret for local waiting-room queue tickets and admission cookies. Initial queues are local to each edge node and are not globally fair across nodes.
CDNLITE_EDGE_CONFIG_MAX_BYTES, CDNLITE_EDGE_CONFIG_REFRESH_SECONDSMaximum accepted edge snapshot size and worker config refresh interval.
CDNLITE_EDGE_TELEMETRY_BATCH_SIZE, CDNLITE_EDGE_TELEMETRY_FLUSH_INTERVAL_SECONDS, CDNLITE_EDGE_TELEMETRY_QUEUE_MAX_ITEMS, CDNLITE_EDGE_TELEMETRY_QUEUE_MAX_BYTESBounded edge telemetry queue and flush controls. Drops are counted and visible on /ready.
CDNLITE_EDGE_RESOLVER, CDNLITE_EDGE_CLIENT_*, CDNLITE_EDGE_PROXY_*DNS resolver, header/body buffer, request body, and upstream timeout tuning for OpenResty.
CDNLITE_EDGE_LOG_FORMATEdge access log format selector; json is the default and writes to stdout.
CDNLITE_EDGE_LOG_LEVELEdge diagnostic log level: debug, info, warn, or error; default info.
CDNLITE_EDGE_LOG_REQUEST_BODYReserved for future strict-redaction body logging; keep false.
CDNLITE_EDGE_DEBUG_HEADERSReserved for future debug header logging; keep false unless a runbook explicitly enables it.
CDNLITE_EDGE_MMDB_FILEGeoIP MMDB used by the edge for country WAF/origin decisions; default /var/lib/cdnlite/mmdb/GeoLite2-City.mmdb.

Waiting-room shared memory stores rolling counters, queue population, active origin counts, and short-lived ticket/admission state. Increase CDNLITE_EDGE_WAITING_ROOM_DICT_SIZE only when /ready, edge logs, or queue status indicate sustained local state pressure. Use the dashboard policy fields to tune traffic behavior first: lower admission_rate_per_minute protects origins more, lower queue_limit bounds memory more tightly, and higher polling jitter spreads queue status checks. | CDNLITE_EDGE_CLEARANCE_SECRET | Shared edge secret for signed challenge and clearance cookies. Set the same strong value on every edge; rotation invalidates existing clearances. | | CDNLITE_EDGE_CHALLENGE_DIFFICULTY | Default self-hosted edge challenge difficulty, from 1 to 6; default 3. WAF and rate-limit challenge rules can override this per path or pattern with challenge_difficulty. Level 1 performs a lightweight browser check without proof-of-work. Levels 2 through 6 require increasing SHA-256 proof-of-work before origin routing. | | EDGE_AGENT_IDLE | CI flag to keep agent idle while scripts drive flow manually. |

Recommended starting values:

VariableDevelopmentProduction starting point
CDNLITE_EDGE_WORKER_PROCESSES1auto
CDNLITE_EDGE_WORKER_CONNECTIONS10248192
CDNLITE_EDGE_LIMITS_DICT_SIZE10m50m
CDNLITE_EDGE_REQUEST_CONTEXT_DICT_SIZE5m20m
CDNLITE_EDGE_METRIC_QUEUE_DICT_SIZE5m32m
CDNLITE_EDGE_SECURITY_EVENT_QUEUE_DICT_SIZE5m32m
CDNLITE_EDGE_CONFIG_MAX_BYTES10485765242880
CDNLITE_EDGE_CONFIG_REFRESH_SECONDS11
CDNLITE_EDGE_TELEMETRY_BATCH_SIZE50500
CDNLITE_EDGE_TELEMETRY_FLUSH_INTERVAL_SECONDS11
CDNLITE_EDGE_TELEMETRY_QUEUE_MAX_ITEMS5000100000
CDNLITE_EDGE_TELEMETRY_QUEUE_MAX_BYTES52428816777216
CDNLITE_EDGE_CLIENT_HEADER_BUFFER_SIZE4k8k
CDNLITE_EDGE_LARGE_CLIENT_HEADER_BUFFERS4 8k8 16k
CDNLITE_EDGE_CLIENT_BODY_BUFFER_SIZE64k256k
CDNLITE_EDGE_CLIENT_MAX_BODY_SIZE10m100m
CDNLITE_EDGE_PROXY_CONNECT_TIMEOUT3s5s
CDNLITE_EDGE_PROXY_READ_TIMEOUT30s60s
CDNLITE_EDGE_PROXY_SEND_TIMEOUT30s60s

The development profile keeps memory use small and deterministic for local tests. The production profile is a safe starting point for a single edge host; increase queue sizes or shared dictionaries only after /ready shows drops, corruptions, or sustained high queue depth under real traffic.

The normal root Compose stack mounts the MMDB updater volume into the edge container read-only. Standalone edge deployments run their own edge-mmdb-updater sidecar, using the same downloader as DNSGeo, and mount its database into /var/lib/cdnlite/mmdb. Country-based WAF rules and country origin selection use X-CDNLITE-Country or CF-IPCountry when a trusted upstream sets one, otherwise the edge resolves remote_addr through the mounted MMDB.

OpenResty writes edge access logs to stdout and diagnostics to stderr. Runtime metrics and security events are first stored in bounded shared-memory queues and then flushed in batches to the existing agent files, so collector or disk outages do not create unbounded memory growth. Live operations can use:

bash
docker compose logs -f edge
curl -s http://localhost:8081/ready
docker compose exec edge tail -f /var/lib/cdnlite/metrics.ndjson

Access logs include request id, host, method, path, status, selected origin id, upstream status/time, cache status, and byte counts. Query parameters with names such as token, key, secret, password, auth, or signature are redacted in structured diagnostics and metrics.

Detailed request/activity rows are retained until an operator prunes them. Run a dry run first, then prune rows older than CDNLITE_ANALYTICS_RETENTION_DAYS or an explicit --days value:

bash
docker compose exec core php artisan cdn:usage:prune --dry-run
docker compose exec core php artisan cdn:usage:prune --days=30

Config snapshots are a published edge cache, not the source of truth. The core keeps the active snapshot plus the latest two snapshots by default and prunes after successful publishes. Operators with a large historical table can prune in batches without deleting the active snapshot:

bash
docker compose exec core php artisan cdn:config-snapshots:prune --keep=2 --batch=5000 --dry-run
docker compose exec core php artisan cdn:config-snapshots:prune --keep=2 --batch=5000

Use the wider retention pass for high-volume operational rows after reviewing the dry run. It prunes raw request rows, high-volume security events, successful DNS sync events, terminal SSL jobs, expired edge nonces, and old ingest idempotency keys in bounded batches:

bash
docker compose exec core php artisan cdn:usage:prune --all --dry-run
docker compose exec core php artisan cdn:usage:prune --all

The retention-scheduler service ships disabled by default. Set CDNLITE_RETENTION_PRUNE_ENABLED=true only after confirming the dry-run counts match your operational policy. Longer-term rollups and dashboard summaries should use aggregate views and exports, not indefinite raw request retention. Security-event ingest hashes client IPs by default; set CDNLITE_STORE_FULL_CLIENT_IP=true only when your privacy policy and retention process explicitly allow it.

Dashboard variables:

VariablePurpose
VITE_CDNLITE_CORE_URLBrowser URL for core.
VITE_CDNLITE_EDGE_URLBrowser URL for edge.
VITE_CDNLITE_APP_NAMEDashboard name.
VITE_CDNLITE_API_TOKENOptional local/private API token compiled into assets.
VITE_ENABLE_EDGE_DEV_TOOLSEnables signed edge request tools.
VITE_ENABLE_USAGE_SIMULATOREnables usage simulation tools.
VITE_ENABLE_SSL_TOOLSShows SSL tooling.
VITE_ENABLE_SECURITY_EVENT_VIEWERShows security event screens.
VITE_ENABLE_LOG_VIEWERShows event/log viewer.

PowerDNS Operations

PowerDNS writes are verified by reading the affected zone back after each successful PATCH. Temporary connection failures, HTTP 429 responses, and HTTP 5xx responses are retried with exponential backoff. Configure this behavior with CDNLITE_POWERDNS_VERIFY_AFTER_WRITE, CDNLITE_POWERDNS_RETRIES, CDNLITE_POWERDNS_RETRY_SLEEP_MS, and CDNLITE_POWERDNS_TIMEOUT_SECONDS.

Validate DNS publishing against the bundled stack:

bash
docker compose up -d --build
curl -fsS -H "X-API-Key: $PDNS_API_KEY" \
  http://localhost:8089/api/v1/servers/localhost
dig @127.0.0.1 -p "${PDNS_DNS_HOST_PORT:-5353}" example.net SOA

Tests mutate only the local PostgreSQL-backed PowerDNS instance.

Core stores every zone write attempt in dns_sync_events and keeps the latest per-zone result in dns_sync_state.

Managed zones use platform SOA authority settings: CDNLITE_DNS_PRIMARY_NS=ns1.faratar.ir., CDNLITE_DNS_HOSTMASTER=hostmaster.faratar.ir., CDNLITE_DNS_SOA_REFRESH=7200, CDNLITE_DNS_SOA_RETRY=3600, CDNLITE_DNS_SOA_EXPIRE=1209600, CDNLITE_DNS_SOA_MINIMUM=60, and CDNLITE_DNS_SOA_TTL=60 by default. The sync keeps exactly one apex SOA and uses a stored monotonic serial that changes only when zone content changes.

bash
docker compose exec core php artisan cdn:powerdns:doctor
docker compose exec core php artisan cdn:powerdns:dry-run
docker compose exec core php artisan cdn:powerdns:force-sync
curl -fsS http://localhost:8080/cdn-health

doctor reports SOA validity for each zone, dry-run builds the current DNS projection and SOA repairs without writing PowerDNS, and force-sync republishes customer and edge records, repairs SOA, and verifies the result.

Testing

Host-side checks:

bash
docker compose config --quiet
find core -name '*.php' -print0 | xargs -0 -n1 php -l
pytest -q core/tests
cd dash && npm ci && npm run typecheck && npm test && npm run build

Shell syntax checks:

bash
sh -n edge/agent/register.sh
sh -n edge/agent/heartbeat.sh
sh -n edge/agent/pull_config.sh
sh -n edge/agent/push_metrics.sh
sh -n edge/agent/run.sh
bash -n ci/smoke.sh
bash -n ci/e2e.sh
bash -n ci/dns_e2e.sh

Smoke and e2e:

bash
docker compose up -d --build --wait
./ci/smoke.sh

docker compose up -d --build
EDGE_AGENT_IDLE=1 CDNLITE_CACHE_DEFAULT_TTL=1s ./ci/e2e.sh
CDNLITE_EDGE_HEALTH_MODE=static ./ci/dns_e2e.sh

Production DNS scale qualification is destructive and must run on a disposable fresh-install stack:

bash
./ci/stress-dns.sh

Its defaults are the full target: 10,000 domains, 1,000 records per domain, 10 edge nodes across three regions, ten health flaps, and a 10-second maximum for the edge-only reconciliation. It resets both Core and PowerDNS data, runs a full verified reconciliation, changes one edge IP, exercises concurrent user record changes during edge health flaps, and writes JSON/Markdown reports to ci/reports/.

For a mechanics-only local check, reduce the dataset explicitly:

bash
STRESS_DOMAINS=10 STRESS_RECORDS_PER_DOMAIN=20 \
STRESS_EDGE_NODES=6 STRESS_FLAP_ITERATIONS=2 ./ci/stress-dns.sh

Only the default 10,000 x 1,000 run qualifies the full load model. GitHub Actions exposes the same default run through the manual run_dns_stress workflow input. See DNS Stress Testing for the complete destructive-run procedure, configuration variables, assertions, reports, and recovery steps.

The DNS acceptance flow verifies Core-created zones, raw CNAME/LUA records, apex LUA answers with dig, edge health reconciliation, stale record deletion, persisted failure state, and recovery. Static Lua answers are used only for deterministic documentation-range CI fixtures.

Core and the DNS reconciler receive the same CDN zone, proxy hostname, and TTL settings. By default, shared edge-pool Lua records use edge IP, country, and continent data from Core and fall back to the first eligible edge IP. Static proxy anycast settings replace those Lua records with plain A/AAAA records for the configured families. Recreate both services after changing CDN DNS values.

Dashboard validation uses typechecking, unit tests, a production build, and manual operator QA. Browser automation is intentionally outside the release gate.

Deployment

For production topology selection, immutable image tags, security, backup, upgrade, rollback, and release qualification, use the Production Deployment guide.

  1. Copy .env.example to .env and replace every local secret.
  2. Set CDNLITE_BOOTSTRAP_ADMIN_USER=0 after creating durable admin credentials.
  3. Set CDNLITE_BOOTSTRAP_EDGE_TOKEN=0 after registering production edge tokens.
  4. Set CDNLITE_API_TOKEN for control-plane API protection.
  5. Set dashboard VITE_* URLs to public browser-reachable hosts and rebuild the dashboard image.
  6. Put core and dashboard behind TLS and production authentication at the platform or reverse proxy layer.
  7. Run docker compose up -d --build.
  8. Run readiness, smoke, DNS, edge, and SSL checks before sending traffic.

GitHub Pages Rendering

The docs use VitePress. Source files live under docs/, the VitePress config lives at docs/.vitepress/config.mts, and the static build is emitted to docs/.vitepress/dist.

The API contract is published at https://vaheed.github.io/CDNLite/api/openapi.yaml. The source file is docs/public/api/openapi.yaml; keep it updated with route additions and request/response shape changes so developers can generate clients or load the spec into API tools.

Local preview:

bash
cd docs
npm ci
npm run docs:dev

Production build:

bash
cd docs
npm ci
npm run docs:build
npm run docs:preview

After a build, confirm the OpenAPI file is included in the static output:

bash
test -f docs/.vitepress/dist/api/openapi.yaml

GitHub Pages deployment is handled by .github/workflows/docs.yml. The workflow installs docs dependencies, builds VitePress, uploads docs/.vitepress/dist, and deploys through GitHub Pages Actions.

The default VitePress base path is /CDNLite/. The Pages workflow overrides it with the repository name:

text
VITEPRESS_BASE=/${{ github.event.repository.name }}/

For a custom domain or a different Pages path, set VITEPRESS_BASE before running the build.

If dependencies are not installed, validate links and Markdown file presence with:

bash
find docs -name '*.md' -print
rg -n '\\[[^]]+\\]\\(([^)#][^)]+\\.md)\\)' docs README.md

CDNLITE_POWERADMIN_URL controls the operator link shown by the DNS Operations page and defaults to http://localhost:9191. It does not change the Poweradmin listener or expose it publicly.

Released under the MIT License.