Hosting your static blog on Codeberg Pages
Codeberg Pages serves static files and runs no build of its own. Repository layout, first deploy, automation, custom domain, and what you trade away compared to GitHub.
Codeberg comes up in most conversations about leaving GitHub. It is run by Codeberg e.V., a non profit association registered in Berlin, it runs on Forgejo (a soft fork of Gitea), it is funded by memberships and donations rather than venture capital, and it is hosted in the EU. For a personal blog, those reasons are usually enough to make you look.
The practical question is whether you can actually host the blog there, not just the repository. You can. Codeberg Pages serves static files over HTTPS, supports custom domains, and asks nothing in return.
This post covers the whole path: how Pages works, how to lay out the repository, the first deploy, how to automate it, the custom domain, and what you give up compared to GitHub Pages or Netlify. The examples use Bloggrify, a static blog generator built on Nuxt, but the deployment part is the same for Hugo, Astro or Jekyll: only the build command and the output directory change.
What Codeberg Pages does, and what it does not
Codeberg Pages does one thing: it serves the files it finds in a git branch. There is no build step on their side.
That is the main difference with what you may be used to. GitHub Pages ships with Jekyll built in. Netlify, Vercel and Cloudflare Pages run your build command for you. Codeberg does not. The build happens somewhere else, on your machine or in a CI job, and Pages only publishes the result.
For a Bloggrify blog that constraint is not a problem, because the output is already a plain folder:
npm run generate
This writes the whole site to .output/public (Nuxt also links it as dist), as HTML, CSS, JS and images. No server, no runtime, nothing to install on the host. That folder is what Codeberg will serve.
Choose your URL shape first
Two layouts are possible, and the choice affects your Bloggrify configuration, so make it before you push anything.
A repository named pages. The site is served at the root of your subdomain:
https://yourname.codeberg.page/
Any other repository, with a branch named pages. The site is served under a path:
https://yourname.codeberg.page/myblog/
In both cases the published files live on a branch named pages. In a repository named pages, you can simply make it the default branch.
If you go with the second option, your blog lives in a subfolder, and a static site needs to know that when it generates links, assets and canonical URLs. In Bloggrify that means two settings. First the base path, in nuxt.config.ts:
export default defineNuxtConfig({
extends: ['@bloggrify/core'],
app: {
baseURL: '/myblog/',
},
})
Then the public URL, in app.config.ts, which feeds the sitemap, the RSS feed, the canonical tags and the OG images:
export default defineAppConfig({
url: 'https://yourname.codeberg.page/myblog',
})
Getting this wrong is the most common way to end up with a site that loads a blank page and 404s on every asset. If you plan to put a custom domain on the blog anyway, take the pages repository option and skip the subfolder entirely.
The first deploy, by hand
The sources stay on main, the build goes to pages. Keeping the two apart is easier with a worktree, so you never have to switch branches in the middle of a build.
Create the branch once, with no shared history:
git switch --orphan pages
git commit --allow-empty -m "init pages"
git push -u origin pages
git switch main
Then attach it as a worktree next to your project:
git worktree add ../myblog-pages pages
From now on, publishing is three commands:
npm run generate
rsync -a --delete --exclude .git .output/public/ ../myblog-pages/
cd ../myblog-pages && git add -A && git commit -m "deploy" && git push
Wait a few seconds and the site is live. This is the whole mechanism, and it is worth doing manually once even if you intend to automate it, because everything that follows is just a machine running these same commands.
Automating it
Here you need to know how Codeberg works, and it is different from GitHub. CI on Codeberg is a shared resource paid for by donations, so it is not handed out by default.
Forgejo Actions. The syntax is close to GitHub Actions, and there is an official action to publish to Pages. A workflow in .forgejo/workflows/deploy.yml looks like this:
on:
push:
branches: [main]
jobs:
deploy:
runs-on: docker
container:
image: node:22
steps:
- uses: actions/checkout@v4
- run: npm ci
- run: npm run generate
- uses: https://codeberg.org/git-pages/action@v2
with:
site: https://yourname.codeberg.page/
token: ${{ forge.token }}
source: .output/public/
The action commits the contents of source to the pages branch and handles the token for you. Adjust runs-on to the labels your runner actually exposes.
The catch: hosted Actions on Codeberg are still limited, for security and maintenance reasons. Check the current status before you count on them. You can always attach your own runner, which does not need a public IP.
Woodpecker CI. Codeberg hosts a Woodpecker instance at ci.codeberg.org. Access is granted on request, through a form, project by project, and only linux/amd64 is available. It works well once you are in, but it is not something you enable in a settings toggle.
Your machine. Which leaves the option people underestimate: keep the three commands above in a script, or a git hook, and run them when you publish. A static blog is not a service, it does not need a build server to stay up. Deploying from your laptop is a legitimate answer, and it is the fastest way to get the blog online today while you sort out CI access.
A custom domain
You do not want your blog to live on codeberg.page forever. Two DNS records are needed.
The first points the domain at Codeberg. On a subdomain, a CNAME:
| Domain | Type | Value |
|---|---|---|
www.example.org | CNAME | codeberg.page. |
On an apex domain, where CNAME is not allowed, use an ALIAS or ANAME record to codeberg.page if your DNS provider supports it. If it does not, or if you use DNSSEC, fall back to the addresses documented by Codeberg (217.197.84.141 and 2a0a:4580:103f:c0de::2), keeping in mind that hardcoded IPs can change.
The second record proves that you own the domain and authorises a specific repository to publish on it. It is a TXT record on a _git-pages subdomain, and its exact name depends on whether you deploy through the webhook or through Forgejo Actions. The custom domain documentation gives the current value for both cases, and it is one of the pages that has changed recently, so read it rather than an old tutorial.
Which brings up a detail worth knowing. Most guides you will find, including good ones, tell you to add a .domains file at the root of your repository, the equivalent of GitHub's CNAME file. The current Pages server no longer needs it. If you are migrating an old site, you can delete it.
Certificates are issued automatically through Let's Encrypt, so HTTPS works without you doing anything. One exception: if your domain has CAA records, you have to allow Let's Encrypt explicitly, otherwise the issuance fails silently and you end up staring at a certificate error.
Finally, update url in app.config.ts to the real domain, and rebuild. Otherwise your sitemap, your RSS feed and your social preview images keep advertising the codeberg.page address.
What you are trading away
An honest comparison, because Codeberg is not trying to be Netlify.
No preview deploys. No per branch URL, no preview on a pull request. You have your local npm run dev, and production.
No global CDN. Pages is served from Codeberg's infrastructure in Europe, not from a hundred edge locations. For a text blog whose pages are a few dozen kilobytes, this is mostly invisible. If your readers are in Europe, it is arguably an advantage, and it keeps the whole stack under EU jurisdiction.
Availability is not a commercial SLA. Codeberg has been the target of repeated DDoS waves, and Pages has taken collateral damage. Nicolas Grisey Demengel measured about 80% availability on his site during one of these episodes, and Jean-Rémy Praud ended up keeping the sources on Codeberg while serving the site from Infomaniak. That split is a reasonable pattern, and it is available to you precisely because the output is a folder of static files: the same build can be pushed to a second host without changing a line of the blog.
CI is requested, not given. See above. Plan for the manual deploy first.
None of this disqualifies Codeberg for a personal blog. It does mean you should pick it for what it is, a community funded forge with a simple static host attached, rather than expecting a free replacement for a commercial platform.
Summary
Codeberg Pages serves a git branch. Bloggrify produces a folder of static files. The connection between them is one rsync and one push, and everything else in this post is polish on top of that: the URL shape, the automation, the domain.
If you are starting from scratch, the installation guide gets you a blog running locally in a few minutes, and the deployment page covers the other hosts, since nothing here locks you into one. Bloggrify itself is open source, on GitHub for now, which is an irony we are aware of.
Enjoyed this article?
Subscribe to get new posts delivered to your inbox.