
\documentclass[12pt]{article}
\usepackage[T2A,OT1]{fontenc}
\usepackage[default]{cantarell}
\usepackage[a4paper, top=20mm, bottom=20mm, left=20mm, right=20mm]{geometry}
\usepackage[utf8]{inputenc}
\usepackage[russian, english]{babel}
\usepackage{tabu}
\usepackage{hyperref}
\usepackage{parskip}
\usepackage{graphicx}
\usepackage{tabularx}
\usepackage{float}
\floatstyle{boxed}
\restylefloat{figure}
\usepackage{setspace}
\onehalfspacing
\author{Artyom Bologov}
\date{\today}
\title{Codeberg Pages with SSL and Custom Domain}
\makeatletter
\def\endenv{\expandafter\end\expandafter{\@currenvir}}
\makeatother
\begin{document}
\maketitle

\includegraphics[width=\textwidth,height=\textheight,keepaspectratio]{./assets/codeberg-pages.png}

Yesterday night, Gitlab sent me an email saying I’m running out of “compute minutes.”
Whatever that means.
Most likely, I was running out of CI/CD resources when building this website on Gitlab Pages.
It’s been a thing going for some time:

\begin{itemize}\item I’m trying some new absurd tech stack.
\item And Gitlab keeps sending me warnings regarding depleted resources.
\end{itemize}

I guess I went overboard with
\href{run:this-post-is-ed}{ using ed(1) as my new site generator}.
Nonetheless, that’s not nice of Gitlab Pages offering “infinite and free” website builds.
And then saying I have no resources anymore.

