EasyPages is a self-hosted dashboard for Cloudflare Pages: your projects, their deployments, the ZIP you are about to publish and the domains pointing at them, in one screen. It ships as a single Docker container for your homelab or private network, and it always works against your Cloudflare account with your API token.

The problem

Publishing a static site is a solved problem right up to the last mile. Then it becomes: open the Cloudflare dashboard, find the project among everything else in an account built to operate infrastructure, upload the build, wait to see whether the deployment came up, go to a different tab for the custom domain, and to another one for the build command.

EasyPages collapses that loop into a panel you run yourself. One list of Pages projects, one button to deploy, one drop zone for the ZIP, and the domains right there next to the project they belong to.

What it does

  • Project list. Every Cloudflare Pages project with its status and its source, in a single view.
  • Create Direct Upload projects. Spin up a new project from the panel, without opening the Cloudflare dashboard.
  • Deployments. Trigger a deployment and browse the recent history with branch, commit and date.
  • ZIP uploads. Drop a .zip and publish it straight to the selected Direct Upload project.
  • Custom domains. Add and remove domains, with a confirmation before deleting one.
  • Build configuration. Edit the current build command and output directory from the settings tab.
  • English and Spanish across the whole interface, switched instantly from the header.

Setup is one variable

CF_API_TOKEN is the only required variable. The account ID is inferred from the token itself, the way Cloudflare's own SDK and Wrangler do, so there is nothing to look up — you only set CF_ACCOUNT_ID in the one case inference cannot resolve, a token that reaches several accounts, and the server log says so explicitly and lists the IDs to choose from.

That resolution is lazy, not at boot: a host that reboots without internet still starts and reports a clear error instead of refusing to come up.

There is no user or password in the .env either. The operator account is created in the browser the first time you open the panel, and no environment variable can create, replace or bypass it — AUTH_USER and AUTH_PASS were removed in the release that introduced the wizard. Lost the password? Stop the app, delete credentials.json, and the wizard comes back; the session secret is untouched.

Security and scope

EasyPages is a single-operator panel: one account, no roles, no multi-tenancy. Anyone who signs in can create, deploy and delete projects and domains with your Cloudflare token, so treat access to the panel as equivalent to access to that token.

  • The password is stored as a scrypt hash (parameters inside the hash, so they can be raised later without invalidating anything). Sessions are a signed, HttpOnly, SameSite=Lax cookie that lasts 24 hours and carries a username and a token version — no secrets.
  • Every credential change bumps that token version, which invalidates every cookie issued before it. That is how "sign out all other devices" works without server-side session storage.
  • CSRF is an opaque per-session token required on every unsafe method, and failed logins are rate limited on two buckets: the reported IP and the socket peer. The second one is not optional — X-Forwarded-For is sent by the client, so without it rotating a forged value would hand out a fresh quota on every request.
  • The Cloudflare token is never written to disk by EasyPages, never sent to the browser and never included in an error response.
  • Between the first start and finishing the wizard, anyone who can reach the port can claim the instance — the same trust-on-first-use trade-off Portainer and Home Assistant make. Complete it right away and do not publish the port before you have.
  • Behind a reverse proxy with TLS, set SESSION_COOKIE_SECURE=true. Exposed directly, set TRUST_PROXY=false so the rate limiter keys on the real socket rather than a header the client controls.

Stack

React 18 with Vite and Tailwind on the frontend; Node 24 with Express on the backend; one Docker container. No database: Cloudflare is the system of record for everything in the domain, and the only local state is the credential and the session-signing key, both 0600 in the data volume. Apache-2.0, image ghcr.io/kn990x/easypages.

CI runs lint, the build and both test suites (node:test on the backend, Vitest on the frontend), plus an image build and a cold-start smoke test with a single environment variable and a clean volume — status, wizard, session, phantom asset, credential file permissions and the container healthcheck.

easypages.app is a separate repository: a static, bilingual Astro site on Cloudflare Workers, with one Preact island — an interactive replica of the dashboard running on sample data — and no third-party requests at all.

Getting started

mkdir easypages && cd easypages
curl -fsSL -o docker-compose.yml https://raw.githubusercontent.com/KN990x/EasyPages/main/docker-compose.yml
curl -fsSL -o .env.example https://raw.githubusercontent.com/KN990x/EasyPages/main/.env.example
cp .env.example .env

# Fill in CF_API_TOKEN
docker compose up -d --pull always

Open http://your-server:8002 and pick a username and password in the wizard. The Compose file ships a pinned GHCR image, ./easypages-data:/data and a healthcheck on /api/health; the container runs as uid 1000, so if you created the directory as root, chown -R 1000:1000 ./easypages-data.

The API token needs exactly one permission: Account → Cloudflare Pages → Edit. Nothing else — the account ID is read from the token.

Full documentation — token permissions, optional variables, reverse proxies and the developer setup — lives at easypages.app and in the KN990x/EasyPages repository.