<?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>Git on Techobyte Blog</title>
    <link>/tags/git/</link>
    <description>Recent content in Git on Techobyte Blog</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <lastBuildDate>Sun, 02 Aug 2026 00:00:00 -0400</lastBuildDate>
    <atom:link href="/tags/git/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>200 Docker Compose Templates</title>
      <link>/posts/200-docker-compose-templates/</link>
      <pubDate>Sun, 02 Aug 2026 00:00:00 -0400</pubDate>
      <guid>/posts/200-docker-compose-templates/</guid>
      <description>&lt;p&gt;&lt;img alt=&#34;200 templates&#34; loading=&#34;lazy&#34; src=&#34;/posts/200-docker-compose-templates/200-compose-templates.png#center&#34;&gt;&lt;/p&gt;
&lt;p&gt;I have been maintaining a git monorepo named &lt;a href=&#34;https://github.com/redjax/docker_templates&#34;&gt;&lt;code&gt;docker_templates&lt;/code&gt;&lt;/a&gt; since sometime in 2023, and I recently added my 200th template &lt;a href=&#34;https://github.com/redjax/docker_templates/tree/7c112b7222d81330c0310ec851c7b512f48347e6&#34;&gt;(commit &lt;code&gt;7c112b7&lt;/code&gt;)&lt;/a&gt;! I thought back to 2015, when I started learning Docker and Docker Compose, and how transformative containers have been in the way I use my machines. There is something very satisfying about describing a runtime you want to work with and encapsulating everything you need to run an app or service the same way each time you run it, on any machine. In reality, there are some edge cases, but it&amp;rsquo;s a beautiful dream.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><img alt="200 templates" loading="lazy" src="/posts/200-docker-compose-templates/200-compose-templates.png#center"></p>
<p>I have been maintaining a git monorepo named <a href="https://github.com/redjax/docker_templates"><code>docker_templates</code></a> since sometime in 2023, and I recently added my 200th template <a href="https://github.com/redjax/docker_templates/tree/7c112b7222d81330c0310ec851c7b512f48347e6">(commit <code>7c112b7</code>)</a>! I thought back to 2015, when I started learning Docker and Docker Compose, and how transformative containers have been in the way I use my machines. There is something very satisfying about describing a runtime you want to work with and encapsulating everything you need to run an app or service the same way each time you run it, on any machine. In reality, there are some edge cases, but it&rsquo;s a beautiful dream.</p>
<p>The structure of the <code>docker_templates</code> repository continues to evolve, but it&rsquo;s also come a long way from where it started. I use this repository in some way almost every day. It is the heart of my homelab, and is where I keep references for pretty much every service I&rsquo;ve stood up on one of my machines. I will now indulge my nostalgia by showing you how the repository has grown over time, how I use it, and share a few of my most useful/favorite containers.</p>
<h2 id="the-beginning-disparate-directories-with-docker-compose-files">The Beginning: Disparate Directories with Docker Compose Files</h2>
<p>I learned to write Dockerfiles to containerize my Python programs, and quickly picked up Docker Compose so I could run things like a Postgres database or Redis message queue (for Python&rsquo;s Celery scheduling library). Eventually, I started to find programs and apps I wanted to host myself, like a media server, document hosting, monitoring/alerting services, etc.</p>
<p>For years, whenever I wanted to try a new service, I would create a directory for it, write a <code>docker-compose.yml</code> file, and run it. I was hardcoding non-secret values in my Compose files for a long time (using a <code>.env</code> or environment variables to pass secrets to the template). Sometimes I would initialize a git repository and push the stack to Github, other times the directory would just sit on one of my machines wherever I originally put it. The mental overhead of deciding if the new service I wanted to try was worthy of initializing as a git repository, making sure my <code>.gitignore</code> would keep my <code>.env</code> file out of git history and ignore host volume mounts, and deciding where to put the code on my machine started to slow me down and made me hesitant to really invest time into the templates I was creating. I ended up with a lot of different repositories and realized this was not sustainable long-term.</p>
<h2 id="the-monorepo">The Monorepo</h2>
<p>At some point in 2023, I decided to start a new git monorepo to store all of the services I ran in Docker Compose. I created my <a href="https://github.com/redjax/docker_templates"><code>docker_templates</code> repository</a>, and one-by-one started copying Compose files I had written into the repository. Pulling everything into one place allowed me to do some cleanup, and made it much easier to start new templates. Whenever I wanted to run a new service, I would clone the repository into a path in my <code>~/git/</code> directory, name it after the service, and create the <code>docker-compose.yml</code> file alongside all my other templates in a branch named after the service. Each service existed in an immediate child of the <code>templates/</code> directory, and over time that path became difficult to sort through.</p>
<p>This worked great for a while, but as I added more and more containers, and thought about all of the templates I planned to add in the future, I realized the structure of the <code>templates/</code> directory would need to change, and the multiplicative git cloning would end up being a storage problem in the long term. I decided to overhaul the organization of templates in the repository, and write scripts to help me manage the complexity and initialize new templates in a standardized way.</p>
<h3 id="cookiecutter-templates">Cookiecutter Templates</h3>
<p>When I would start new service templates, I would usually go to a previous template and copy the <code>docker-compose.yml</code> and <code>README.md</code> files into the new path and rewrite them for the new service. I realized I wanted to standardize the way I initialized new template directories. I was familiar with Jinja2 templating from writing Python programs, and had used <a href="https://github.com/cookiecutter/cookiecutter">Cookiecutter</a> in the past to create templated Python project repositories. So I created a <a href="https://github.com/redjax/docker_templates/tree/main/templates/_cookiecutter/docker-template">standardized Docker Compose service template directory</a>, with a common starting point for all future templates, and a <a href="https://github.com/redjax/docker_templates/blob/main/scripts/new_template.py">Python script</a> to guide the user through initializing a new template. I am definitely yada-yada-ing a lot of steps I took to get to this point, but after a few iterations, I settled on the template I have been using for the majority of the time this repository has existed.</p>
<p>Each new repository created from the Cookiecutter template starts with the same files:</p>
<ul>
<li><code>.docker-compose.template</code>: An empty marker file for some of the maintenance scripts&hellip;I&rsquo;ll explain more later.</li>
<li><code>.env.example</code>: I parameterize my <code>compose.yml</code> files, and provide an example <code>.env</code> file with the defaults for each service.
<ul>
<li>The user is instructed to copy <code>.env.example</code> to <code>.env</code> (which is ignored in the <code>.gitignore</code> for the whole repository) to configure the running stack.</li>
</ul>
</li>
<li><code>.gitignore</code>: Template-local ignore pattern overrides.</li>
<li><code>README.md</code>: The <code>new_template.py</code> script prompts the user for a title, summary, and optional description, and populates the README file with the user&rsquo;s inputs.</li>
<li><code>compose.yml</code>: The Python script generates a Docker Compose template file with the basic shape defined, so I can just start writing service definitions.</li>
</ul>
<h3 id="path-markers">Path Markers</h3>
<p>Once the template business was all sorted out, I decided to create <a href="https://github.com/redjax/docker_templates/tree/main/templates">subdirectories under the <code>templates/</code> path</a> that would serve as &ldquo;categories.&rdquo; I put my Postgres, Mariadb, Redis, and InnoDB containers under the <code>database/</code> path, my Plex and Jellyfin servers under <code>media/</code>, Ntfy and Gotify containers under <code>notifications/</code>, and so on. Splitting things up this way made it easier to browse through the containers on the web, and helped to kept the stacks logically sorted by archetype.</p>
<p>In each &ldquo;category&rdquo; directory, I created an empty <code>.category</code> file marker. I used the <code>.category</code> and <code>.docker-compose.template</code> files to write scripts that managed some of the complexity in the repository. For example, the <a href="https://github.com/redjax/docker_templates/blob/main/scripts/count_templates.py"><code>count_templates.py</code> script</a> finds all of the <code>.docker-compose.template</code> files in the repository and returns a count, which I use to update the templates count in the <a href="https://github.com/redjax/docker_templates/blob/main/README.md">repository&rsquo;s README.md</a>. I wrote a <a href="https://github.com/redjax/docker_templates/blob/main/.github/workflows/update-templates-count.yml">Github workflow</a> to run the script on a schedule, and if the count is different from what&rsquo;s in the README.md, it updates the <code>Templates:\s*\d+</code> pattern with the new count.</p>
<h3 id="repository-map">Repository Map</h3>
<p>I also created a <a href="https://github.com/redjax/docker_templates/tree/main/map">repository &ldquo;map&rdquo;</a>, a README.md file that finds all of the <code>.category</code> markers and creates a file tree from <a href="https://github.com/redjax/docker_templates/blob/main/map/_template/README.md.j2">a README.md Jinja template</a>, and a <a href="https://github.com/redjax/docker_templates/blob/main/scripts/update_repo_map.py">script to update the map README when new categories are created</a>. The <a href="https://github.com/redjax/docker_templates/blob/main/.github/workflows/update-repo-map.yml"><code>update-repo-map.yml</code> Github workflow</a> also runs nightly to keep this file updated with the latest templates.</p>
<h3 id="repository-metadata">Repository Metadata</h3>
<p>The scripts that update my README files also write data to the <a href="https://github.com/redjax/docker_templates/tree/main/metadata"><code>metadata/</code> directory</a>. This can act as a sort of read-only API using cURL requests. For example, the repository map uses the <a href="https://github.com/redjax/docker_templates/blob/main/metadata/categories.json"><code>categories.json</code> file</a> to populate the tree, and all <code>.category</code> and <code>.docker-compose.template</code> markers are in <a href="https://github.com/redjax/docker_templates/blob/main/metadata/beacons.json"><code>beacons.json</code></a>. I started calling the marker files &ldquo;beacons&rdquo; at one point, and it just kind of stuck.</p>
<p>The metadata files are only really used for rendering README templates, but I have plans for things like a frontend webUI to explore the repository&rsquo;s templates, and a Go CLI for downloading and using individual templates.</p>
<h2 id="git-sparse-checkouts">Git Sparse Checkouts</h2>
<p>I mentioned earlier that I was cloning the whole repository each time I wanted to run a template I had in <code>docker_templates</code>. This quickly became a problem as the size of the repository grew. The repository is currently 10MB in size (mostly due to image files and some larger files in history that I&rsquo;ll clean up at some point), so each time I cloned the repository, I added 10MB of disk usage.</p>
<p>I discovered <a href="https://git-scm.com/docs/git-sparse-checkout">git sparse checkouts</a> when I searched for a solution to this problem. A sparse checkout is a git operation that allows you to checkout only a subset of the files in a repository. When I&rsquo;m running a Docker Compose template, I don&rsquo;t need to pull the <code>src/img</code> directory, with the <code>.png</code> I render in the main README.md; I can pull just the files in the Compose template I wish to run.</p>
<p>In practice, most of my sparse checkouts are ~5% of the total size of the repository, which is about the same amount of size they would take as separate git repositories. It adds a few steps to the initial checkout process, but it makes the clone a focused copy with only as much as I need to run.</p>
<p>As an example, if I want to run a <a href="https://github.com/redjax/docker_templates/tree/main/templates/monitoring_alerting/docker_zabbix">Zabbix server container</a>, I would run the following commands:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$&gt; git clone --no-checkout https://github.com/redjax/docker_templates docker_zabbix
</span></span><span class="line"><span class="cl">$&gt; <span class="nb">cd</span> docker_zabbix
</span></span><span class="line"><span class="cl"><span class="c1">## Initialize sparse checkout</span>
</span></span><span class="line"><span class="cl">$&gt; git sparse-checkout init --cone
</span></span><span class="line"><span class="cl"><span class="c1">## Tell git which paths to checkout</span>
</span></span><span class="line"><span class="cl">$&gt; git sparse-checkout <span class="nb">set</span> templates/monitoring_alerting/docker_zabbix
</span></span><span class="line"><span class="cl"><span class="c1">## Checkout the main branch, or a working branch for the zabbix server</span>
</span></span><span class="line"><span class="cl">$&gt; git checkout feat/some-zabbix-feature
</span></span></code></pre></td></tr></table>
</div>
</div><p>This would create a directory named <code>docker_zabbix/</code>, which would have all of the files in the root path, and a single directory named <code>templates/</code>, with <code>monitoring_alerting/docker_zabbix</code>. All of the other containers still exist in the remote, but sparse checkouts let me focus on a single template.</p>
<p><a href="https://git-scm.com/docs/git-worktree">Git Worktrees</a> were not a thing when I started using sparse checkouts. An alternative to the sparse checkout method described above is cloning the whole <code>docker_templates</code> repository once, then creating worktrees for all of the services you want to run. A worktree exists in a separate path on the machine like a sparse clone would. They share the same git object database, meaning paths and objects are deduplicated, which saves space on the disk.</p>
<p>To create a Zabbix server container using a worktree, you would clone the whole repository once, <code>cd</code> into it, then create worktrees for each service you want to run. The limitation here is that each worktree checks out a branch, and only that worktree can use that branch. So, the original repository clone stays on the <code>main</code> branch, and each worktree must have its own branch, even if I&rsquo;m not making any changes on that service.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$&gt; git clone https://github.com/redjax/docker_templates
</span></span><span class="line"><span class="cl">$&gt; <span class="nb">cd</span> docker_templates
</span></span><span class="line"><span class="cl">$&gt; git worktree add -b feat/zabbix-server ../docker_zabbix
</span></span><span class="line"><span class="cl">$&gt; <span class="nb">cd</span> ../docker_zabbix
</span></span></code></pre></td></tr></table>
</div>
</div><p>I have continued to use sparse checkouts because I often checkout a service and just run it on the <code>main</code> branch until I have updates or fixes to apply, and this flow does not work with worktrees.</p>
<h2 id="how-i-use-it">How I Use It</h2>
<p>I have a full clone of this repository on a few of my machines, which I don&rsquo;t modify after cloning except to create new templates. When I want to try a new service, I <code>cd</code> into the local clone, create a branch for the new service, and run the <code>new_template.py</code> script, which prompts me for a template name, category, summary, and optional description. I usually add a link of some sort in the summary, i.e. a Github repository or documentation URL.</p>
<p>I commit the files as the initial starting point for that repository and push the branch up to git. Then I do a sparse clone and checkout the new service&rsquo;s branch, and I get to work building the <code>compose.yml</code> and <code>.env.example</code>. I usually create host volume mounts in the git path, although a better practice would be storing Docker files in another path, like <code>/opt/docker_data</code> or something. The things we wished we learned sooner&hellip;</p>
<p>Eventually, when the template is in working condition, I merge the branch into <code>main</code>. As I make changes to the template, I create <code>feat/</code> and <code>fix/</code> branches, which also get merged into main. The idea/goal is to be running my services off the stable <code>main</code> branch as often as possible.</p>
<h3 id="favorite-containers">Favorite Containers</h3>
<p>With some containers, I get them into working order, then bring the stack down and forget about it. I still like having the reference file in my repository, but I might not actively run the container. If it&rsquo;s in the <code>main</code> branch, it means I got it running at one point and it&rsquo;s ready to run again.</p>
<p>But there are some templates I am constantly running, or re-using on different machines.</p>
<ul>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/networking/docker_pangolin"><code>docker_pangolin</code></a>: One of the most important services in my stack! <a href="https://github.com/fosrl/pangolin">Pangolin</a> is my reverse HTTP proxy.
<ul>
<li>I have Pangolin running on a VPS I rent.</li>
<li>I route my domain name through Cloudflare, and have Cloudflare pointed at the VPS.</li>
<li>In Pangolin, I create subdomain routes to services I want to expose to the Internet, and route the traffic over a private tunnel.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/networking/docker_adguard"><code>docker_adguard</code></a>: I run an <a href="https://github.com/redjax/docker_templates/tree/main/templates/networking/docker_adguard">AdGuard Home container</a> for ad blocking on my entire LAN.
<ul>
<li>My router uses the AdGuard container as its DNS server.</li>
<li>I create records for my important machines, so I can resolve <code>machine-name.home</code> anywhere on my network instead of remembering IP addresses.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/networking/docker_netbird"><code>docker_netbird</code></a>: I use <a href="https://netbird.io/">Netbird</a> to run a private network, similar to how many people use <a href="https://tailscale.com/">Tailscale</a>.
<ul>
<li>I use access policies to allow or restrict traffic between devices, and to give my friends limited access to things like my media and game servers.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/documents/docker_paperless-ngx"><code>docker_paperless-ngx</code></a>: <a href="https://docs.paperless-ngx.com/">Paperless-ngx</a> is an incredibly useful document management system.
<ul>
<li>I upload my receipts, work documents for things like benefits packages and employee handbooks, PDFs, and airline tickets.</li>
<li>Daily backups to local and offsite storage prevent data loss (and I&rsquo;ve had to recover a few times; don&rsquo;t neglect your backups!).</li>
<li>Full-text search lets me find things quickly, and the tagging system lets me sort documents, with tags like <code>receipt</code>, <code>pets</code>, <code>house</code>, <code>vehicle</code>, etc.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/automation/docker_concourse_ci"><code>docker_concourse_ci</code></a>: I use <a href="https://concourse-ci.org/">Concourse CI</a> for some of my homelab automation pipelines.</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/automation/docker_semaphore"><code>docker_semaphore</code></a>: <a href="https://semaphoreui.com/">Semaphore</a> lets me run my <a href="https://gitlab.com/redjax/ansible-roles">Ansible roles</a> and <a href="https://gitlab.com/redjax/ansible-playbooks">playbooks</a>, as well as scheduled Bash scripts and <a href="https://github.com/redjax/Terraform">Terraform</a> from a convenient webUI.</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/backup/docker_gickup"><code>docker_gickup</code></a>: <a href="https://github.com/cooperspencer/gickup">Gickup</a> is a useful utility for backing up and mirroring git repositories.
<ul>
<li>I keep a config file with all of my most important/valuable repositories, and Gickup runs on a Raspberry Pi, pulling my changes to a local drive and mirroring some to a self-hosted Git instance.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/database"><code>database</code></a>: A collection of database templates.
<ul>
<li>I frequently use <a href="https://github.com/redjax/docker_templates/tree/main/templates/database/docker_postgresql">Postgres</a> and <a href="https://github.com/redjax/docker_templates/tree/main/templates/database/docker_redis">Redis</a> in my homelab, and I use <a href="https://github.com/redjax/docker_templates/tree/main/templates/database/docker_influxdb">InfluxDB</a> for some of my time-series data like weather readings.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/bookmarking/docker_linkwarden"><code>docker_linkwarden</code></a>: <a href="https://linkwarden.app/">Linkwarden</a> is a bookmarking/read-it-later app.
<ul>
<li>I imported all of my browser bookmarks when I set it up initially, and I frequently save new links I come across on the web.</li>
<li>I can export Linkwarden&rsquo;s saved links as a <code>bookmarks.html</code> file to synchronize back to my browser.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/code_forges/docker_forgejo"><code>docker_forgejo</code></a>: <a href="https://forgejo.org/">Forgejo</a> is a self-hosted Git forge, reminiscent of 2015-2018 Github.
<ul>
<li>The Forgejo Actions platform is semi-compatible with Github actions, and Forgejo&rsquo;s pipelines are very similar in syntax.</li>
<li>I host a private/local-only Git forge for some of my more sensitive repositories that I do not want to expose to the Internet.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/code_forges/docker_opengist"><code>docker_opengist</code></a>: I am a big fan of Github Gists, and discovered <a href="https://opengist.io/">OpenGist</a>, which is basically the self-hosted version.
<ul>
<li>I put a lot of code snippets, scripts, and reference files here, and occasionally I will expand a gist into an Obsidian note, or a post on this blog!</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/dav"><code>dav</code></a>: I self-host my contacts and calendars, and synchronize them with my email client.
<ul>
<li>I primarily use <a href="https://github.com/redjax/docker_templates/tree/main/templates/dav/docker_radicale">Radicale</a>, but have recently been test driving <a href="https://github.com/redjax/docker_templates/tree/main/templates/dav/docker_davis">Davis</a> and am liking it a lot.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/games"><code>games</code></a>: I host a bunch of game servers for my friends and me. We connect over a private network I host to play.</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/monitoring_alerting"><code>monitoring_alerting</code></a>: I have tried many different monitoring/alerting solutions, and have kept a template for each one in this path.
<ul>
<li>I used <a href="https://github.com/redjax/docker_templates/tree/main/templates/monitoring_alerting/docker_zabbix">Zabbix</a> for a while, but more recently I have used <a href="https://github.com/redjax/docker_templates/tree/main/templates/monitoring_alerting/docker_beszel">Beszel</a> for server metric monitoring, <a href="https://github.com/redjax/docker_templates/tree/main/templates/monitoring_alerting/docker_uptime-kuma">Uptime Kuma</a> for uptime checks, <a href="https://github.com/redjax/docker_templates/tree/main/templates/monitoring_alerting/docker_happydomain">happyDomain</a> to track and monitor my domains and DNS records, and <a href="https://github.com/redjax/docker_templates/tree/main/templates/monitoring_alerting/docker_ntopng">NtopNG</a> for local network monitoring.</li>
<li>I run Uptime Kuma from a VPS so it can watch my homelab for outages.</li>
</ul>
</li>
<li><a href="https://github.com/redjax/docker_templates/tree/main/templates/notifications/docker_ntfy"><code>docker_ntfy</code></a>/<a href="https://github.com/redjax/docker_templates/tree/main/templates/notifications/docker_gotify"><code>docker_gotify</code></a>: My own self-hosted push notification servers.
<ul>
<li>Ntfy and Gotify work very similarly, and I have been test driving both for a while.</li>
<li>I can&rsquo;t decide which one I prefer, and I don&rsquo;t see much harm in using both.</li>
<li>I&rsquo;ve recently added <a href="https://github.com/redjax/docker_templates/tree/main/templates/notifications/docker_apprise">Apprise</a> into the mix, which lets me send notifications to both services.</li>
</ul>
</li>
</ul>
]]></content:encoded>
    </item>
    <item>
      <title>Git Rewrite History</title>
      <link>/notes/git-rewrite-history/</link>
      <pubDate>Tue, 13 Jan 2026 23:38:06 -0500</pubDate>
      <guid>/notes/git-rewrite-history/</guid>
      <description>Code snippet or command reference</description>
      <content:encoded><![CDATA[<p>If you have ever accidentally committed code under the wrong <code>git.user</code>/<code>git.email</code>, you should know you can rewrite the <code>git log</code> to change commit authors using <code>git filter-branch</code>.</p>
<h2 id="git-filter-repo-plugin">Git filter-repo plugin</h2>
<p>Install the <a href="https://github.com/newren/git-filter-repo/blob/main/INSTALL.md"><code>git-filter-repo</code></a> plugin for Git with Python:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">pip install git-filter-repo
</span></span></code></pre></td></tr></table>
</div>
</div><p>This plugin is required to run the <code>git filter-repo</code> command. It is ok to install this using &ldquo;system Python.&rdquo;</p>
<p>If you are running Linux, you can install it with <code>apt install -y git-filter-repo</code> (or whatever package manager your distribution uses, i.e. <code>dnf</code> for RedHat/Fedora).</p>
<h2 id="steps">Steps</h2>
<p>Clone the repository that has the history you want to rewrite using the <code>--bare</code> flag:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git clone --bare git@github.com:user/repo.git
</span></span></code></pre></td></tr></table>
</div>
</div><p>Run the following command to replace all instances of the old/wrong name &amp; email with a different Git user:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span><span class="lnt">8
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git filter-branch --env-filter <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">if [ &#34;$GIT_COMMITTER_NAME&#34; = &#34;Old Name&#34; ] &amp;&amp; [ &#34;$GIT_COMMITTER_EMAIL&#34; = &#34;old.email@example.com&#34; ]; then
</span></span></span><span class="line"><span class="cl"><span class="s1">    GIT_COMMITTER_NAME=&#34;New Name&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">    GIT_COMMITTER_EMAIL=&#34;new.email@example.com&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">    GIT_AUTHOR_NAME=&#34;New Name&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">    GIT_AUTHOR_EMAIL=&#34;new.email@example.com&#34;
</span></span></span><span class="line"><span class="cl"><span class="s1">fi
</span></span></span><span class="line"><span class="cl"><span class="s1">&#39;</span> --tag-name-filter cat -- --branches --tags
</span></span></code></pre></td></tr></table>
</div>
</div><style type="text/css">
     
    .notice {
        --title-color: #fff;
        --title-background-color: #6be;
        --content-color: #444;
        --content-background-color: #e7f2fa;
    }

    .notice.info {
        --title-background-color: #fb7;
        --content-background-color: #fec;
    }

    .notice.tip {
        --title-background-color: #5a5;
        --content-background-color: #efe;
    }

    .notice.warning {
        --title-background-color: #c33;
        --content-background-color: #fee;
    }

     
    @media (prefers-color-scheme:dark) {
        .notice {
            --title-color: #fff;
            --title-background-color: #069;
            --content-color: #ddd;
            --content-background-color: #023;
        }

        .notice.info {
            --title-background-color: #a50;
            --content-background-color: #420;
        }

        .notice.tip {
            --title-background-color: #363;
            --content-background-color: #121;
        }

        .notice.warning {
            --title-background-color: #800;
            --content-background-color: #400;
        }
    }

    body.dark .notice {
        --title-color: #fff;
        --title-background-color: #069;
        --content-color: #ddd;
        --content-background-color: #023;
    }

    body.dark .notice.info {
        --title-background-color: #a50;
        --content-background-color: #420;
    }

    body.dark .notice.tip {
        --title-background-color: #363;
        --content-background-color: #121;
    }

    body.dark .notice.warning {
        --title-background-color: #800;
        --content-background-color: #400;
    }

     
    .notice {
        padding: 18px;
        line-height: 24px;
        margin-bottom: 24px;
        border-radius: 4px;
        color: var(--content-color);
        background: var(--content-background-color);
    }

    .notice p:last-child {
        margin-bottom: 0
    }

     
    .notice-title {
        margin: -18px -18px 12px;
        padding: 4px 18px;
        border-radius: 4px 4px 0 0;
        font-weight: 700;
        color: var(--title-color);
        background: var(--title-background-color);
    }

     
    .icon-notice {
        display: inline-flex;
        align-self: center;
        margin-right: 8px;
    }

    .icon-notice img,
    .icon-notice svg {
        height: 1em;
        width: 1em;
        fill: currentColor;
    }

    .icon-notice img,
    .icon-notice.baseline svg {
        top: .125em;
        position: relative;
    }
</style><div class="notice info" >
    <p class="notice-title">
        <span class="icon-notice baseline">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="92 59.5 300 300">
  <path d="M292 303.25V272c0-3.516-2.734-6.25-6.25-6.25H267v-100c0-3.516-2.734-6.25-6.25-6.25h-62.5c-3.516 0-6.25 2.734-6.25 6.25V197c0 3.516 2.734 6.25 6.25 6.25H217v62.5h-18.75c-3.516 0-6.25 2.734-6.25 6.25v31.25c0 3.516 2.734 6.25 6.25 6.25h87.5c3.516 0 6.25-2.734 6.25-6.25Zm-25-175V97c0-3.516-2.734-6.25-6.25-6.25h-37.5c-3.516 0-6.25 2.734-6.25 6.25v31.25c0 3.516 2.734 6.25 6.25 6.25h37.5c3.516 0 6.25-2.734 6.25-6.25Zm125 81.25c0 82.813-67.188 150-150 150-82.813 0-150-67.188-150-150 0-82.813 67.188-150 150-150 82.813 0 150 67.188 150 150Z"/>
</svg>

        </span> Info </p><p>Replace <code>Old Name</code> with the old <code>git config user.name</code>, <code>old.email@example.com</code> with the old <code>git config user.email</code>, and do the same for <code>New Name</code> and <code>new.email@example.com</code>.</p></div>

<p>Clean the <code>reflog</code> and run Git garbage collection to remove any cached history with the old Git user:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git reflog expire --expire<span class="o">=</span>now --all
</span></span><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now
</span></span></code></pre></td></tr></table>
</div>
</div><p>Finally, force push the changes back to the remote:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git push --force --all
</span></span><span class="line"><span class="cl">git push --force --tags
</span></span></code></pre></td></tr></table>
</div>
</div><div class="notice tip" >
    <p class="notice-title">
        <span class="icon-notice baseline">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="300.5 134 300 300">
  <path d="M551.281 252.36c0-3.32-1.172-6.641-3.515-8.985l-17.774-17.578c-2.344-2.344-5.469-3.711-8.789-3.711-3.32 0-6.445 1.367-8.789 3.71l-79.687 79.493-44.141-44.14c-2.344-2.344-5.469-3.712-8.79-3.712-3.32 0-6.444 1.368-8.788 3.711l-17.774 17.579c-2.343 2.343-3.515 5.664-3.515 8.984 0 3.32 1.172 6.445 3.515 8.789l70.704 70.703c2.343 2.344 5.664 3.711 8.789 3.711 3.32 0 6.64-1.367 8.984-3.71l106.055-106.056c2.343-2.343 3.515-5.468 3.515-8.789ZM600.5 284c0 82.813-67.188 150-150 150-82.813 0-150-67.188-150-150 0-82.813 67.188-150 150-150 82.813 0 150 67.188 150 150Z"/>
</svg>

        </span> Tip </p><p>If you have branch protection rules that prevent force pushing, you will need to turn them off temporarily to run the force push.</p></div>

<p>Verify the rewrite succeeded by cloning the repository to a new path and search the log for the old user:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">mkdir ~/tmp
</span></span><span class="line"><span class="cl">git clone git@github.com:user/repo.git ~/tmp/repo
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> ~/tmp/repo
</span></span><span class="line"><span class="cl">git log --author<span class="o">=</span><span class="s2">&#34;Old Name&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>You should not see any results; if you do, look back through the history of your commands to see if there were any errors during the process.</p>
<h2 id="bash-script">Bash script</h2>
<p>On a Linux or Mac system, you can use this Bash script to automate the steps above. Run the script with <code>--help</code> to see the usage menu.</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">  1
</span><span class="lnt">  2
</span><span class="lnt">  3
</span><span class="lnt">  4
</span><span class="lnt">  5
</span><span class="lnt">  6
</span><span class="lnt">  7
</span><span class="lnt">  8
</span><span class="lnt">  9
</span><span class="lnt"> 10
</span><span class="lnt"> 11
</span><span class="lnt"> 12
</span><span class="lnt"> 13
</span><span class="lnt"> 14
</span><span class="lnt"> 15
</span><span class="lnt"> 16
</span><span class="lnt"> 17
</span><span class="lnt"> 18
</span><span class="lnt"> 19
</span><span class="lnt"> 20
</span><span class="lnt"> 21
</span><span class="lnt"> 22
</span><span class="lnt"> 23
</span><span class="lnt"> 24
</span><span class="lnt"> 25
</span><span class="lnt"> 26
</span><span class="lnt"> 27
</span><span class="lnt"> 28
</span><span class="lnt"> 29
</span><span class="lnt"> 30
</span><span class="lnt"> 31
</span><span class="lnt"> 32
</span><span class="lnt"> 33
</span><span class="lnt"> 34
</span><span class="lnt"> 35
</span><span class="lnt"> 36
</span><span class="lnt"> 37
</span><span class="lnt"> 38
</span><span class="lnt"> 39
</span><span class="lnt"> 40
</span><span class="lnt"> 41
</span><span class="lnt"> 42
</span><span class="lnt"> 43
</span><span class="lnt"> 44
</span><span class="lnt"> 45
</span><span class="lnt"> 46
</span><span class="lnt"> 47
</span><span class="lnt"> 48
</span><span class="lnt"> 49
</span><span class="lnt"> 50
</span><span class="lnt"> 51
</span><span class="lnt"> 52
</span><span class="lnt"> 53
</span><span class="lnt"> 54
</span><span class="lnt"> 55
</span><span class="lnt"> 56
</span><span class="lnt"> 57
</span><span class="lnt"> 58
</span><span class="lnt"> 59
</span><span class="lnt"> 60
</span><span class="lnt"> 61
</span><span class="lnt"> 62
</span><span class="lnt"> 63
</span><span class="lnt"> 64
</span><span class="lnt"> 65
</span><span class="lnt"> 66
</span><span class="lnt"> 67
</span><span class="lnt"> 68
</span><span class="lnt"> 69
</span><span class="lnt"> 70
</span><span class="lnt"> 71
</span><span class="lnt"> 72
</span><span class="lnt"> 73
</span><span class="lnt"> 74
</span><span class="lnt"> 75
</span><span class="lnt"> 76
</span><span class="lnt"> 77
</span><span class="lnt"> 78
</span><span class="lnt"> 79
</span><span class="lnt"> 80
</span><span class="lnt"> 81
</span><span class="lnt"> 82
</span><span class="lnt"> 83
</span><span class="lnt"> 84
</span><span class="lnt"> 85
</span><span class="lnt"> 86
</span><span class="lnt"> 87
</span><span class="lnt"> 88
</span><span class="lnt"> 89
</span><span class="lnt"> 90
</span><span class="lnt"> 91
</span><span class="lnt"> 92
</span><span class="lnt"> 93
</span><span class="lnt"> 94
</span><span class="lnt"> 95
</span><span class="lnt"> 96
</span><span class="lnt"> 97
</span><span class="lnt"> 98
</span><span class="lnt"> 99
</span><span class="lnt">100
</span><span class="lnt">101
</span><span class="lnt">102
</span><span class="lnt">103
</span><span class="lnt">104
</span><span class="lnt">105
</span><span class="lnt">106
</span><span class="lnt">107
</span><span class="lnt">108
</span><span class="lnt">109
</span><span class="lnt">110
</span><span class="lnt">111
</span><span class="lnt">112
</span><span class="lnt">113
</span><span class="lnt">114
</span><span class="lnt">115
</span><span class="lnt">116
</span><span class="lnt">117
</span><span class="lnt">118
</span><span class="lnt">119
</span><span class="lnt">120
</span><span class="lnt">121
</span><span class="lnt">122
</span><span class="lnt">123
</span><span class="lnt">124
</span><span class="lnt">125
</span><span class="lnt">126
</span><span class="lnt">127
</span><span class="lnt">128
</span><span class="lnt">129
</span><span class="lnt">130
</span><span class="lnt">131
</span><span class="lnt">132
</span><span class="lnt">133
</span><span class="lnt">134
</span><span class="lnt">135
</span><span class="lnt">136
</span><span class="lnt">137
</span><span class="lnt">138
</span><span class="lnt">139
</span><span class="lnt">140
</span><span class="lnt">141
</span><span class="lnt">142
</span><span class="lnt">143
</span><span class="lnt">144
</span><span class="lnt">145
</span><span class="lnt">146
</span><span class="lnt">147
</span><span class="lnt">148
</span><span class="lnt">149
</span><span class="lnt">150
</span><span class="lnt">151
</span><span class="lnt">152
</span><span class="lnt">153
</span><span class="lnt">154
</span><span class="lnt">155
</span><span class="lnt">156
</span><span class="lnt">157
</span><span class="lnt">158
</span><span class="lnt">159
</span><span class="lnt">160
</span><span class="lnt">161
</span><span class="lnt">162
</span><span class="lnt">163
</span><span class="lnt">164
</span><span class="lnt">165
</span><span class="lnt">166
</span><span class="lnt">167
</span><span class="lnt">168
</span><span class="lnt">169
</span><span class="lnt">170
</span><span class="lnt">171
</span><span class="lnt">172
</span><span class="lnt">173
</span><span class="lnt">174
</span><span class="lnt">175
</span><span class="lnt">176
</span><span class="lnt">177
</span><span class="lnt">178
</span><span class="lnt">179
</span><span class="lnt">180
</span><span class="lnt">181
</span><span class="lnt">182
</span><span class="lnt">183
</span><span class="lnt">184
</span><span class="lnt">185
</span><span class="lnt">186
</span><span class="lnt">187
</span><span class="lnt">188
</span><span class="lnt">189
</span><span class="lnt">190
</span><span class="lnt">191
</span><span class="lnt">192
</span><span class="lnt">193
</span><span class="lnt">194
</span><span class="lnt">195
</span><span class="lnt">196
</span><span class="lnt">197
</span><span class="lnt">198
</span><span class="lnt">199
</span><span class="lnt">200
</span><span class="lnt">201
</span><span class="lnt">202
</span><span class="lnt">203
</span><span class="lnt">204
</span><span class="lnt">205
</span><span class="lnt">206
</span><span class="lnt">207
</span><span class="lnt">208
</span><span class="lnt">209
</span><span class="lnt">210
</span><span class="lnt">211
</span><span class="lnt">212
</span><span class="lnt">213
</span><span class="lnt">214
</span><span class="lnt">215
</span><span class="lnt">216
</span><span class="lnt">217
</span><span class="lnt">218
</span><span class="lnt">219
</span><span class="lnt">220
</span><span class="lnt">221
</span><span class="lnt">222
</span><span class="lnt">223
</span><span class="lnt">224
</span><span class="lnt">225
</span><span class="lnt">226
</span><span class="lnt">227
</span><span class="lnt">228
</span><span class="lnt">229
</span><span class="lnt">230
</span><span class="lnt">231
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="cp">#!/usr/bin/env bash
</span></span></span><span class="line"><span class="cl"><span class="nb">set</span> -euo pipefail
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Find a Python interpreter for pip installs</span>
</span></span><span class="line"><span class="cl"><span class="nv">PYTHON_BIN</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">for</span> bin in python3 python py py3 python<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nb">command</span> -v <span class="s2">&#34;</span><span class="nv">$bin</span><span class="s2">&#34;</span> &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">PYTHON_BIN</span><span class="o">=</span><span class="nv">$bin</span>
</span></span><span class="line"><span class="cl">    <span class="nb">break</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Ensure git-filter-repo is installed (try uv first, then pip)</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! <span class="nb">command</span> -v git-filter-repo &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;git-filter-repo not found.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">## Install with uv, if available</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="nb">command</span> -v uv &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;uv found. Installing git-filter-repo as a tool...&#34;</span>
</span></span><span class="line"><span class="cl">    uv tool install git-filter-repo
</span></span><span class="line"><span class="cl">    <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$PYTHON_BIN</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;No Python interpreter found. Please install Python or uv.&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="c1">## Fallback to Python</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Using </span><span class="nv">$PYTHON_BIN</span><span class="s2"> to install git-filter-repo via pip...&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="s2">&#34;</span><span class="nv">$PYTHON_BIN</span><span class="s2">&#34;</span> -m pip install --user git-filter-repo
</span></span><span class="line"><span class="cl">  <span class="nb">export</span> <span class="nv">PATH</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$HOME</span><span class="s2">/.local/bin:</span><span class="nv">$PATH</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Test git-filter-repo was installed correctly</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> ! <span class="nb">command</span> -v git-filter-repo &gt;/dev/null 2&gt;<span class="p">&amp;</span>1<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;git-filter-repo still not found after installation attempts.&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Function to print help menu/usage</span>
</span></span><span class="line"><span class="cl">usage<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Usage: </span><span class="nv">$0</span><span class="s2"> [--force] \\&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;          --repo-url git@github.com:user/repo.git \\&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;          --source-email &#39;old.email@example.com&#39; \\&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;          --target-email &#39;new.email@example.com&#39; \\&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;          [--source-name &#39;Old Name&#39;] \\&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;          [--target-name &#39;New Name&#39;]&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="o">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Default vars</span>
</span></span><span class="line"><span class="cl"><span class="nv">REPO_URL</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">SRC_EMAIL</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">TGT_EMAIL</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">SRC_NAME</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">TGT_NAME</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">FORCE_PUSH</span><span class="o">=</span><span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Parse arguments</span>
</span></span><span class="line"><span class="cl"><span class="k">while</span> <span class="o">[[</span> <span class="nv">$#</span> -gt <span class="m">0</span> <span class="o">]]</span><span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  <span class="k">case</span> <span class="nv">$1</span> in
</span></span><span class="line"><span class="cl">  --repo-url<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">REPO_URL</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">shift</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  --source-email<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">SRC_EMAIL</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">shift</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  --target-email<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">TGT_EMAIL</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">shift</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  --source-name<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">SRC_NAME</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">shift</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  --target-name<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">TGT_NAME</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$2</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">shift</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  --force<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">FORCE_PUSH</span><span class="o">=</span><span class="m">1</span>
</span></span><span class="line"><span class="cl">    <span class="nb">shift</span>
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  -h <span class="p">|</span> --help<span class="o">)</span>
</span></span><span class="line"><span class="cl">    usage
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  *<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;Invalid argument: </span><span class="nv">$1</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">    usage
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  <span class="k">esac</span>
</span></span><span class="line"><span class="cl"><span class="k">done</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -z <span class="s2">&#34;</span><span class="nv">$REPO_URL</span><span class="s2">&#34;</span> <span class="o">||</span> -z <span class="s2">&#34;</span><span class="nv">$SRC_EMAIL</span><span class="s2">&#34;</span> <span class="o">||</span> -z <span class="s2">&#34;</span><span class="nv">$TGT_EMAIL</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Missing required arguments.&#34;</span>
</span></span><span class="line"><span class="cl">  usage
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Repo URL: </span><span class="nv">$REPO_URL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Replacing source email &lt;</span><span class="nv">$SRC_EMAIL</span><span class="s2">&gt; with target email &lt;</span><span class="nv">$TGT_EMAIL</span><span class="s2">&gt;&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$SRC_NAME</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Source name: </span><span class="nv">$SRC_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$TGT_NAME</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Target name: </span><span class="nv">$TGT_NAME</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Create temporary directory to clone repo into</span>
</span></span><span class="line"><span class="cl"><span class="nv">TMP_DIR</span><span class="o">=</span><span class="k">$(</span>mktemp -d<span class="k">)</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Mirror cloning repository into temporary directory: </span><span class="nv">$TMP_DIR</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">git clone --mirror <span class="s2">&#34;</span><span class="nv">$REPO_URL</span><span class="s2">&#34;</span> <span class="s2">&#34;</span><span class="nv">$TMP_DIR</span><span class="s2">/repo&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">cd</span> <span class="s2">&#34;</span><span class="nv">$TMP_DIR</span><span class="s2">/repo&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Rewriting commit history emails with git-filter-repo...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Read history with source username/email, replace with target</span>
</span></span><span class="line"><span class="cl"><span class="nv">COMMIT_CALLBACK</span><span class="o">=</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">if commit.author_email.decode(&#39;utf-8&#39;) == &#39;</span><span class="nv">$SRC_EMAIL</span><span class="s2">&#39;:
</span></span></span><span class="line"><span class="cl"><span class="s2">    commit.author_email = b&#39;</span><span class="nv">$TGT_EMAIL</span><span class="s2">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$TGT_NAME</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">COMMIT_CALLBACK</span><span class="o">+=</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    commit.author_name = b&#39;</span><span class="nv">$TGT_NAME</span><span class="s2">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">COMMIT_CALLBACK</span><span class="o">+=</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">if commit.committer_email.decode(&#39;utf-8&#39;) == &#39;</span><span class="nv">$SRC_EMAIL</span><span class="s2">&#39;:
</span></span></span><span class="line"><span class="cl"><span class="s2">    commit.committer_email = b&#39;</span><span class="nv">$TGT_EMAIL</span><span class="s2">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$TGT_NAME</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nv">COMMIT_CALLBACK</span><span class="o">+=</span><span class="s2">&#34;
</span></span></span><span class="line"><span class="cl"><span class="s2">    commit.committer_name = b&#39;</span><span class="nv">$TGT_NAME</span><span class="s2">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Generated callback:&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$COMMIT_CALLBACK</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Rewrite history</span>
</span></span><span class="line"><span class="cl">git filter-repo --force --commit-callback <span class="s2">&#34;</span><span class="nv">$COMMIT_CALLBACK</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;git filter-repo completed successfully&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Verifying first few commits after rewrite...&#34;</span>
</span></span><span class="line"><span class="cl">git log --all --pretty<span class="o">=</span>format:<span class="s2">&#34;%h %ad %an &lt;%ae&gt;&#34;</span> --date<span class="o">=</span>iso -5
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Removing backup refs...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">##  Remove all refs with old data</span>
</span></span><span class="line"><span class="cl">git <span class="k">for</span>-each-ref --format<span class="o">=</span><span class="s1">&#39;%(refname)&#39;</span> refs/original <span class="p">|</span> xargs -r git update-ref -d
</span></span><span class="line"><span class="cl">git <span class="k">for</span>-each-ref --format<span class="o">=</span><span class="s1">&#39;%(refname)&#39;</span> refs/backup <span class="p">|</span> xargs -r git update-ref -d
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Expiring reflogs and pruning unreachable objects...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Expire old data locally before pushing</span>
</span></span><span class="line"><span class="cl">git reflog expire --expire<span class="o">=</span>now --all
</span></span><span class="line"><span class="cl">git gc --prune<span class="o">=</span>now --aggressive
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Verifying no lingering commits with source email anywhere...&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Get list of commits with source email</span>
</span></span><span class="line"><span class="cl"><span class="nv">SOURCE_COMMITS</span><span class="o">=</span><span class="k">$(</span>git <span class="k">for</span>-each-ref --format<span class="o">=</span><span class="s1">&#39;%(refname)&#39;</span> <span class="p">|</span> <span class="k">while</span> <span class="nb">read</span> -r ref<span class="p">;</span> <span class="k">do</span>
</span></span><span class="line"><span class="cl">  git log <span class="s2">&#34;</span><span class="nv">$ref</span><span class="s2">&#34;</span> --pretty<span class="o">=</span>format:<span class="s2">&#34;%H%x09%ad%x09%an%x09%ae%x09%cN%x09%cE&#34;</span> --date<span class="o">=</span>iso <span class="p">|</span>
</span></span><span class="line"><span class="cl">    awk -v <span class="nv">src_email</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$SRC_EMAIL</span><span class="s2">&#34;</span> <span class="s1">&#39;
</span></span></span><span class="line"><span class="cl"><span class="s1">        $4 == src_email || $6 == src_email {
</span></span></span><span class="line"><span class="cl"><span class="s1">            print FILENAME &#34;\t&#34; $0
</span></span></span><span class="line"><span class="cl"><span class="s1">        }&#39;</span> <span class="nv">FILENAME</span><span class="o">=</span><span class="s2">&#34;</span><span class="nv">$ref</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="k">done)</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## If any commits remain with source email, exit</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="nv">$SOURCE_COMMITS</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Some commits still contain the source email &lt;</span><span class="nv">$SRC_EMAIL</span><span class="s2">&gt;:&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> -e <span class="s2">&#34;Ref\tCommit\tDate\tAuthorName\tAuthorEmail\tCommitterName\tCommitterEmail&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;</span><span class="nv">$SOURCE_COMMITS</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nb">echo</span> <span class="s2">&#34;Aborting push.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="nb">exit</span> <span class="m">2</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Removing local refs under refs/merge-requests/ to avoid push errors...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Remove all refs under refs/merge-requests/</span>
</span></span><span class="line"><span class="cl">git <span class="k">for</span>-each-ref --format<span class="o">=</span><span class="s1">&#39;%(refname)&#39;</span> refs/merge-requests <span class="p">|</span> xargs -r -n <span class="m">1</span> git update-ref -d <span class="o">||</span> <span class="nb">true</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Adding remote origin after filter-repo cleanup...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Re-add origin (git-filter-repo removes it)</span>
</span></span><span class="line"><span class="cl">git remote add origin <span class="s2">&#34;</span><span class="nv">$REPO_URL</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Pushing all branches and tags to origin forcibly...&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Push rewritten histories back up</span>
</span></span><span class="line"><span class="cl"><span class="k">if</span> <span class="o">[[</span> -n <span class="s2">&#34;</span><span class="si">${</span><span class="nv">FORCE_PUSH</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span><span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! git push origin --force --all<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Failed to push rewritten commits to origin.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! git push origin --force --tags<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Failed to push rewritten commits to origin.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! git push origin --force --all<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Failed to push rewritten commits to origin.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">if</span> ! git push origin --force --tags<span class="p">;</span> <span class="k">then</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;&#34;</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Failed to push rewritten commits to origin.&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">    <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl">  <span class="k">fi</span>
</span></span><span class="line"><span class="cl"><span class="k">fi</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Successfully rewrote all commits and pushed to remote.&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nb">echo</span> <span class="s2">&#34;Temporary repo location: </span><span class="nv">$TMP_DIR</span><span class="s2">/repo&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nb">exit</span> <span class="m">0</span>
</span></span></code></pre></td></tr></table>
</div>
</div>]]></content:encoded>
    </item>
    <item>
      <title>Host Hugo with Netlify</title>
      <link>/posts/host-with-netlify/</link>
      <pubDate>Fri, 09 Jan 2026 01:56:08 -0500</pubDate>
      <guid>/posts/host-with-netlify/</guid>
      <description>Build &amp;amp; deploy a Hugo site from a Github/Gitlab repository and host it on Netlify behind a custom domain (with optional Cloudflare).</description>
      <content:encoded><![CDATA[<p><a href="https://gohugo.com">Hugo</a> is a <a href="https://www.cloudflare.com/learning/performance/static-site-generator/">static site generator</a> that enables you to write your website&rsquo;s content as Markdown files, and render them to nice-looking websites using <a href="https://themes.gohugo.io">Hugo themes</a>. The word &ldquo;static&rdquo; in this context means that the files Hugo renders are meant to be served as-is, there is no &ldquo;backend&rdquo; for the site (unless you add custom Javascript code).</p>
<p>Static sites are perfect for many different kinds of websites, from personal blogs like this site to product sites (i.e. <a href="https://brave.com">Brave browser&rsquo;s website</a>) to an organization&rsquo;s home page (i.e. <a href="https://letsencrypt.org">the LetsEncrypt project&rsquo;s site</a>) to documentation sites (i.e. <a href="https://docs.digitalocean.com">DigitalOcean&rsquo;s documentation</a>). With a static site generator, you do not have to deal with web code like HTML or Javascript, but you still have the option of <a href="https://gohugo.io/commands/hugo_new_theme/">creating your own custom themes</a> if that appeals to you.</p>
<p>When Hugo renders your source code into a website, it outputs everything you need to start serving the site to a <code>public/</code> directory, allowing you to host the page in <a href="https://hugomods.com">Docker</a>, or on a VPS you control, or with a service like <a href="https://www.netlify.com">Netlify</a>. You could even host your site on a <a href="https://snapcraft.io/install/hugo/raspbian">Raspberry Pi you own</a> (although I would recommend looking into running the site in a Docker container, instead of a Canonical Snap). Anywhere that can serve HTML should be able to serve your compiled Hugo website.</p>
<p>This blog will run through the basic steps to initialize a Hugo project, host it on Github, and setup hosting with Netlify.</p>
<h2 id="requirements">Requirements</h2>
<ul>
<li><a href="https://gohugo.io/installation/">Hugo</a>
<ul>
<li>You should also pick a <a href="https://themes.gohugo.io">Hugo theme</a>, i.e. <a href="https://themes.gohugo.io/themes/hugo-papermod/">PaperMod</a></li>
</ul>
</li>
<li><a href="https://go.dev/doc/install">Go</a> (optional)
<ul>
<li>You can <a href="https://gohugo.io/hugo-modules/use-modules/">add themes as a Hugo module</a> instead of using Git submodules.</li>
</ul>
</li>
<li><a href="https://git-scm.org">Git</a></li>
<li><a href="https://github.com">Github account</a></li>
<li><a href="https://netlify.com">Netlify account</a></li>
</ul>
<h2 id="initialize-hugo-repository">Initialize Hugo repository</h2>
<p>There are different ways of structuring the repository, like putting the site&rsquo;s content in a <code>src/</code> directory or storing multiple Hugo sites in a single monorepo. This guide assumes you follow the <a href="https://gohugo.io/getting-started/quick-start/#create-a-site">default steps for initializing a Hugo project</a>, where all source code is in the &ldquo;root&rdquo; of the repository.</p>
<p>First, <code>cd</code> to a path where you want to initialize your Hugo site, i.e. <code>~/git</code> or just <code>~/</code> and initialize the site with:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">hugo new site &lt;your-blog-name&gt;
</span></span></code></pre></td></tr></table>
</div>
</div><p>Replace <code>&lt;your-blog-name&gt;</code> above with the name of your blog. The documentation uses <code>quickstart</code> as an example. You can name the site whatever you want, and can change it later by editing the <code>hugo.toml</code>/<code>hugo.yml</code> file the <code>hugo new site</code> command creates. When you run the Hugo command, you will see output with some first steps and instructions for editing the <code>hugo.toml</code> to change the site&rsquo;s configuration:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">$&gt; hugo new site your-site-name
</span></span><span class="line"><span class="cl">Congratulations! Your new Hugo site was created in /home/username/git/your-site-name.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">Just a few more steps...
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">1. Change the current directory to /home/username/git/your-site-name.
</span></span><span class="line"><span class="cl">2. Create or install a theme:
</span></span><span class="line"><span class="cl">   - Create a new theme with the <span class="nb">command</span> <span class="s2">&#34;hugo new theme &lt;THEMENAME&gt;&#34;</span>
</span></span><span class="line"><span class="cl">   - Or, install a theme from https://themes.gohugo.io/
</span></span><span class="line"><span class="cl">3. Edit hugo.toml, setting the <span class="s2">&#34;theme&#34;</span> property to the theme name.
</span></span><span class="line"><span class="cl">4. Create new content with the <span class="nb">command</span> <span class="s2">&#34;hugo new content &lt;SECTIONNAME&gt;/&lt;FILENAME&gt;.&lt;FORMAT&gt;&#34;</span>.
</span></span><span class="line"><span class="cl">5. Start the embedded web server with the <span class="nb">command</span> <span class="s2">&#34;hugo server --buildDrafts&#34;</span>.
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">See documentation at https://gohugo.io/.
</span></span></code></pre></td></tr></table>
</div>
</div><p>This is an example of the files <code>hugo new site</code> creates:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">your-site-name/
</span></span><span class="line"><span class="cl">├── themes
</span></span><span class="line"><span class="cl">├── static
</span></span><span class="line"><span class="cl">├── layouts
</span></span><span class="line"><span class="cl">├── i18n
</span></span><span class="line"><span class="cl">├── hugo.toml
</span></span><span class="line"><span class="cl">├── data
</span></span><span class="line"><span class="cl">├── content
</span></span><span class="line"><span class="cl">├── assets
</span></span><span class="line"><span class="cl">└── archetypes
</span></span><span class="line"><span class="cl">    └── default.md
</span></span></code></pre></td></tr></table>
</div>
</div><p>Since this blog post is not meant to be an in-depth tutorial of how to build a website with Hugo, we will not go into detail on every single file and folder this command creates, but some important things to know are:</p>
<ul>
<li>The <code>themes/</code> directory is where you store themes added as Git submodules.
<ul>
<li>This guide assumes you are using Hugo modules to install themes, instead of the old Git submodule way, so you will not need to interact with the <code>themes/</code> directory.</li>
<li>This is also where you would store a custom theme, if you wanted to create your own Hugo theme for your site.</li>
</ul>
</li>
<li>The <code>static/</code> directory is where you would store static assets for your site, including images and a <code>favicon.ico</code> for the site.
<ul>
<li>Although counter-intuitive, it is generally considered best practice to include the binaries for your static resources in the Git repository.</li>
<li>Save your <code>favicon.ico</code> to <code>static/favicon.ico</code> (if you have one), and any images you use in your posts in <code>static/img</code> (this directory doesn&rsquo;t exist by default, you have to create it).</li>
</ul>
</li>
<li>The <code>layouts/</code> directory is where you can create <a href="https://gohugo.io/content-management/data-sources/">Hugo templates</a> for finer-grained control of how content is displayed on your site.</li>
</ul>
<p>The <code>hugo.toml</code> file is the main configuration file for your site. If you prefer YAML, you can copy the contents of <code>hugo.toml</code> into <a href="https://www.convertsimple.com/convert-toml-to-yaml/">convertsimple.com</a>, delete or rename <code>hugo.toml</code> to <code>hugo.yaml</code>/<code>hugo.yml</code>, and paste the converted configuration into the YAML file. Hugo will detect a file named <code>hugo.yml</code> or <code>hugo.toml</code> wherever you run <code>hugo</code> commands from. This guide assumes you are using YAML for your configuration.</p>
<style type="text/css">
     
    .notice {
        --title-color: #fff;
        --title-background-color: #6be;
        --content-color: #444;
        --content-background-color: #e7f2fa;
    }

    .notice.info {
        --title-background-color: #fb7;
        --content-background-color: #fec;
    }

    .notice.tip {
        --title-background-color: #5a5;
        --content-background-color: #efe;
    }

    .notice.warning {
        --title-background-color: #c33;
        --content-background-color: #fee;
    }

     
    @media (prefers-color-scheme:dark) {
        .notice {
            --title-color: #fff;
            --title-background-color: #069;
            --content-color: #ddd;
            --content-background-color: #023;
        }

        .notice.info {
            --title-background-color: #a50;
            --content-background-color: #420;
        }

        .notice.tip {
            --title-background-color: #363;
            --content-background-color: #121;
        }

        .notice.warning {
            --title-background-color: #800;
            --content-background-color: #400;
        }
    }

    body.dark .notice {
        --title-color: #fff;
        --title-background-color: #069;
        --content-color: #ddd;
        --content-background-color: #023;
    }

    body.dark .notice.info {
        --title-background-color: #a50;
        --content-background-color: #420;
    }

    body.dark .notice.tip {
        --title-background-color: #363;
        --content-background-color: #121;
    }

    body.dark .notice.warning {
        --title-background-color: #800;
        --content-background-color: #400;
    }

     
    .notice {
        padding: 18px;
        line-height: 24px;
        margin-bottom: 24px;
        border-radius: 4px;
        color: var(--content-color);
        background: var(--content-background-color);
    }

    .notice p:last-child {
        margin-bottom: 0
    }

     
    .notice-title {
        margin: -18px -18px 12px;
        padding: 4px 18px;
        border-radius: 4px 4px 0 0;
        font-weight: 700;
        color: var(--title-color);
        background: var(--title-background-color);
    }

     
    .icon-notice {
        display: inline-flex;
        align-self: center;
        margin-right: 8px;
    }

    .icon-notice img,
    .icon-notice svg {
        height: 1em;
        width: 1em;
        fill: currentColor;
    }

    .icon-notice img,
    .icon-notice.baseline svg {
        top: .125em;
        position: relative;
    }
</style><div class="notice tip" >
    <p class="notice-title">
        <span class="icon-notice baseline">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="300.5 134 300 300">
  <path d="M551.281 252.36c0-3.32-1.172-6.641-3.515-8.985l-17.774-17.578c-2.344-2.344-5.469-3.711-8.789-3.711-3.32 0-6.445 1.367-8.789 3.71l-79.687 79.493-44.141-44.14c-2.344-2.344-5.469-3.712-8.79-3.712-3.32 0-6.444 1.368-8.788 3.711l-17.774 17.579c-2.343 2.343-3.515 5.664-3.515 8.984 0 3.32 1.172 6.445 3.515 8.789l70.704 70.703c2.343 2.344 5.664 3.711 8.789 3.711 3.32 0 6.64-1.367 8.984-3.71l106.055-106.056c2.343-2.343 3.515-5.468 3.515-8.789ZM600.5 284c0 82.813-67.188 150-150 150-82.813 0-150-67.188-150-150 0-82.813 67.188-150 150-150 82.813 0 150 67.188 150 150Z"/>
</svg>

        </span> Tip </p><p>You can initialize Hugo with a YAML config file instead of TOML by running the  <code>hugo new site</code> command with a <code>--format yaml</code> flag:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">hugo new site site-name --format yaml
</span></span></code></pre></td></tr></table>
</div>
</div></div>

<h2 id="convert-site-to-hugo-module">Convert Site to Hugo Module</h2>
<p>Next, initialize your site as a <a href="https://gohugo.io/commands/hugo_mod_init/">Hugo module</a>:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">hugo mod init github.com/username/your-site-name
</span></span></code></pre></td></tr></table>
</div>
</div><p>After running this, you will see a <code>go.mod</code> file. This allows Go to mannage your site so you can install themes like they are Go packages. For example, to install the <code>PaperMod</code> theme by editing your <code>hugo.yml</code> file and adding the following block of code:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">module</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">imports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">github.com/adityatelange/hugo-PaperMod</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>The full <code>hugo.yml</code> should now look like this:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span><span class="lnt">6
</span><span class="lnt">7
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nt">baseURL</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;https://example.org/&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">languageCode</span><span class="p">:</span><span class="w"> </span><span class="l">en-us</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">title</span><span class="p">:</span><span class="w"> </span><span class="l">My New Hugo Site</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">module</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">imports</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">path</span><span class="p">:</span><span class="w"> </span><span class="l">github.com/adityatelange/hugo-PaperMod</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><div class="notice note" >
    <p class="notice-title">
        <span class="icon-notice baseline">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 128 300 300">
  <path d="M150 128c82.813 0 150 67.188 150 150 0 82.813-67.188 150-150 150C67.187 428 0 360.812 0 278c0-82.813 67.188-150 150-150Zm25 243.555v-37.11c0-3.515-2.734-6.445-6.055-6.445h-37.5c-3.515 0-6.445 2.93-6.445 6.445v37.11c0 3.515 2.93 6.445 6.445 6.445h37.5c3.32 0 6.055-2.93 6.055-6.445Zm-.39-67.188 3.515-121.289c0-1.367-.586-2.734-1.953-3.516-1.172-.976-2.93-1.562-4.688-1.562h-42.968c-1.758 0-3.516.586-4.688 1.563-1.367.78-1.953 2.148-1.953 3.515l3.32 121.29c0 2.734 2.93 4.882 6.64 4.882h36.134c3.515 0 6.445-2.148 6.64-4.883Z"/>
</svg>

        </span> Note </p><p>Run <code>hugo mod tidy</code>; this will download the theme(s)/module(s) you declare, remove old versions, and update your <code>go.mod</code> file for you.</p></div>

<h2 id="run-local-hugo-development-server">Run Local Hugo Development Server</h2>
<p>Now you can test running the server with <code>hugo serve</code>, which will compile the site and start serving it at <code>http://localhost:1313/</code> by default. To change the host address, i.e. to access it from another machine on the network, use <code>--bind 0.0.0.0</code>, and to change the port use <code>-p &lt;port&gt;</code>. This is Hugo&rsquo;s development server. As you edit files, the server will &lsquo;hot reload,&rsquo; re-compiling the Markdown you write and restarting the server.</p>
<p>Hugo does this <em>very</em> quickly. Each page takes mere milliseconds to render, even those with images or a lot of text, and it caches between rebuilds, only re-rendering the new content. This leads to a pleasant &ldquo;change something and see it instantly&rdquo; writing experience.</p>
<p>If you start the server with <code>-D</code>, Hugo will also render your drafts in the development server.</p>
<h2 id="git-repository-setup">Git Repository Setup</h2>
<p>Run <code>git init -b main</code> in your Hugo site&rsquo;s directory to initialize a new Git repository. Before committing any code, add a <code>.gitignore</code> that ignores Hugo&rsquo;s <code>public/</code> and <code>resources/</code> directories that it creates when you run the development server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-plaintext" data-lang="plaintext"><span class="line"><span class="cl">/public/
</span></span><span class="line"><span class="cl">/resources/
</span></span></code></pre></td></tr></table>
</div>
</div><p>Then, do your initial commit:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git add *
</span></span><span class="line"><span class="cl">git commit -m <span class="s2">&#34;Initial commit&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>Create a repository on Github for your Hugo site. This should match the URL you used when you create the Hugo module with <code>hugo mod init github.com/user/your-site-name</code>. Add the remote to your local repository:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">git remote <span class="nb">set</span> origin git@github.com:username/your-site-name.git
</span></span></code></pre></td></tr></table>
</div>
</div><p>And push your code with <code>git push -u origin main</code>.</p>
<h2 id="netlify-setup">Netlify Setup</h2>
<p>Create an account on <a href="https://netlify.com">Netlify</a>. During the setup process, you will be asked to create a project. Allow Netlify to integrate with your Github and find the repository with your Hugo site. If you already have a Netlify site, add a new project and use &ldquo;Import an existing project&rdquo; and find your Hugo repository.</p>
<p>When you are prompted to input build settings like the base directory/package directory, the build command, the publish directory, etc, leave all of these blank. You can optionally leave the functions directory as <code>netlify/functions</code>. If you set values here, the site will fail to build because the <code>hugo.yml</code> interferes with these settings. Do your site configuration in <code>hugo.yml</code>, not Netlify.</p>
<p>When asked which branch to trigger Netlify deploys on, you have a decision to make: do you want your site to deploy every single time you open a PR to the main branch, including times where you are doing small cleanup chores like updating the <code>.gitignore</code>, or setting up a new pipeline? Or do you want to have more control over deploys, i.e. only when merging into a branch named <code>netlify</code>?</p>
<p>I chose the latter, and created a <code>netlify</code> branch, then configured Netlify to deploy from the <code>netlify</code> branch instead of <code>main</code>. I also turned off deployment previews, so any PR into <code>netlify</code> will trigger a deploy.</p>
<h3 id="netlifytoml-config">netlify.toml Config</h3>
<p>After finishing the setup, create a <code>netlify.toml</code> file in the root of your Hugo repository :</p>
<div class="notice note" >
    <p class="notice-title">
        <span class="icon-notice baseline">
            <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 128 300 300">
  <path d="M150 128c82.813 0 150 67.188 150 150 0 82.813-67.188 150-150 150C67.187 428 0 360.812 0 278c0-82.813 67.188-150 150-150Zm25 243.555v-37.11c0-3.515-2.734-6.445-6.055-6.445h-37.5c-3.515 0-6.445 2.93-6.445 6.445v37.11c0 3.515 2.93 6.445 6.445 6.445h37.5c3.32 0 6.055-2.93 6.055-6.445Zm-.39-67.188 3.515-121.289c0-1.367-.586-2.734-1.953-3.516-1.172-.976-2.93-1.562-4.688-1.562h-42.968c-1.758 0-3.516.586-4.688 1.563-1.367.78-1.953 2.148-1.953 3.515l3.32 121.29c0 2.734 2.93 4.882 6.64 4.882h36.134c3.515 0 6.445-2.148 6.64-4.883Z"/>
</svg>

        </span> Note </p><p>If you have a domain name, you can set it in <code>HUGO_BASEURL</code> instead of the Netlify app URL. Also, make sure to check the current versions of <a href="https://github.com/gohugoio/hugo/releases">Hugo</a> and <a href="https://nodejs.org/en/download/current">Node</a>. for NPM versions, just put the major version number, i.e. <code>24</code>, <code>25</code>, etc.</p></div>

<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-toml" data-lang="toml"><span class="line"><span class="cl"><span class="p">[</span><span class="nx">build</span><span class="p">.</span><span class="nx">environment</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">HUGO_BASEURL</span> <span class="p">=</span> <span class="s2">&#34;https://netlify-project-name.netlify.app&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">HUGO_VERSION</span> <span class="p">=</span> <span class="s2">&#34;0.152.2&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">HUGO_ENV</span> <span class="p">=</span> <span class="s2">&#34;production&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">NODE_VERSION</span> <span class="p">=</span> <span class="s2">&#34;24&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">HUGO_ENABLEGITINFO</span> <span class="p">=</span> <span class="s2">&#34;true&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="p">[</span><span class="nx">build</span><span class="p">]</span>
</span></span><span class="line"><span class="cl"><span class="nx">command</span> <span class="p">=</span> <span class="s2">&#34;hugo --gc --minify&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nx">publish</span> <span class="p">=</span> <span class="s2">&#34;public&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>This file tells Netlify how to build your Hugo site. Commit it to git with <code>git add * &amp;&amp; git commit -m &quot;Add netlify config&quot;</code> and push to main with <code>git push</code>. Even better, create a new branch before adding/committing your code, i.e. <code>git switch -c feat/netlify-setup</code>, then add the files and commit message. When you push, use <code>git push -u origin feat/netlify-setup</code>. Merge the branch into the <code>main</code> branch in Github.</p>
<h2 id="example-production-hugo-dockerfile">Example Production Hugo Dockerfile</h2>
<p>If you plan to host your Hugo site yourself, you can set up a Dockerfile to build your site and serve it behind a proxy like <a href="https://caddyserver.com">Caddy</a> or <a href="https://nginx.org/en/">NGINX</a>.</p>
<p>You can use a multi-stage Docker build to have a smaller final image that only has your rendered HTML and the Caddy server to serve it:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-dockerfile" data-lang="dockerfile"><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">hugomods/hugo:exts</span><span class="w"> </span><span class="k">AS</span><span class="w"> </span><span class="s">builder</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">WORKDIR</span><span class="w"> </span><span class="s">/src</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="c">## Copy Hugo site files &amp; config</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> archetypes/ archetypes/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> assets/ assets/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> content/ content/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> layouts/ layouts/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> static/ static/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> hugo.yml go.mod go.sum .hugo_build.lock ./<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">ARG</span> <span class="nv">HUGO_BASEURL</span><span class="o">=</span>http://localhost/<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">RUN</span> hugo <span class="se">\
</span></span></span><span class="line"><span class="cl">  --minify <span class="se">\
</span></span></span><span class="line"><span class="cl">  --gc <span class="se">\
</span></span></span><span class="line"><span class="cl">  --baseURL<span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">HUGO_BASEURL</span><span class="si">}</span><span class="s2">&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="cl">  --destination /output/public<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">FROM</span><span class="w"> </span><span class="s">caddy:alpine</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> --from<span class="o">=</span>builder /output/public /usr/share/caddy<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">COPY</span> ./Caddyfile /etc/caddy/Caddyfile<span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">EXPOSE</span><span class="w"> </span><span class="s">80</span> <span class="m">443</span><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="err">
</span></span></span><span class="line"><span class="cl"><span class="k">CMD</span> <span class="p">[</span><span class="s2">&#34;caddy&#34;</span><span class="p">,</span> <span class="s2">&#34;run&#34;</span><span class="p">,</span> <span class="s2">&#34;--config&#34;</span><span class="p">,</span> <span class="s2">&#34;/etc/caddy/Caddyfile&#34;</span><span class="p">]</span><span class="err">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Create a <code>Caddyfile</code> the container will use to configure the Caddy server:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-caddyfile" data-lang="caddyfile"><span class="line"><span class="cl"><span class="p">{</span><span class="c1">
</span></span></span><span class="line"><span class="cl"><span class="c1">  # debug
</span></span></span><span class="line"><span class="cl">  <span class="k">email</span> <span class="se">{$CADDY_EMAIL}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="se">{$CADDY_SITE_ADDRESS}</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">  <span class="k">root</span> <span class="nd">*</span> <span class="s">/usr/share/caddy</span>
</span></span><span class="line"><span class="cl">  <span class="k">file_server</span>
</span></span><span class="line"><span class="cl">  <span class="k">encode</span> <span class="s">gzip</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl">  <span class="k">header</span> <span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">X-Content-Type-Options</span> <span class="s">nosniff</span>
</span></span><span class="line"><span class="cl">    <span class="k">X-Frame-Options</span> <span class="s">DENY</span>
</span></span><span class="line"><span class="cl">  <span class="p">}</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>When you build the container, you will need to set environment variables for <code>CADDY_EMAIL</code> (for automated LetsEncrypt SSL certificates) and <code>CADDY_SITE_ADDRESS</code> (for handling connections coming to the Caddy URL). The 2 env vars are <a href="https://docs.docker.com/build/building/variables/">&ldquo;build arguments&rdquo;</a>, which you can inject in your <code>docker build</code> command using <code>--build-arg ARG_NAME=value</code>. For example, to build this Dockerfile:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl">docker build --build-arg <span class="nv">CADDY_EMAIL</span><span class="o">=</span><span class="s2">&#34;youremail@address.com&#34;</span> --build-arg <span class="nv">CADDY_SITE_ADDRESS</span><span class="o">=</span><span class="s2">&#34;myblog.com&#34;</span> .
</span></span></code></pre></td></tr></table>
</div>
</div><h2 id="check-links-with-lychee">Check Links with Lychee</h2>
<p><a href="https://github.com/lycheeverse/lychee">Lychee</a> is a <em>fast</em> link checker you can use to check your site for broken links. You can install it locally and point it at your live site, or you can run it against the <code>public/</code> directory after Hugo builds to check your links before deploying.</p>
<p>Lychee&rsquo;s syntax is simple:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt">1
</span><span class="lnt">2
</span><span class="lnt">3
</span><span class="lnt">4
</span><span class="lnt">5
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-shell" data-lang="shell"><span class="line"><span class="cl"><span class="c1">## Check links on your live site</span>
</span></span><span class="line"><span class="cl">lychee https://your-site-name.com
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="c1">## Check links in the public/ directory after a Hugo build</span>
</span></span><span class="line"><span class="cl">lychee public/
</span></span></code></pre></td></tr></table>
</div>
</div><p>As a Github action:</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;Test Blog Links&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">## Allow manual runs</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">workflow_dispatch</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="c">## Every 6 hours</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">schedule</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span>- <span class="nt">cron</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0 */6 * * *&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">concurrency</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span><span class="nt">group</span><span class="p">:</span><span class="w"> </span><span class="l">${{ github.workflow }}-${{ github.ref }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Test live site links</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">lycheeverse/lychee-action@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="p">&gt;-</span><span class="sd">
</span></span></span><span class="line"><span class="cl"><span class="sd">            --base-verification false 
</span></span></span><span class="line"><span class="cl"><span class="sd">            --no-progress 
</span></span></span><span class="line"><span class="cl"><span class="sd">            &#34;${{ vars.HUGO_BASEURL }}&#34;/*</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><p>Or in a full pipeline that builds the site and then tests the <code>public/</code> directory (this is useful to do before deploying):</p>
<div class="highlight"><div class="chroma">
<table class="lntable"><tr><td class="lntd">
<pre tabindex="0" class="chroma"><code><span class="lnt"> 1
</span><span class="lnt"> 2
</span><span class="lnt"> 3
</span><span class="lnt"> 4
</span><span class="lnt"> 5
</span><span class="lnt"> 6
</span><span class="lnt"> 7
</span><span class="lnt"> 8
</span><span class="lnt"> 9
</span><span class="lnt">10
</span><span class="lnt">11
</span><span class="lnt">12
</span><span class="lnt">13
</span><span class="lnt">14
</span><span class="lnt">15
</span><span class="lnt">16
</span><span class="lnt">17
</span><span class="lnt">18
</span><span class="lnt">19
</span><span class="lnt">20
</span><span class="lnt">21
</span><span class="lnt">22
</span><span class="lnt">23
</span><span class="lnt">24
</span><span class="lnt">25
</span><span class="lnt">26
</span><span class="lnt">27
</span><span class="lnt">28
</span><span class="lnt">29
</span><span class="lnt">30
</span><span class="lnt">31
</span><span class="lnt">32
</span><span class="lnt">33
</span><span class="lnt">34
</span><span class="lnt">35
</span><span class="lnt">36
</span><span class="lnt">37
</span><span class="lnt">38
</span><span class="lnt">39
</span><span class="lnt">40
</span><span class="lnt">41
</span><span class="lnt">42
</span><span class="lnt">43
</span><span class="lnt">44
</span><span class="lnt">45
</span></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;PR checks&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">on</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull_request</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">branches</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">netlify]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">types</span><span class="p">:</span><span class="w"> </span><span class="p">[</span><span class="l">opened, synchronize, reopened]</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">permissions</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">contents</span><span class="p">:</span><span class="w"> </span><span class="l">read</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">pull-requests</span><span class="p">:</span><span class="w"> </span><span class="l">write</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">env</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">HUGO_VERSION</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;0.154.3&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">jobs</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">  </span><span class="nt">test</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build &amp; test Hugo site</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">runs-on</span><span class="p">:</span><span class="w"> </span><span class="l">ubuntu-latest</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">    </span><span class="nt">steps</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Checkout</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">actions/checkout@v4</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">fetch-depth</span><span class="p">:</span><span class="w"> </span><span class="m">0</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Vale prose linting</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">errata-ai/vale-action@v2.1.1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">files</span><span class="p">:</span><span class="w"> </span><span class="s1">&#39;[&#34;content/**.{md}&#34;]&#39;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Setup Hugo</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">peaceiris/actions-hugo@v3</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">hugo-version</span><span class="p">:</span><span class="w"> </span><span class="l">${{ env.HUGO_VERSION }}</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">extended</span><span class="p">:</span><span class="w"> </span><span class="kc">true</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Build site</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">run</span><span class="p">:</span><span class="w"> </span><span class="l">hugo --gc --minify</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">      </span>- <span class="nt">name</span><span class="p">:</span><span class="w"> </span><span class="l">Check links with Lychee</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">uses</span><span class="p">:</span><span class="w"> </span><span class="l">lycheeverse/lychee-action@v1</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">        </span><span class="nt">with</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">args</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;public/**/*.html --base .&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="w">          </span><span class="nt">fail</span><span class="p">:</span><span class="w"> </span><span class="kc">false</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div><h2 id="closing">Closing</h2>
<p>Building a blog with Hugo is fun, and relatively quick and easy to get up and running. You can build a complex site for your product, or a simple blog you update a couple times a year, all with the same tooling. The minimal amount of configuration you need to get started lends itself to quick development, while still allowing you to build up <a href="https://gohugo.io/configuration/">a more complex configuration over time</a>. Hugo modules make it even easier to install themes and site extensions, and the ability to host the site anywhere that can serve static web files gives you a ton of flexibility to where you put the site online.</p>
<p>This guide did not go in-depth on configuring and customizing Hugo, nor did it walk through creating content for the site. The <a href="https://gohugo.io/getting-started/">Hugo quickstart guide</a> will do a better job of walking you through building your first blog, and I highly recommend keeping this site open and using the search bar at the top as you&rsquo;re learning to find new ways of using Hugo.</p>
]]></content:encoded>
    </item>
  </channel>
</rss>
