<?xml version="1.0" encoding="utf-8" standalone="yes"?><rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/"><channel><title>Ci-Cd on blog.ntcu.be</title><link>https://blog.ntcu.be/tags/ci-cd/</link><description>Recent content in Ci-Cd on blog.ntcu.be</description><image><title>blog.ntcu.be</title><url>https://blog.ntcu.be/og-image.png</url><link>https://blog.ntcu.be/og-image.png</link></image><generator>Hugo</generator><language>en-us</language><copyright>Jens · RSS</copyright><lastBuildDate>Tue, 13 Oct 2026 15:00:00 +0200</lastBuildDate><atom:link href="https://blog.ntcu.be/tags/ci-cd/index.xml" rel="self" type="application/rss+xml"/><item><title>Self-hosting my code: Forgejo, runners and push-to-deploy</title><link>https://blog.ntcu.be/posts/self-hosted-git-and-ci/</link><pubDate>Tue, 13 Oct 2026 15:00:00 +0200</pubDate><guid>https://blog.ntcu.be/posts/self-hosted-git-and-ci/</guid><description>How I moved my apps off GitHub to a self-hosted Forgejo with my own CI runner, a private container registry and push-to-deploy.</description><content:encoded><![CDATA[<p>For a long time my side projects were deployed the classic homelab way: SSH into
the Docker host, <code>git pull</code> (if the project was even in git), <code>docker compose up -d --build</code> and hope for the best. One of my apps turned out to exist <em>only</em> on
that Docker host. Time to fix that.</p>
<h2 id="the-goal">The goal</h2>
<ul>
<li>Git hosting I control, on my own hardware</li>
<li>CI runners I host myself</li>
<li><code>git push</code> to <code>main</code> = the change is live a minute later</li>
<li>Easy rollback when something breaks</li>
</ul>
<h2 id="the-pieces">The pieces</h2>
<p><strong>Forgejo</strong> is the git server. It&rsquo;s a lightweight, community-run fork of Gitea
and it runs comfortably in a small Proxmox LXC container (2 cores, 2 GB RAM).
It brings repositories, pull requests, a container registry and <strong>Forgejo
Actions</strong>, which uses (almost) the same workflow syntax as GitHub Actions.</p>
<p><strong>A dedicated runner VM</strong> picks up the jobs. It has more CPU and memory than the
Docker host, so all the image building happens there, with a warm build cache.</p>
<p><strong>The Docker host</strong> no longer builds anything. It just pulls images and
restarts containers.</p>
<h2 id="how-a-deploy-works">How a deploy works</h2>
<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="cl">git push ──► runner builds image ──► Forgejo registry ──► Docker host pulls + restarts
</span></span><span class="line"><span class="cl">             (tagged with the commit)
</span></span></code></pre></div><ol>
<li>Every repo has a <code>.forgejo/workflows/deploy.yml</code> with two jobs: <code>build</code> and <code>deploy</code>.</li>
<li><code>build</code> builds the images and pushes them to the Forgejo registry, tagged with
the full commit SHA.</li>
<li><code>deploy</code> SSHes into the Docker host with a key that can do exactly one thing:
run a small deploy script. That script checks out that exact commit and runs
the repo&rsquo;s <code>ci/deploy.sh</code>, which pulls the images and restarts the containers.</li>
</ol>
<p>Because every image is tagged with its commit, <strong>rolling back is just re-running
an older pipeline</strong>.</p>
<h2 id="locking-it-down">Locking it down</h2>
<ul>
<li>The deploy key is restricted with <code>command=</code> and <code>from=</code> in <code>authorized_keys</code>:
it only works from the runner&rsquo;s IP and can only run the deploy script.</li>
<li>Secrets (<code>.env</code> files) never go into git; they only live on the Docker host.</li>
<li>The registry push token can only write packages; the Docker host has a
read-only token.</li>
<li>Old images are cleaned up automatically by a registry cleanup rule.</li>
</ul>
<h2 id="whats-next">What&rsquo;s next</h2>
<ul>
<li>Tests that must pass before anything deploys</li>
<li>A health check after deploy with automatic rollback</li>
<li>Automatic dependency update PRs</li>
</ul>
<p>And this blog? It&rsquo;s a Hugo site in the same Forgejo, deployed by the same
pipeline. Writing this post was <code>git commit &amp;&amp; git push</code>.</p>
]]></content:encoded></item></channel></rss>