Deploying from Forgejo to Cloudflare Pages¶
Cloudflare Pages' built-in Git integration only connects to GitHub and GitLab — not a self-hosted Forgejo/Gitea instance. To deploy from Forgejo, a Forgejo Actions workflow builds the site and pushes it to Pages with Wrangler (Direct Upload).
The workflow lives at .forgejo/workflows/deploy.yml in your repo
and runs on every push to main.
graph LR
A[git push main] --> B[Forgejo Actions runner]
B --> C[zensical build --clean]
C --> D[wrangler pages deploy site]
D --> E[Cloudflare Pages]
No secrets in the repo
The API token and account ID are stored as Forgejo Actions secrets, never committed. The project name is a plain variable.
1. Register a Forgejo Actions runner¶
Forgejo Actions needs a runner (act_runner)
to execute jobs — the instance has none by default. On a host with Docker:
# Get a registration token from:
# Repo → Settings → Actions → Runners → Create new runner
# (or a site-wide token under Site Administration → Actions → Runners)
docker run -d --restart=always --name forgejo-runner \
-v /var/run/docker.sock:/var/run/docker.sock \
-v $PWD/runner-data:/data \
-e FORGEJO_INSTANCE_URL=https://forgejo.<YOUR_DOMAIN> \
-e FORGEJO_RUNNER_REGISTRATION_TOKEN=<registration-token> \
code.forgejo.org/forgejo/runner:latest
Check the runner registration steps
The official Forgejo Runner image does not document registering itself from FORGEJO_INSTANCE_URL /
FORGEJO_RUNNER_REGISTRATION_TOKEN environment variables. Current Forgejo docs register the runner by putting the
UUID and token from Create new runner into the runner's config file (server.connections), then start it with
forgejo-runner daemon --config runner-config.yml. See Runner installation (Docker)
and Runner registration. Pin a runner version tag
(e.g. data.forgejo.org/forgejo/runner:13) instead of latest. Mounting /var/run/docker.sock gives the runner
root-equivalent access to the host.
The runner registers with labels (e.g. ubuntu-latest) mapped to Docker images.
ubuntu-latest maps to catthehacker/ubuntu:act-latest, which already includes
Python, Node, and npm — everything the build needs.
Then enable Actions on the repo: Settings → Actions → Enable.
2. Create the Cloudflare Pages project¶
Create a Direct Upload project (not Git-connected — Wrangler pushes to it):
…or in the dashboard: Workers & Pages → Create → Pages → Direct Upload.
Don't mix Git integration with Direct Upload
A Pages project connected to a Git repo rejects wrangler uploads. If this
site is already deploying from the GitHub → Pages
integration, either point this pipeline at a separate Pages project, or
disconnect the Git integration and let Forgejo be the single source of deploys.
3. Create a scoped API token¶
Cloudflare dashboard → My Profile → API Tokens → Create Token → Custom token:
| Permission | Scope |
|---|---|
| Account → Cloudflare Pages → Edit | Your account |
Copy the token, and grab your Account ID from the Cloudflare dashboard sidebar
(or wrangler whoami).
4. Add the secrets and variable in Forgejo¶
Repo → Settings → Actions:
| Type | Name | Value |
|---|---|---|
| Secret | CLOUDFLARE_API_TOKEN |
the token from step 3 |
| Secret | CLOUDFLARE_ACCOUNT_ID |
your Cloudflare account ID |
| Variable | CF_PAGES_PROJECT |
the Pages project name (e.g. <PROJECT_NAME>) |
5. Push¶
The runner picks up the push, builds with Zensical, and uploads site/ to Pages.
Watch it under Repo → Actions. Once the first deploy succeeds, add your custom
domain on the Pages project (Custom domains → Set up a custom domain).
Notes¶
- Mirroring GitHub: if your repo also has a
.github/workflows/docs.ymlthat deploys to GitHub Pages: the two CI systems ignore each other's workflow directories, so keeping both remotes in sync is fine. - Action resolution: Forgejo resolves
uses:actions from the instance's default actions URL. Ifactions/checkoutfails to resolve, setDEFAULT_ACTIONS_URLunder[actions]in Forgejo'sapp.initohttps://github.com, or pin the full URL in the workflow.