So I decided to do the most Saturday evening thing programmers do:
migrate my website to an absolutely different hosting setup.
\href{https://codeberg.page}{To Codeberg Pages}.

Codeberg doesn’t offer some of the features Gitlab provides.
In particular, no CI/CD/website builds, and no automatic SSL certificates.
Thus this post: to document what I went through to reproduce my current setup on Codeberg:

\begin{description}\item[Domain]
aartaka.me and subdomains, managed by Namecheap.
\item[SSL Certificates]
With DNS verification, issued by Let’s Encrypt
\href{run:digital-bum}{(because I’m poor and anti-capitalist.)}
\item[Email]
Custom Domain Email by Disroot.
\item[Build process]
Makefiles and ed(1).
\end{description}

So here’s how I went about it.

\section*{Getting Started with Codeberg Pages} \label{pages}

\href{https://docs.codeberg.org/codeberg-pages}{The documentation by Codeberg is good enough}:
Create a \verb|pages| repository, add HTML files to the root, and \verb|git push| them there.

I diverged from these instructions once:
I named the default branch “pages”, instead of “main”.
This way, my website is deployed from “pages”, while “main” is reserved for my experiments and drafts.

I also had to move all my files to repository root.
On Gitlab, I had them in \verb|public/| subdirectory for deployment.
But that’s too minor to care.

\section*{DNS \& Email} \label{dns-email}

\href{https://docs.codeberg.org/codeberg-pages/using-custom-domain}{ Again, documentation by Codeberg works}:
Create a \verb|.domains| file with your domains:

\begin{figure}[h!]\begin{verbatim}
aartaka.me
aartaka.codeberg.page
pages.aartaka.codeberg.page
pages.pages.aartaka.codeberg.page
\end{verbatim}\caption{My .domains contents}\end{figure}

and add DNS records:

\begin{description}\item[CNAME]
The simplest case for when you only care about the website.
Doesn’t work with e.g. my custom domain email setup.
\item[ALIAS]
A simple enough case that works with my email.
That’s the one I picked.
\item[A/AAAA]
Didn’t try it, sorry.
\end{description}

So here’s my final setup:

\begin{figure}[h!]\begin{verbatim}
ALIAS @ codeberg.page.
TXT @ pages.aartaka.codeberg.page
\end{verbatim}\caption{DNS records for Codeberg Pages (as displayed by Namecheap, @ is aartaka.me)}\end{figure}

The email is managed by MX and SPF records, and ALIAS/TXT don’t interfere with these.
Should be fine, given that I’m still receiving spam.

\section*{Setting Up SSL with Let’s Encrypt} \label{ssl}

This is likely covered elsewhere, but I want to reproduce this anyway.
Especially given that Codeberg docs don’t cover this except for one note in Custom Domain page.
So I need to explain future myself how in the world all of this works.

So... Let’s Encrypt.
They are doing miracles, but they require domain ownership check first.
And this check is done by putting a file into a \verb|.well-known| subdirectory on the server.

The problem is: Codeberg blocks requests to \verb|.well-known|.
So one has to use alternative verification strategies.
Like DNS verification.
\href{"https://www.digitalocean.com/community/tutorials/how-to-acquire-a-let-s-encrypt-certificate-using-dns-validation-with-acme-dns-certbot-on-ubuntu-18-04"}{Here’s the DigitalOcean guide I followed}.
But the procedure is relatively simple:

\begin{itemize}\item Install certbot,
\item Download the certbot extension (acme-dns-auth.py) they’re linking to,
\item Make it executable with \verb|chmod +x acme-dns-auth.py|,
\item Add it to the path where certbot and find it: /etc/letsencrypt/acme-dns-auth.py
\item And run a magic incantation:
\end{itemize}

\begin{figure}[h!]\begin{verbatim}
sudo certbot certonly --manual --manual-auth-hook /etc/letsencrypt/acme-dns-auth.py --preferred-challenges dns --debug-challenges -d *.aartaka.me -d aartaka.me
\end{verbatim}\caption{Certbot call with a DNS auth extension}\end{figure}

It provides all the instructions, and the guide above augments it.
Here are the records I had to add:

\begin{figure}[h!]\begin{verbatim}
// Notice the single letsencrypt.org here:
// Namecheap didn’t allow me to include any value beyond that.
CAA 0 issue "letsencrypt.org"
CNAME _acme-challenge xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx.auth.acme-dns.io.
\end{verbatim}\caption{CAA record to allow certificates from Let’s Encrypt and the actual challenge value (redacted)}\end{figure}

After DNS records update (I waited for two minutes, but better wait five, just in case,)
you can press \verb|Enter| to continue setup.
Certbot will register the domain and generate your certificates.

When the certificates expire, I’ll have to run this scary one-liner again.
But that’s going to happen in 90 days, so who cares?

\section*{Website Generation} \label{generation}

Now the hardest part: Codeberg doesn’t offer any CI/CD beyond static resource publishing.
I might’ve tried to hack them or use some Turing-complete CI quirk.
But I don’t want trouble.

So I’m generating my pages locally and then pushing them to Codeberg.
That’s where the “pages” branch comes into play.
I’m keeping my “main” clean, and then switch to “pages” and generate all the posts in all the formats:

\begin{figure}[h!]\begin{verbatim}
git checkout pages
git merge master
make all -j 4
git commit --all -m "..."
git push
\end{verbatim}\caption{Every post publication procedure (I usually do that in Magit, though)}\end{figure}

I can’t imagine the trouble I’d need to go through if I was using some fancy blog engine.
Endless npm vulnerabilities and gigabytes of \verb|node_modules|, likely.
But, luckily, I’m using ed(1) as my SSG and I don’t have to mess with any of that.
And it’s taking a really short time to generate all 5 (!) formats for all the ~50 pages I have.

So yes, you might be dissatisfied with Codeberg Pages if you want fancy blog engine builds somewhere in the cloud.
But I don’t do that, so I’m perfectly fine with Codeberg.
Still, do give it a try!

\par\noindent\rule{\textwidth}{0.4pt}
Copyright 2022-2025 Artyom Bologov (aartaka).
Any and all opinions listed here are my own and not representative of my employers; future, past and present.

\end{document}
