<?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>Techobyte Blog</title>
    <link>/</link>
    <description>Recent content on Techobyte Blog</description>
    <generator>Hugo</generator>
    <language>en-US</language>
    <lastBuildDate>Sun, 02 Aug 2026 00:00:00 -0400</lastBuildDate>
    <atom:link href="/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>Leaving Google Photos</title>
      <link>/posts/leaving-google-photos/</link>
      <pubDate>Sun, 19 Jul 2026 00:00:00 -0400</pubDate>
      <guid>/posts/leaving-google-photos/</guid>
      <description>&lt;p&gt;I have been using Google Photos to backup my pictures since it was released in May of 2015. The software works great, seamlessly backing up any picture or video I take on my phone and making it simple to share photos and albums with my friends.&lt;/p&gt;
&lt;p&gt;Like many people, I am trying to reduce my reliance on Google products. I have not gone as hard as some people, like the users of the &lt;a href=&#34;https://reddit.com/r/degoogle&#34;&gt;DeGoogle subreddit&lt;/a&gt;, although I admire their efforts and agree with their philosophy. I have so, so many things tied to the Google/Gmail account I created in 2005. A younger me had no concept of being the product when a service is free, and I foolishly tied so much of my online life to my Gmail account. Extricating is a long, difficult process, and one I&amp;rsquo;m doing in baby steps.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>I have been using Google Photos to backup my pictures since it was released in May of 2015. The software works great, seamlessly backing up any picture or video I take on my phone and making it simple to share photos and albums with my friends.</p>
<p>Like many people, I am trying to reduce my reliance on Google products. I have not gone as hard as some people, like the users of the <a href="https://reddit.com/r/degoogle">DeGoogle subreddit</a>, although I admire their efforts and agree with their philosophy. I have so, so many things tied to the Google/Gmail account I created in 2005. A younger me had no concept of being the product when a service is free, and I foolishly tied so much of my online life to my Gmail account. Extricating is a long, difficult process, and one I&rsquo;m doing in baby steps.</p>
<p>The AI craze has been just the push I needed to take my privacy more seriously. Companies are changing their Terms of Service, shoving AI hamfistedly into every corner of their product suite, trampling privacy and protection, and will not face any consequences for it in the foreseeable future. While it is too late to keep anything I&rsquo;ve currently handed over to another entity to hold (like my pictures and personal information), I can limit further damage by pulling my most sensitive pieces of data back under my own control, and my photos felt like a good starting point.</p>
<h2 id="reasons-for-leaving">Reasons for Leaving</h2>
<p><a href="https://support.google.com/photos/answer/15344015?sjid=10263950563787648465-NC">Google Docs: Gemini features in Photos privacy hub</a> states they do not use your photos to train, but the capability is there and I don&rsquo;t trust them to resist temptation. And whether or not they directly access my photos, enough of my life is wrapped up in Google&rsquo;s products that I don&rsquo;t believe they&rsquo;d even need direct access to my pictures to abuse my privacy.</p>
<p>There are also horror stories, like the one where <a href="https://www.pcworld.com/article/3104521/teenagers-gemini-mistake-locks-entire-family-out-of-google-accounts.html">a whole house/family lost access to their Google accounts because of 1 user&rsquo;s data</a>. There is also the fact that <a href="https://www.forbes.com/sites/zakdoffman/2025/02/28/google-starts-scanning-your-photos-without-any-warning/">Google quietly enabled photo scanning on user devices, and made it opt-out by default</a>, an evil pattern we did not react strongly enough to a decade ago.</p>
<p>Additionally, you could potentially lose access to your account simply because <a href="https://www.koffellaw.com/blog/google-ai-technology-flags-dad-who-took-photos-o/">Google decides your pictures are dangerous, without a care for context and with no recourse</a>.</p>
<p>With how sensitive my Google account is, and with how much I currently rely on them, anything I can do to pull my data back into my own control is worth the effort. I researched a few products (hosted and self-hosted, free and paid), and while I did find some promising options, I ultimately settled on 1 self hosted piece of software and a backup plan that I control.</p>
<h2 id="alternatives-to-google-photos">Alternatives to Google Photos</h2>
<p>I started by looking into other hosted options, and the only one I would feel comfortable using is <a href="https://ente.com">Ente photos</a>. Ente is an open source company with a strong focus on privacy. They offer 10GB of free hosting (which is nowhere near enough for my library, but is a generous free tier), and the photos are end-to-end encrypted, meaning they can&rsquo;t see my photos even if they want to. I am comfortable with that level of control over my own data while hosting it on someone else&rsquo;s servers. I use some of Ente&rsquo;s other products and I think they do great work, and for someone who does not want to go through the effort of hosting it themselves, I think Ente is one of the best, most reasonably-priced alternatives to Google Photos there is.</p>
<p>I also evaluated 2 self-hosted solutions: <a href="https://www.photoprism.app">PhotoPrism</a> and <a href="https://immich.app">Immich</a>. <a href="https://meichthys.github.io/foss_photo_libraries/">This comparison between self-hosted photo apps</a> came in handy while I was looking over alternatives.</p>
<p>PhotoPrism is more intended for a single user, and for automated/(local) AI-assisted operations on your photos. It has great reviews, it&rsquo;s been around for a little while and has many users, and setup with Docker looks relatively simple.</p>
<p>Immich can perform many of the same functions as PhotoPrism, automatically recognizing faces and locations, tagging things, sorting by location, etc, but it&rsquo;s also geared more towards multiple users. And while I am currently the only user of my photo suite, the potential to allow family or friends to host their photos in my library is appealing.</p>
<h2 id="the-winner-immich">The winner: Immich</h2>
<p><img alt="Immich logo" loading="lazy" src="/posts/leaving-google-photos/immich-logo-stacked-light.svg#center"></p>
<p>Immich is backed by <a href="https://futo.tech">Futo</a>, a company that makes local, privacy-respecting AI tools. They have a <a href="https://github.com/immich-app/immich#features">host of features I care about</a>, and their <a href="https://play.google.com/store/apps/details?id=app.alextran.immich">Android app</a> works great.</p>
<p>Moving my photos out of Google Photos and into Immich was also relatively painless. Below I detail the steps in detail, but I was able to use Google Takeout to export my photos, and the <a href="https://github.com/simulot/immich-go"><code>immich-go</code> CLI</a> to import them into my Immich server. After that, I merely had to install the Immich app, sign into my server, and turn on backups from my phone.</p>
<h2 id="moving-to-immich">Moving to Immich</h2>
<p>I created a <a href="https://github.com/redjax/docker_templates/tree/main/templates/media/docker_immich">Docker Compose template</a> to containerize the application. On a machine where I want to run my Immich stack, I use git&rsquo;s <a href="https://git-scm.com/docs/git-sparse-checkout">sparse checkout feature</a> to download only the <code>media/docker_immich</code> directory:</p>
<ul>
<li>First, clone the repository without checking out a branch:
<ul>
<li><code>git clone --no-checkout https://github.com/redjax/docker_templates docker_immich</code></li>
<li><code>cd docker_immich</code></li>
</ul>
</li>
<li>Initialize the sparse checkout: <code>git sparse-checkout init --cone</code></li>
<li>Set the checkout path: <code>git sparse-checkout set templates/media/docker_immich</code></li>
<li>Checkout the main branch: <code>git checkout main</code></li>
</ul>
<p>Bring the whole stack up 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><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></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 compose <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f compose.yml <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f overlays/postgres.yml <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f overlays/redis.yml <span class="se">\
</span></span></span><span class="line"><span class="cl">  -f overlays/caddy.yml <span class="se">\
</span></span></span><span class="line"><span class="cl">  up -d
</span></span></code></pre></td></tr></table>
</div>
</div><p>The <code>-f overlays/service-name.yml</code> syntax is for <a href="https://docs.docker.com/compose/how-tos/multiple-compose-files/merge/">merging Docker Compose files</a>. Running <code>docker compose &lt;command&gt;</code> without any additional <code>-f</code> paths runs only the services in <code>compose.yml</code>. These overlays provide Immich with a database, web server, and cache. If I am using a database or redis cache on another host, I would omit the <code>-f overlays/{postgres,redis}.yml</code> files and provide connection details for the remote.</p>
<h3 id="export-google-photos">Export Google Photos</h3>
<p><img alt="Google Takeout logo" loading="lazy" src="/posts/leaving-google-photos/google-takeout-img.png#center"></p>
<p>Google offers a feature for exporting your Google Photos (and more account/app data) with <a href="https://takeout.google.com">Google Takeout</a>. Using this feature, I exported ~200GB of photos and videos I&rsquo;ve taken over the years into a .zip archive.</p>
<p><img alt="Google Takeout photos" loading="lazy" src="/posts/leaving-google-photos/google-takeout-selected.png#center"></p>
<h3 id="networking">Networking</h3>
<p>I use <a href="https://github.com/fosrl/pangolin">Pangolin</a> as a reverse proxy for my services. This allows me to route web traffic incoming to <code>https://immich.mydomain.com</code> to the Immich container running on a machine in my homelab. When protecting my site with <a href="https://docs.pangolin.net/manage/resources/public/authentication">Pangolin&rsquo;s SSO authentication</a>, I found I could not reach the URL when using the <a href="https://docs.immich.app/features/mobile-app/">Immich Android app</a>. I found <a href="https://docs.pangolin.net/manage/resources/public/authentication">this answer on Github</a> that shows the custom request headers required to enable <a href="https://docs.pangolin.net/manage/resources/public/authentication#shareable-links-and-access-tokens">passing an access token</a>.</p>
<p>After setting up Immich as an HTTPS resource in Pangolin, create a shareable link and set the new Immich service as the resource. Set this link to &ldquo;never expire&rdquo; for convenience; if you set an expiration date, you will need to update the app with a new link each time it expires.</p>
<p><img alt="Immich create shareable link" loading="lazy" src="/posts/leaving-google-photos/immich-create-shareable-link.png#center"></p>
<p>On the next screen, scroll past the QR code and copy the link below it. This is the link you will provide to the Android app. Click the <code>Usage Examples</code> button and note the <code>P-Access-Token-Id</code> and <code>P-Access-Token</code> values.</p>
<p><img alt="Immich shareable link access token" loading="lazy" src="/posts/leaving-google-photos/immich-shareable-link-access-token.png#center"></p>
<p>You will use this link and the access token to allow the Immich app to communicate with the server behind the Pangolin proxy.</p>
<h3 id="setup-android-app">Setup Android App</h3>
<p>In the Immich Android app, navigate to Settings &gt; Advanced &gt; Custom proxy headers, and click &ldquo;Add new header.&rdquo; Name the header <code>P-Access-Token-Id</code> and paste the value from when you created the token. Create another header named <code>P-Access-Token</code>.</p>
<p><img alt="Immich proxy headers" loading="lazy" src="/posts/leaving-google-photos/immich-android-proxy-headers.png#center"></p>
<p>This setup authenticates requests from the Android app to the Pangolin proxy, then passes the traffic to the Immich server.</p>
<h3 id="upload-photos">Upload Photos</h3>
<p>I uploaded the collection I exported with Google Takeout to Immich using the <code>immich-go</code> CLI tool. First, I had to create an API key in Immich with the following permissions:</p>
<ul>
<li><code>asset.read</code></li>
<li><code>asset.statistics</code></li>
<li><code>asset.update</code></li>
<li><code>asset.upload</code></li>
<li><code>asset.copy</code></li>
<li><code>asset.replace</code></li>
<li><code>asset.delete</code></li>
<li><code>asset.download</code></li>
<li><code>album.create</code></li>
<li><code>album.read</code></li>
<li><code>albumAsset.create</code></li>
<li><code>server.about</code></li>
<li><code>stack.create</code></li>
<li><code>tag.asset</code></li>
<li><code>tag.create</code></li>
<li><code>user.read</code></li>
<li><code>job.create</code></li>
<li><code>job.read</code></li>
</ul>
<p>I also created an admin key in Immich from the administrator account, with full permissions. The <code>immich-go</code> CLI uses the admin token to pause Immich jobs during upload. Othewrise, the regular API key will be used for operations. I created a <code>.env</code> file to store these values:</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-plaintext" data-lang="plaintext"><span class="line"><span class="cl">export IMMICH_SERVER_URL=https://photos.mydomain.com
</span></span><span class="line"><span class="cl">export IMMICH_KEY=&lt;Immich user key&gt;
</span></span><span class="line"><span class="cl">export IMMICH_ADMIN_KEY=&lt;Immich admin key&gt;
</span></span><span class="line"><span class="cl">export IMMICH_LOCAL_PHOTOS=&#34;/path/to/google-takeout/Takeout/Google Photos/&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><p>I wrote a script so I could repeat this upload process in the future if needed. Before running the script, I source the <code>.env</code> file with <code>. .env</code>, exporting the Immich variables to my environment.</p>
<p>This is the script I used to upload my photos:</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></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="k">if</span> ! <span class="nb">command</span> -v immich-go &gt;<span class="p">&amp;</span>/dev/null<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;[ERROR] immich-go is not installed.&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</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="nv">IMMICH_URL</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">IMMICH_SERVER_URL</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">IMMICH_KEY</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">IMMICH_KEY</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">IMMICH_ADMIN_KEY</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">IMMICH_ADMIN_KEY</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="nv">PHOTO_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">IMMICH_LOCAL_PHOTOS</span><span class="k">:-</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl"><span class="c1">## Trim trailing slashes</span>
</span></span><span class="line"><span class="cl"><span class="nv">PHOTO_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">PHOTO_DIR</span><span class="p">%/</span><span class="si">}</span><span class="s2">&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="nv">DRY_RUN</span><span class="o">=</span><span class="s2">&#34;false&#34;</span>
</span></span><span class="line"><span class="cl">
</span></span><span class="line"><span class="cl"><span class="k">function</span> usage<span class="o">()</span> <span class="o">{</span>
</span></span><span class="line"><span class="cl">  cat <span class="s">&lt;&lt;EOF
</span></span></span><span class="line"><span class="cl"><span class="s">Usage: ${0} [OPTIONS]
</span></span></span><span class="line"><span class="cl"><span class="s">
</span></span></span><span class="line"><span class="cl"><span class="s">Options:
</span></span></span><span class="line"><span class="cl"><span class="s">  -h, --help        Print this help menu
</span></span></span><span class="line"><span class="cl"><span class="s">  -u, --server-url  Immich server URL
</span></span></span><span class="line"><span class="cl"><span class="s">  -k, --api-key     Immich API token
</span></span></span><span class="line"><span class="cl"><span class="s">  -K, --admin-key   Immich API token with admin privileges
</span></span></span><span class="line"><span class="cl"><span class="s">  -p, --local-path  Path to local photos directory
</span></span></span><span class="line"><span class="cl"><span class="s">  --dry-run         Describe actions without taking them
</span></span></span><span class="line"><span class="cl"><span class="s">EOF</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">## Parse CLI 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">  -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="nb">exit</span> <span class="m">0</span>
</span></span><span class="line"><span class="cl">    <span class="p">;;</span>
</span></span><span class="line"><span class="cl">  -u <span class="p">|</span> --server-url<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">IMMICH_URL</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="si">}</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">  -k <span class="p">|</span> --api-key<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">IMMICH_KEY</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="si">}</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">  -K <span class="p">|</span> --admin-key<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">IMMICH_ADMIN_KEY</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="si">}</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">  -p <span class="p">|</span> --local-path<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">PHOTO_DIR</span><span class="o">=</span><span class="s2">&#34;</span><span class="si">${</span><span class="nv">2</span><span class="si">}</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">  --dry-run<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nv">DRY_RUN</span><span class="o">=</span><span class="s2">&#34;true&#34;</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">  *<span class="o">)</span>
</span></span><span class="line"><span class="cl">    <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Invalid arg: </span><span class="nv">$1</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span>
</span></span><span class="line"><span class="cl">    usage
</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="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="c1">## Validate inputs</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> -z <span class="s2">&#34;</span><span class="si">${</span><span class="nv">IMMICH_URL</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Missing --server-url&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span> <span class="o">&amp;&amp;</span> usage <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> -z <span class="s2">&#34;</span><span class="si">${</span><span class="nv">IMMICH_KEY</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Missing --api-key&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span> <span class="o">&amp;&amp;</span> usage <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">1</span>
</span></span><span class="line"><span class="cl"><span class="o">[[</span> -z <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PHOTO_DIR</span><span class="si">}</span><span class="s2">&#34;</span> <span class="o">]]</span> <span class="o">&amp;&amp;</span> <span class="nb">echo</span> <span class="s2">&#34;[ERROR] Missing --local-path&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</span> <span class="o">&amp;&amp;</span> usage <span class="o">&amp;&amp;</span> <span class="nb">exit</span> <span class="m">1</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> ! -d <span class="s2">&#34;</span><span class="si">${</span><span class="nv">PHOTO_DIR</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="nb">echo</span> <span class="s2">&#34;[ERROR] Could not find local photos dir: </span><span class="si">${</span><span class="nv">PHOTO_DIR</span><span class="si">}</span><span class="s2">&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</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="nv">cmd</span><span class="o">=(</span>immich-go upload from-google-photos -s <span class="s2">&#34;</span><span class="nv">$IMMICH_URL</span><span class="s2">&#34;</span> -k <span class="s2">&#34;</span><span class="nv">$IMMICH_KEY</span><span class="s2">&#34;</span> --on-errors <span class="k">continue</span><span class="o">)</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="si">${</span><span class="nv">IMMICH_ADMIN_KEY</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="nv">cmd</span><span class="o">+=(</span>--admin-api-key <span class="s2">&#34;</span><span class="si">${</span><span class="nv">IMMICH_ADMIN_KEY</span><span class="si">}</span><span class="s2">&#34;</span><span class="o">)</span>
</span></span><span class="line"><span class="cl"><span class="k">else</span>
</span></span><span class="line"><span class="cl">  <span class="nv">cmd</span><span class="o">+=(</span>--pause-immich-jobs<span class="o">=</span>FALSE<span class="o">)</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">## Append photo dir to end of command</span>
</span></span><span class="line"><span class="cl"><span class="nv">cmd</span><span class="o">+=(</span><span class="s2">&#34;</span><span class="nv">$PHOTO_DIR</span><span class="s2">&#34;</span><span class="o">)</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> <span class="s2">&#34;</span><span class="nv">$DRY_RUN</span><span class="s2">&#34;</span> <span class="o">==</span> <span class="s2">&#34;true&#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;[DRY RUN] Running immich-go in dry-run mode&#34;</span>
</span></span><span class="line"><span class="cl">  <span class="nv">cmd</span><span class="o">+=(</span>--dry-run<span class="o">)</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;Starting upload from </span><span class="nv">$PHOTO_DIR</span><span class="s2"> to server: </span><span class="nv">$IMMICH_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="k">if</span> ! <span class="s2">&#34;</span><span class="si">${</span><span class="nv">cmd</span><span class="p">[@]</span><span class="si">}</span><span class="s2">&#34;</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;[ERROR] Failed uploading photos&#34;</span> &gt;<span class="p">&amp;</span><span class="m">2</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="nb">echo</span> <span class="s2">&#34;Upload complete&#34;</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>If I ever need to re-import my collection, or if I create a new Google Takeout with my photos, I can use this script to upload them to my Immich server. The tool works great, it even imported my albums and all of the EXIF data from Google.</p>
<h2 id="tradeoffs">Tradeoffs</h2>
<p>Immich feels like a true replacement for Google Photos. The app is familiar enough if you&rsquo;ve used Google Photos. It has many of the same features, like duplicate and face detection, searches for objects like &ldquo;dog&rdquo; or &ldquo;tree,&rdquo; and creating shareable links for photos, videos, and galleries. But there are several real tradeoffs you need to be aware of, and risks you need to accept, when moving from a hosted photo provider to your own self-hosted setup.</p>
<h3 id="security">Security</h3>
<p>The first and biggest concern is security. A large company like Google has an interest in protecting what you upload to their servers. While they may use this data in objectionable ways, it is undeniably more secure to entrust your memories to a corporation with a dedicated security team and another team of engineers improving the service over time. While Google is famous for <a href="https://killedbygoogle.com/">abruptly killing useful services</a>, when you self host your media, you are also responsible for how you expose the server to the world (if you do this at all), and protecting the resources from active and passive attacks, and for ensuring your authentication layer and firewall keep unsavory traffic out of your systems.</p>
<h3 id="backups">Backups</h3>
<p>The next important factor to consider is backups. When you host your own media collection, you will want to ensure memories aren&rsquo;t lost from hardware failures or containerization problems. Keeping backups is crucial, and entirely your responsibility. When backing up important data, you should follow the <a href="https://www.acronis.com/en/blog/posts/backup-rule/">&ldquo;3-2-1 backups&rdquo; principle</a>, which boil down to:</p>
<ul>
<li>Keep 3 copies of your data: the original data on your primary device, and at least 2 copies.</li>
<li>Use 2 different storage devices: a PC, external disk, USB flash drive, NAS or SAN, or cloud storage services; pick any 2.</li>
<li>Keep 1 backup copy off-site: always have a copy of the backup somewhere other than where all of the main data and backups are. For example, if you are backing up to an external drive and a NAS on your home network, keeping a backup copy in an encrypted S3 bucket will prevent total data loss due to damage that might occur at home, like a fire/flood or heat damage.</li>
</ul>
<p>This strategy can be intense for a homelab, and it increases the size your collection uses on-disk. I generally keep 1 copy somewhere local (an external drive or my NAS), and upload an encrypted copy to <a href="https://wasabi.com/">Wasabi cloud storage</a>. If you want to avoid cloud hosting, you could also do a &ldquo;buddy backup,&rdquo; which is where you keep a storage device at a &ldquo;buddy&rsquo;s&rdquo; location (a relative or friend&rsquo;s house, or rented networked storage at a local datacenter, etc).</p>
<p><a href="/posts/comparing-backup-solutions">I use Restic for backups</a>, and which makes it simple to keep a local backup, a backup on another machine on my LAN via <a href="https://rclone.org/sftp/">Rclone SFTP copy</a>, and a remote copy in <a href="https://pcloud.com">pCloud</a> or Wasabi S3.</p>
<h3 id="maintenance">Maintenance</h3>
<p>Finally, there is the maintenance cost to consider. Comparing again to a hosted service, the company will have a dedicated team of engineers responsible for keeping hardware and software up to date, managing storage and quotas, and fixing things when they break. When you self-host, you become a 1-person maintenance team. The upside of choosing your own SLA is that you choose your own SLA; the downside is that shit&rsquo;s broken until you fix it. And if you experience a hardware failure, you also become the data recovery team. This can be a burden, but tools like <a href="https://github.com/redjax/docker_templates/tree/main/templates/networking/docker_dockhand">Dockhand</a> can simplify maintenance operations.</p>
<h2 id="conclusion">Conclusion</h2>
<p>Pulling my pictures and videos out of Google Photos felt great. I have all of my media on a drive in my house, backed up in multiple places where I feel comfortable I won&rsquo;t lose my collection due to an errant account ban or deletion, and I know my media isn&rsquo;t being ingested and mixed training data for an AI to use. I have full control of my media, accepting the tradeoffs in maintenance and backup responsibilities to have freedom from a service I grow continually displeased with, and do not trust to continue providing the service without interruption or deprecation on a whim. And best of all, I no longer need to pay Google a subscription to hold my growing photo collection. Goodbye, Google One!</p>
]]></content:encoded>
    </item>
    <item>
      <title>Comparing backup solutions (why I picked Restic)</title>
      <link>/posts/comparing-backup-solutions/</link>
      <pubDate>Thu, 26 Feb 2026 10:00:00 -0500</pubDate>
      <guid>/posts/comparing-backup-solutions/</guid>
      <description>My thoughts on backup managers I&amp;#39;ve tried throughout the years, and why I think Restic is the perfect tool for many backup scenarios.</description>
      <content:encoded><![CDATA[<p>Having a good backup strategy is essential, especially when you host your own data. If you have not experienced the dread of realizing you&rsquo;ve lost an important file, consider yourself lucky and keep reading to learn why you should have a backup strategy for your data. I have lost important data due to lack of sufficient backups a number of times in my life. It never gets any less devastating, but each time it&rsquo;s happened I have inched closer to a sufficient backup strategy.</p>
<p>There are a number of great solutions out there, from <a href="https://kopia.io">Kopia</a>, <a href="https://duplicati.com">Duplicati</a> and <a href="https://www.borgbackup.org">Borg</a>, to good ol&rsquo; <a href="https://rsync.net">rsync</a> and a remote share, and I have tried most of them. Let&rsquo;s talk about them, and why I ended up using Restic for all of my backups.</p>
<h2 id="the-problem">The Problem</h2>
<p>For a long time, I used Bash scripts scheduled with cron to create <code>.tar.gz</code> archives of important directories in a directory on my machine, i.e. <code>/opt/backups</code>. I eventually started using the <code>rsync</code> CLI tool to copy the backups via SSH to a central machine with a few extra hard drives.</p>
<p>These scripts got unwieldy, and I&rsquo;ve lost most of them because I was not using version control yet. I also use both Windows and Linux, and translating my backup scripts to Powershell from Bash was good practice, but tedious.</p>
<h2 id="the-journey">The Journey</h2>
<p>I eventually started looking for tools I could install, and was pleased to find a ton of different open source projects with varying approaches to backups. I tried a bunch of them, feeling more and more like Goldilocks as I found one solution too resource intensive, another too complicated, some were too unreliable and others difficult to navigate. In my quest to find the perfect solution for my needs, the following utilities felt closest to my nebulous idea of the perfect solution for my homelab.</p>
<h3 id="duplicati--wasabi">Duplicati + Wasabi</h3>
<p><img alt="Duplicati logo" loading="lazy" src="/posts/comparing-backup-solutions/duplicati-logo.png#center"></p>
<p>I started with Duplicati, which I used to create scheduled backups to my NAS and <a href="https://wasabi.com">Wasabi S3 storage</a>. Creating the scheduled jobs was easy, and there is a management webUI that made creating and monitoring the backups pretty easy.</p>
<p>I was able to avert a couple of data loss events by restoring from a Duplicati backup, but I ended up running into database corruption a time or two (a known issue with Duplicati, if you search &ldquo;Duplicati database corrupted&rdquo;). You can recover from this state, but after the 2nd time it happened, I started looking for a different solution.</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>Duplicati Pros:</p>
<ul>
<li>WebUI was a nice way to manage the backups graphically.</li>
<li>Scheduling jobs with retry logic was easy.</li>
<li>Multiple backends, from local storage to cloud buckets to webDAV.</li>
</ul>
<p>Duplicati Cons:</p>
<ul>
<li>Slow backups, slow restores.</li>
<li>Restore operations less reliable.
<ul>
<li>More prone to database corruption.</li>
<li>Manual restore process is tedious.</li>
</ul>
</li>
<li>Functional webUI, not winning any style awards.</li>
</ul></div>

<h3 id="kopia">Kopia</h3>
<p><img alt="Kopia logo" loading="lazy" src="/posts/comparing-backup-solutions/kopia-logo.svg#center"></p>
<p>I really liked being able to add a Wasabi bucket as one of the backup destinations, and my searches for an alternative to Duplicati eventually lead me to try Kopia. I had read forum posts of people moving to Kopia from Duplicati, and I ran the two side-by-side for a while to get a feel for Kopia.</p>
<p>It did not take long to drop Duplicati entirely. It took me a little while to get used to having only 1 repository for backups, and I never got around to setting up a <a href="https://kopia.io/docs/repository-server/">Kopia server</a> to allow for more. The way I used Kopia was essentially a per-machine repository.</p>
<p>Kopia worked well for me, and I would still recommend it as an option. The UI was the biggest pain point for me: the CLI commands were long and hard to remember, and the webUI was spartan and at times unintuitive.</p>
<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>Kopia Pros:</p>
<ul>
<li>Fairly easy to pick up and understand.</li>
<li>Reliable scheduled backups, faster than Duplicati, and the resulting snapshots were smaller.</li>
<li>Different apps for different needs (a CLI, an optional web interface, and a desktop app).</li>
<li>Test data restores worked flawlessly every time, and Kopia allowed for restoring to a destination, or in-place (restoring directly back to the path where the original file/directory was).</li>
</ul>
<p>Kopia Cons:</p>
<ul>
<li>The webUI, while better than Duplicati, still left something to be desired.</li>
<li>While Kopia has a ton of features, the complexity could be difficult to navigate.</li>
</ul></div>

<h3 id="borg-backup">Borg Backup</h3>
<p><img alt="Borg Backup" loading="lazy" src="/posts/comparing-backup-solutions/borg-logo.svg#center"></p>
<p>I will be honest, I probably didn&rsquo;t give Borg enough time to write an honest review about it. I started using it essentially in tandem with picking up restic, and quickly gravitated to restic.</p>
<p>I love Borg in theory, it&rsquo;s a tool known for its relative ease of use and reliability when it comes time to restore from a backup. In comparing Borg and restic, I found <a href="https://www.reddit.com/r/BorgBackup/comments/v3bwfg/why_should_i_switch_from_restic_to_borg/">a helpful Reddit thread</a> comparing the two tools, and a few of the points were enough for me to move fully to restic. Below are the direct quotes from the post that swayed me (in case the link rots over time):</p>
<ul>
<li>Restic&rsquo;s cryptography is much better because it has been <a href="https://words.filippo.io/restic-cryptography/">endorsed</a> by one of Google&rsquo;s cryptography experts that wrote the crypto library for Google&rsquo;s Go language. He ended up choosing Restic as his personal backup system after the investigation.</li>
<li>Borg&rsquo;s cryptography has many security flaws and they&rsquo;re working on a rewrite of it for the next 1.3+ release named &ldquo;Helium&rdquo;. &hellip;<em>truncated</em>
<ul>
<li><em>Borg 2.0 addresses these concerns, but is still in beta as of February 2026 and not recommended for production use.</em></li>
</ul>
</li>
<li>Borg requires that the receiver runs Borg on the server, which limits it to rsync.net and borgbase.com for online cloud storage. &hellip;<em>truncated</em>
<ul>
<li><em>Hetzner storage boxes are also supported now, but the number of available backends for Borg is still limited.</em></li>
</ul>
</li>
</ul>
<h2 id="arriving-at-restic">Arriving at Restic</h2>
<p><img alt="Restic icon" loading="lazy" src="/posts/comparing-backup-solutions/restic-logo.png#center"></p>
<p>As I researched and tried different backup solutions, I kept seeing comments and posts about restic. I learned about people <a href="https://blog.bithive.space/post/automatic-backups-with-restic/">scripting restic with Bash</a> like I had done with rsync.</p>
<p>Restic has powerful deduplication with their <a href="https://restic.net/blog/2015-09-12/restic-foundation1-cdc/">Content Defined Chunking (CDC) implementation</a>, leading to smaller backups and better accuracy.</p>
<p>Restic also works with <a href="https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html">many different backends</a>, and can use <a href="https://rclone.org">Rclone</a> to expand backup destination options even more.</p>
<p>With restic, you <a href="https://restic.readthedocs.io/en/stable/030_preparing_a_new_repo.html">create a repository</a> (it&rsquo;s easy to create and use multiple repositories stored in different backends), point the program at a path, give it an <a href="https://restic.readthedocs.io/en/stable/040_backup.html#excluding-files">excludes file</a> (or an <a href="https://restic.readthedocs.io/en/stable/040_backup.html#including-files">includes file</a>), and <a href="https://restic.readthedocs.io/en/stable/040_backup.html">let it rip</a>. It&rsquo;s pretty much that easy.</p>
<p>I have tested Restic with an external SSD, a directory on another machine (via SSH, facilitated by Rclone), pCloud (also via Rclone), and a Wasabi S3 bucket. Restic does a great job of making all of this feel insignificant and simple; it doesn&rsquo;t matter if your backup is on the same machine or a cloud bucket in another region of the world, what matters is the contents and integrity of the backup, which Restic handles gracefully.</p>
<p>Restic makes it easy to <a href="https://restic.readthedocs.io/en/stable/045_working_with_repos.html">explore your snapshots</a>, and in my tests <a href="https://restic.readthedocs.io/en/stable/050_restore.html">restoring data</a> is quick, simple, and &ldquo;just works.&rdquo; You can also <a href="https://restic.readthedocs.io/en/stable/075_scripting.html">script Restic operations</a>, although I never dove too deep into this.</p>
<p>Instead, I found two other tools that cemented my choice to use restic for my backups: <a href="https://github.com/creativeprojects/resticprofile">Resticprofile</a> and <a href="https://github.com/garethgeorge/backrest">Backrest</a>.</p>
<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>Restic pros:</p>
<ul>
<li>Backup speed, especially during incremental backups.</li>
<li>Powerful deduplication saves space.</li>
<li>Great 3rd party tools like resticprofile and backrest.</li>
<li>Good mix of security and usability.</li>
</ul>
<p>Restic cons:</p>
<ul>
<li>There is a slight learning curve.</li>
<li>Command chains can feel unnatural at first, especially if you&rsquo;re coming from a GUI/webUI application.</li>
<li>Restic on its own can feel cumbersome, with long commands and a lot of args. Extra tooling like Resticprofile or Backrest helps significantly with this.</li>
</ul></div>

<h3 id="resticprofile">Resticprofile</h3>
<p>Resticprofile is a wrapper for restic that works by reading backup <a href="https://creativeprojects.github.io/resticprofile/configuration/index.html">configurations from a file</a>. You can define defaults, the file supports <a href="https://creativeprojects.github.io/resticprofile/configuration/inheritance/index.html">inheritance</a>, <a href="https://creativeprojects.github.io/resticprofile/configuration/templates/index.html">templates</a>, and <a href="https://creativeprojects.github.io/resticprofile/configuration/templates/index.html">template variables</a>; you can &ldquo;group&rdquo; individual profiles which you can call with <code>resticprofile -n &lt;profile-name&gt;</code>. After defining your backups, you can run <code>resticprofile schedule install</code> to add scheduled tasks (systemd on Linux, Task Scheduler on Windows, Launchd on macOS), which fully automates your backups. It can monitor backup status using <a href="https://creativeprojects.github.io/resticprofile/monitoring/status/index.html">a JSON file</a> or by <a href="https://creativeprojects.github.io/resticprofile/monitoring/prometheus/index.html">sending metrics to Prometheus</a>.</p>
<p>Restic and Resticprofile being cross-platform, with support for Linux, Windows, and macOS, meant I could use the same app and portions of the same configuration file across my machines. Automating the backups meant I did not have to write custom, per-platform scripts to run the backups on a schedule.</p>
<h3 id="backrest">Backrest</h3>
<p>I also add Backrest to some of my servers, which provides a webUI I can use instead of logging into the machine directly. Backrest can import and use existing repositories, and the UI allows for the same level of orchestration as resticprofile or the restic CLI. You can browse snapshots and restore files in the webUI, add notifications to Discord, Slack, Gotify, etc, and it has pre/post command hooks to execute shell scripts. I generally use either Backrest <em>or</em> resticprofile on a given machine, as they both serve essentially the same function. But since they can both use the same backend repository, it&rsquo;s easy to switch them out on demand if I decide I want to manage backups using one or the other.</p>
<p>Backrest also has <a href="https://github.com/garethgeorge/backrest?tab=readme-ov-file#running-with-docker-compose">a Docker container</a>, which makes it a great addition to Docker Compose stacks for automating container data backup.</p>
<h2 id="conclusion">Conclusion</h2>
<p>I was aware of restic for a while before I actually decided to try it, and I can say I wish I had tried this tool sooner. While there&rsquo;s a slight learning curve, once you get your first backup running, and especially after using a tool to automate the complexity like resticprofile or backrest, it&rsquo;s clear to me that restic is the perfect combination of user friendliness, security, reliability, and simplicity for a backup solution. I will write another post or two describing how I actually use restic with examples of a resticprofile configuration file and some dos and don&rsquo;ts.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Blog Setup: Challenges</title>
      <link>/posts/blog-setup/challenges/</link>
      <pubDate>Wed, 25 Feb 2026 00:00:00 -0500</pubDate>
      <guid>/posts/blog-setup/challenges/</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of a series: &lt;a href=&#34;/series/blog-setup&#34;&gt;Blog Setup&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;p&gt;I also faced some difficulties getting started with the blog. I spent way too much time creating the Github Action that deploys my content to Netlify. I failed so many times before getting it right, I exceeded Netlify&amp;rsquo;s free tier 🤡. Most of the difficulty actually came from the &amp;ldquo;extra&amp;rdquo; functionality with Lychee link checking and Vale linting. Although I was able to get both of these tools running well locally on my machine, I ran into challenges getting them to run in the pipeline.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>This post is part of a series: <a href="/series/blog-setup">Blog Setup</a>.</em></p>
<p>I also faced some difficulties getting started with the blog. I spent way too much time creating the Github Action that deploys my content to Netlify. I failed so many times before getting it right, I exceeded Netlify&rsquo;s free tier 🤡. Most of the difficulty actually came from the &ldquo;extra&rdquo; functionality with Lychee link checking and Vale linting. Although I was able to get both of these tools running well locally on my machine, I ran into challenges getting them to run in the pipeline.</p>
<p>Linting with Vale was very difficult to nail down. I was able to get it to work reliabily in the local repository, but struggled with containerizing it and running in a pipeline. I also had invalid syntax in my Lychee link scanning for a while. I resolved both issues, but definitely got caught up in the weeds and did not write any new blog posts in the meantime.</p>
<p>I reposted [one of my old blogs from Medium](/posts/linux-frustrated-lovingI don&rsquo;t), and realized Hugo doesn&rsquo;t have a great way of centering images. You can use <a href="https://gohugo.io/content-management/shortcodes/">Hugo shortcodes</a>, but those are specific to/dependent on Hugo, and in the interest of keeping the content as decoupled from the site generator as possible, I did not want to invest too heavily in shortcodes. I found <a href="http://www.ebadf.net/2016/10/19/centering-images-in-hugo/">a really helpful blog post on ebaddf.net</a> that had a simple, elegant fix that should work across site generators.</p>
<p>It involves creating a custom CSS file, i.e. at <code>static/css/custom.css</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><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></code></pre></td>
<td class="lntd">
<pre tabindex="0" class="chroma"><code class="language-css" data-lang="css"><span class="line"><span class="cl"><span class="nt">img</span><span class="o">[</span><span class="nt">src</span><span class="o">$=</span><span class="s1">&#39;#center&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">display</span><span class="p">:</span> <span class="kc">block</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mf">0.7</span><span class="kt">rem</span> <span class="kc">auto</span><span class="p">;</span> <span class="c">/* you can replace the vertical &#39;0.7rem&#39; by
</span></span></span><span class="line"><span class="cl"><span class="c">                            whatever floats your boat, but keep the
</span></span></span><span class="line"><span class="cl"><span class="c">                            horizontal &#39;auto&#39; for this to work */</span>
</span></span><span class="line"><span class="cl">    <span class="c">/* whatever else styles you fancy here */</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="nt">img</span><span class="o">[</span><span class="nt">src</span><span class="o">$=</span><span class="s1">&#39;#floatleft&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">float</span><span class="p">:</span><span class="kc">left</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mf">0.7</span><span class="kt">rem</span><span class="p">;</span>      <span class="c">/* this margin is totally up to you */</span>
</span></span><span class="line"><span class="cl">    <span class="c">/* whatever else styles you fancy here */</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="nt">img</span><span class="o">[</span><span class="nt">src</span><span class="o">$=</span><span class="s1">&#39;#floatright&#39;</span><span class="o">]</span>
</span></span><span class="line"><span class="cl"><span class="p">{</span>
</span></span><span class="line"><span class="cl">    <span class="k">float</span><span class="p">:</span><span class="kc">right</span><span class="p">;</span>
</span></span><span class="line"><span class="cl">    <span class="k">margin</span><span class="p">:</span> <span class="mf">0.7</span><span class="kt">rem</span><span class="p">;</span>      <span class="c">/* this margin is totally up to you */</span>
</span></span><span class="line"><span class="cl">    <span class="c">/* whatever else styles you fancy here */</span>
</span></span><span class="line"><span class="cl"><span class="p">}</span>
</span></span></code></pre></td></tr></table>
</div>
</div><p>And embedding images using that class:</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-markdown" data-lang="markdown"><span class="line"><span class="cl">![<span class="nt">your_img</span>](<span class="na">/img/your_img.png#center</span>)
</span></span><span class="line"><span class="cl">![<span class="nt">your_img</span>](<span class="na">/img/your_img.png#floatleft</span>)
</span></span><span class="line"><span class="cl">![<span class="nt">your_img</span>](<span class="na">/img/your_img.png#floatright</span>)
</span></span></code></pre></td></tr></table>
</div>
</div><p>Getting URL slugs to work for content nested in directories by year (i.e. <code>content/posts/&lt;year&gt;/post-name.md</code> in the source code) to work proved harder than I expected. I wanted to organize the source code for my blogs by year (both as a way to reduce clutter/page loads in Github, and because it&rsquo;s easier to manage in a code editor), but I wanted the URLs to omit the year (i.e. <code>/posts/post-name</code> without the year).</p>
<p>I tried a few different things:</p>
<ul>
<li>Using <a href="https://gohugo.io/configuration/cascade/#target">the <code>cascade</code> configuration option</a> to &ldquo;collapse&rdquo; URLs
<ul>
<li>This was supposed to move anything in subdirectories under <code>content/posts/**</code> up to the root <code>posts/</code> path when building the site.</li>
<li>It worked, but it also caused <code>/posts/</code> to return a <code>404</code> error.</li>
</ul>
</li>
<li>Creating a branch bundle with <code>content/posts/&lt;year&gt;/_index.md</code>.
<ul>
<li>
<p>This solution also made use of the <code>cascade</code> option, but in a more targeted way:</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-markdown" data-lang="markdown"><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">cascade</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nt">url</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;/posts/:slug/&#34;</span><span class="w">
</span></span></span><span class="line"><span class="cl"><span class="nn">---</span><span class="w">
</span></span></span></code></pre></td></tr></table>
</div>
</div></li>
<li>
<p>I originally used <code>headless: true</code>, but this option contributed to my <code>404</code> problem, because I use <a href="/posts"><code>/posts</code></a> to aggregate all of my posts.</p>
</li>
</ul>
</li>
<li>Adding a <code>slug</code> to each page.
<ul>
<li>I tried adding <code>slug: &quot;url-slug-i-want&quot;</code>. I even added it to my <code>posts</code> <a href="https://gohugo.io/content-management/archetypes/#article">archetype</a></li>
<li>This apparently has no effect on bundles.</li>
</ul>
</li>
</ul>
<p>I found that using a <a href="https://gohugo.io/content-management/urls/#slug"><code>slug</code></a> to manually set the URL path works well. For some pages, the <a href="https://gohugo.io/content-management/urls/#url"><code>url</code></a> option worked better. <code>slug</code> is for overriding the last segment of the path, i.e. <code>/page-name/slug-name</code>, while <code>url</code> gives full control over the URL path, even allowing characters that are reserved by the OS, i.e. adding colons <code>:</code> to a URL like <code>http://example.com/my:example</code>.</p>
<p>If both <code>slug</code> and <code>url</code> are set, the <code>url</code> parameter takes precedence</p>
<p>For posts, this looks like:</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-markdown" data-lang="markdown"><span class="line"><span class="cl"><span class="gh"># archetypes/posts.md
</span></span></span><span class="line"><span class="cl">---
</span></span><span class="line"><span class="cl">title: &#34;{{ replace .Name &#34;-&#34; &#34; &#34; | title }}&#34;
</span></span><span class="line"><span class="cl">date: {{ .Date }}
</span></span><span class="line"><span class="cl">draft: true
</span></span><span class="line"><span class="cl">slug: &#34;/post-name/optional-sub-post-name&#34;
</span></span><span class="line"><span class="cl"><span class="gu">## Settinng a URL too will override the slug
</span></span></span><span class="line"><span class="cl">url: &#34;post-name/optional-sub-post-name&#34;
</span></span></code></pre></td></tr></table>
</div>
</div><p>I am sure there will be many other challenges (for instance, I have not tried changing the blog&rsquo;s theme yet, and that looks like a whole adventure if you rely on theme features), but the <a href="https://discourse.gohugo.io">community forums</a> are filled with very helpful threads, and it is relatively easy to find help for Hugo online. It&rsquo;s a popular tool that&rsquo;s well documented and has an active community. Some of the challenges I&rsquo;ve faced are quirks or limitations of Hugo, while some are problems I would have had with any static site generator. I&rsquo;m overall happy with the Hugo experience.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Blog Setup: Tools</title>
      <link>/posts/blog-setup/tools/</link>
      <pubDate>Sun, 22 Feb 2026 05:00:00 -0500</pubDate>
      <guid>/posts/blog-setup/tools/</guid>
      <description>&lt;p&gt;&lt;em&gt;This post is part of a series: &lt;a href=&#34;/series/blog-setup&#34;&gt;Blog Setup&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;
&lt;h2 id=&#34;tools&#34;&gt;Tools&lt;/h2&gt;
&lt;p&gt;I am using &lt;a href=&#34;https://mise.jdx.dev&#34;&gt;Mise&lt;/a&gt; to handle installing all of the tools I need to develop/build the blog. The &lt;a href=&#34;https://github.com/redjax/blog/blob/main/.mise.toml&#34;&gt;&lt;code&gt;.mise.toml&lt;/code&gt; file in the repository&lt;/a&gt; defines these tools, so I can simply clone the repository, install Mise, and run &lt;code&gt;mise trust &amp;amp;&amp;amp; mise install&lt;/code&gt; on a new machine to get up and running. Mise is also useful in container environments and CI/CD pipelines, it can ensure I&amp;rsquo;m installing the same tools in every environment, with version pinning for ones I want to control upgrades for like the &lt;code&gt;hugo&lt;/code&gt; CLI.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p><em>This post is part of a series: <a href="/series/blog-setup">Blog Setup</a>.</em></p>
<h2 id="tools">Tools</h2>
<p>I am using <a href="https://mise.jdx.dev">Mise</a> to handle installing all of the tools I need to develop/build the blog. The <a href="https://github.com/redjax/blog/blob/main/.mise.toml"><code>.mise.toml</code> file in the repository</a> defines these tools, so I can simply clone the repository, install Mise, and run <code>mise trust &amp;&amp; mise install</code> on a new machine to get up and running. Mise is also useful in container environments and CI/CD pipelines, it can ensure I&rsquo;m installing the same tools in every environment, with version pinning for ones I want to control upgrades for like the <code>hugo</code> CLI.</p>
<p><a href="https://taskfile.dev">Taskfile</a> is a new tool I found that lets you write YAML files to define tasks that can be called with the <code>task</code> CLI. It&rsquo;s a very versatile tool, and because it uses an embedded shell, it&rsquo;s (pretty much) fully cross platform. This is very useful, because it means the same tasks can run on Windows, Mac, or Linux, in CI/CD pipelines, Dockerfiles, etc. I use it for simplifying some commands I run frequently, like <a href="https://github.com/redjax/blog/blob/main/.tasks/dockertasks.yml">Docker commands</a> or <a href="https://github.com/redjax/blog/blob/main/.tasks/hugotasks.yml">Hugo commands</a>.</p>
<p><a href="https://vale.sh">Vale</a> is a utility that lets you define a style guide and lint text content against it. You can use predefined styles, like Microsoft or Google&rsquo;s, and can mix and match. When Vale runs, it will surface writing and readability issues from the chosen style guides.</p>
<p><a href="https://lychee.cli.rs">Lychee</a> is a fast, easy to use link checker. I found it while setting up this blog, tried it once, and immediately added it to the stack. Lychee can scan live sites/URLs, but can also be pointed at the raw Markdown files that comprise this blog, or the rendered HTML. This allows me to catch broken links on the site before deploying, or to detect <a href="https://en.wikipedia.org/wiki/Link_rot">linkrot</a>.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Blog Setup: Choices Made</title>
      <link>/posts/blog-setup/choices-made/</link>
      <pubDate>Thu, 19 Feb 2026 23:00:00 -0500</pubDate>
      <guid>/posts/blog-setup/choices-made/</guid>
      <description>The setup &amp;amp; hosting choices I made for creating this blog.</description>
      <content:encoded><![CDATA[<p><em>This post is part of a series: <a href="/series/blog-setup">Blog Setup</a>.</em></p>
<p>While setting up the blog I had to make a number of choices that affect the short and long term health of the blog. Deciding where and how to host the code and the static site, what tooling to add, how to structure the repository, etc.</p>
<h2 id="git-forge">Git Forge</h2>
<p>Spoiler: <a href="https://github.com/redjax/blog">I went with Github</a>.</p>
<p>I considered many options for where to host the source code for the blog. Github is an obvious frontrunner, partially becaause <a href="https://github.com/redjax">I am already active on Github</a>, but also the network effect. While I don&rsquo;t promote my blog, I also like the idea that I&rsquo;m not just shouting into the void with my posts, and discoverability is best where the people are.</p>
<p>Github hit <a href="https://github.com/Red-Killer/shit/issues/1/">one billion repositories on June 11, 2025</a>, the first public forge to do so, highlighting the sheer amount of attention and usage on the platform. Most of my coding activity is still on Github (although I <a href="https://codeberg.org/redjax">mirror many of my repositories to codeberg.org</a>), so anyone stopping by my profile is more likely to find my blog.</p>
<p>One of my concerns while setting up the blog was the ability to quickly and easily move to a new Git host. If I want to move the blog to <a href="https://gitlab.com">Gitlab</a> or my own <a href="%5Bhttps://forgejo.%5D(https://forgejo.org)">self hosted git forge</a>, I don&rsquo;t to have to labor to get back up and running. I plan to only dip my toes into a given platform; I will take advantage of some of the features, but will prefer external or embedded solutions for most things (sidenote: I need to find a way to keep issues out of Github.)</p>
<p>And so Github it is, for now.</p>
<h2 id="cicd">CI/CD</h2>
<p>I spent time planning the structure of <a href="https://github.com/redjax/blog">the git repository for the blog</a> to make it portable. I have seen a number of blogs switch platforms over the years, and the posts they write at the end of a migration have inspired some of the choices made for this blog.</p>
<p>For example, while this project is hosted on Github and I am using Github Actions to do things like lint the content and publish new pages, I also recreated the functionality in <a href="https://concourse-ci.org">Concourse CI pipelines</a> so I am not tied to a specific git platform. I could move the blog&rsquo;s repository to any other remote, like <a href="https://gitlab.com">Gitlab</a> or <a href="https://codeberg.org">Codeberg</a> without breaking my CI/CD. At some point I will probably try writing the pipelines in <a href="https://dagger.io">Dagger</a> to make them truly portable.</p>
<p>I also considered <a href="https://woodpecker-ci.org">Woodpecker CI</a>, which is a fork of <a href="https://www.drone.io">Drone</a>, <a href="https://crowci.dev/v5-2/">Crow CI</a> which felt a bit too simplistic, and <a href="https://komo.do">Komodo</a>. I may still integrate Komodo for GitOps like deploying on specific conditions in a PR.</p>
<h2 id="hosting">Hosting</h2>
<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 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>I moved this blog from Netlify to <a href="https://pages.cloudflare.com/">Cloudflare Pages</a> on 2026-08-02. For a simple static site like this, a Netlify deployment ended up being overkill, and had the potential to run up billing, limiting my ability to publish the site and <a href="https://answers.netlify.com/t/free-plan-usage-limit-website-suspension/145978/4">taking it offline completely</a> until the next billing cycle.</p>
<p>Netlify&rsquo;s service was very easy to get started with, and I like the service. It offers a lot more than Cloudflare Pages, and this blog simply doesn&rsquo;t need those &ldquo;extras.&rdquo; It made sense to switch to Cloudflare Pages, where I get unlimited deployments and free, simple hosting.</p></div>

<p>As of 01/24/2026, this blog is hosted on <a href="https://netlify.com">Netlify</a>, a platform I have been looking for a reason to try. I disabled Netlify&rsquo;s automated rebuilds on merges to main so I could <a href="https://github.com/redjax/blog/blob/main/.github/workflows/hugo-deploy.yml">write a deployment pipeline of my own</a>, mainly for the experience, but also to ensure I don&rsquo;t break the site when I&rsquo;m trying new things. I hit Netlify&rsquo;s free tier limit in 2 days because of all the pipeline failures&hellip;I mean &ldquo;tests&rdquo; that I ran&hellip;but the team was generous enough to refresh my credits when I reached out for support. +1 to Netlify!</p>
<p>Because the site is just static HTML/JS/CSS, I leave the option open to move the site&rsquo;s hosting to basically anywhere. I build a <a href="https://github.com/redjax/blog/tree/main/.containers/prod">production Docker image</a>, so I could deploy a container somewhere and route the <code>techobyte.cc</code> domain to it, or simply copy the static files to any host that can serve files via a web server. This ensures I won&rsquo;t get &ldquo;stuck&rdquo; with a specific host, or with a specific deployment method. I have heard of many bloggers who run their blogs off a Raspberry Pi they have in their house, and there&rsquo;s really nothing stopping me from doing the same!</p>
<p>I also evaluated <a href="https://fly.io">Fly</a>, but decided they were more geared at full blown application deployments, <a href="https://azure.microsoft.com/en-us/products/category/containers/">Azure Containers</a>, a <a href="https://www.hetzner.com">Hetzner VPS</a> I would have to maintain, <a href="https://www.digitalocean.com/products/droplets">DigitalOcean Droplets</a>, and of course hosting it myself on a machine in my home, behind a reverse proxy.</p>
<h2 id="theme">Theme</h2>
<p>This was a tough one&hellip; Hugo has <a href="https://themes.gohugo.io">a lot of themes</a>. Most of them look good, some of them look great, but I had a few requirements in mind:</p>
<ul>
<li>Optimized for text-based posts. Most of my posts will be text-heavy, so optimizing layout for reading was an important factor.</li>
<li>Simplicity, I didn&rsquo;t want to use a highly customizable theme that would require a lot of configuration and tweaks.
<ul>
<li>I am aware of my tendency to find <a href="https://en.wiktionary.org/wiki/yak_shaving">yaks to shave</a>, and want to focus on writing.</li>
<li>Limiting myself to themes that are opinionated and rigid narrowed the selection pretty significantly.</li>
</ul>
</li>
<li>As generic as possible. Themes like <a href="https://themes.gohugo.io/themes/blowfish/">Blowfish</a> are amazing, but you have to go all-in to really take advantage.
<ul>
<li>I am building this blog for the long term. The ability to switch themes/site generators is highly appealing.</li>
<li>The more customization I need to do to the blog, the more I am building the site around the theme rather than the content.</li>
</ul>
</li>
<li>Content is front and center, no landing pages or superflous sections.</li>
<li>Support RSS (this is <a href="https://gohugo.io/templates/rss/">officially supported by Hugo</a>, but some themes handle revealing the functionality to readers better than others).</li>
</ul>
<p>I decided to start with <a href="https://themes.gohugo.io/themes/hugo-papermod/">PaperMod</a> as my theme. While it is kind of opinionated and has some theme-specific configurations, they seem easy enough to &ldquo;rip out&rdquo; if I ever want to change it later. It has a light and dark mode, and center-aligns the content for distraction-free reading. RSS setup was easy, and the icon is very obvious on pages that support it.</p>
]]></content:encoded>
    </item>
    <item>
      <title>Blog Setup</title>
      <link>/posts/blog-setup/</link>
      <pubDate>Tue, 03 Feb 2026 23:51:00 +0000</pubDate>
      <guid>/posts/blog-setup/</guid>
      <description>&lt;p&gt;Starting a blog has been an interest of mine for a long time. I have always envied people who began writing their tech blogs decades ago and still post to them today. I have written articles on Medium and other platforms over the years, but have never centralized them or made them &amp;ldquo;portable&amp;rdquo; in a meaningful way.&lt;/p&gt;
&lt;p&gt;I decided to finally start my blog, becaause each year that passes where I haven&amp;rsquo;t joined the ranks of nerd bloggers is another year I can&amp;rsquo;t look back on in this format.&lt;/p&gt;</description>
      <content:encoded><![CDATA[<p>Starting a blog has been an interest of mine for a long time. I have always envied people who began writing their tech blogs decades ago and still post to them today. I have written articles on Medium and other platforms over the years, but have never centralized them or made them &ldquo;portable&rdquo; in a meaningful way.</p>
<p>I decided to finally start my blog, becaause each year that passes where I haven&rsquo;t joined the ranks of nerd bloggers is another year I can&rsquo;t look back on in this format.</p>
<p>This blog setup series chronicles the process of getting this blog up and running. I will release posts detailing tools I am using, choices I&rsquo;ve made during setup, and challenges I faced being new to <a href="https://hugo.io">Hugo</a>, the static site generator I&rsquo;m using to build the blog. I may retroactively edit pages while the series is in progress.</p>
<h2 id="posts">Posts</h2>
<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>This series is a work in progress. The list of posts below is incomplete until this message is removed.</p></div>

<ul>
<li><a href="/posts/blog-setup/choices-made">Part 1: Choices Made</a></li>
<li><a href="/posts/blog-setup/tools">Part 2: Tools</a></li>
<li><a href="/posts/blog-setup/challenges">Part 3: Challenges</a></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>
    <item>
      <title>First</title>
      <link>/posts/first/</link>
      <pubDate>Sun, 04 Jan 2026 23:21:26 -0500</pubDate>
      <guid>/posts/first/</guid>
      <description>The first post on the site</description>
      <content:encoded><![CDATA[<p>Welcome to Techobyte, a personal blog about technology. This blog covers varied topics with no posting cadence or central theme. The blog&rsquo;s content is text-heavy, although some articles may use images to enhance or clarify things.</p>
<p>I write about tools I use and projects I work on in my homelab, and share the knowledge on this blog. In my free time I like to try new technologies and tools, and I love to share that knowledge with others. I hope the hours I spend struggling on certain things and choose to write about can get you over a hump.</p>
<p>You can subscribe to the <a href="/index.xml">RSS feed</a>, a feed of the entire site. Add <code>/index.xml</code> to individual pages/sections on the site to subscribe to content of that type. For example, check the <a href="/posts/index.xml"><code>posts</code> feed</a> to see a feed of only the <a href="/posts/">posts</a> made on this site (omitting <a href="/notes/">notes</a>).</p>
<p>Techobyte is a <a href="https://gohugo.io">Hugo</a> static site. Feel free to <a href="https://github.com/Techobytedigital/blog">check out the source code for this blog</a>!</p>
]]></content:encoded>
    </item>
    <item>
      <title>How Linux Frustrated Me Into Loving It</title>
      <link>/posts/linux-frustrated-loving/</link>
      <pubDate>Thu, 24 Mar 2016 00:00:00 -0500</pubDate>
      <guid>/posts/linux-frustrated-loving/</guid>
      <description>Re-post of an old Medium article I wrote in 2016.</description>
      <content:encoded><![CDATA[<p><img alt="Header image" loading="lazy" src="/posts/linux-frustrated-loving/linux-story.jpeg#center"></p>
<p>I have been very interested in Linux since my entry into the Wonderful World of Unix in 2006. I found Ubuntu and installed it on a crappy Dell desktop computer I was given when I was doing online schooling. The computer originally came with Windows, and one day while I was browsing, I decided to search for “alternative to Windows.” Linux popped up right away. I had never heard of Linux before, but after voraciously reading article after article, I decided Linux was the path for my future.</p>
<p><img alt="&ldquo;Linux for human beings&rdquo; was not applicable in 2006" loading="lazy" src="/posts/linux-frustrated-loving/ubuntu-logo.jpeg#center"></p>
<p>Like most Linux newbies, <a href="https://ubuntu.com">Ubuntu</a> was the first distribution I found. Back in 2006, Ubuntu wasn’t ugly, necessarily, but it definitely was not a pretty distribution. When I first found Ubuntu, they were on version 6.10, “Edgy Eft.” My initial thoughts were that their naming convention was stupid (Ubuntu picks an animal for each release and creates an adjective-noun alliteration for it), but it looked interesting, and I decided to try it. I downloaded the image and burnt it to a CD (I’ve since moved to multibooting Linux images from a USB stick), popped it in my CD tray, and jumped straight down the rabbit hole.</p>
<p><img alt="Ubuntu Linux 6.10 &ldquo;Edgy Eft&rdquo;" loading="lazy" src="/posts/linux-frustrated-loving/ubuntu-edgyeft-desktop.jpeg#center"></p>
<p>I was always interested in computers as a kid, but I had never entered into the world of partition and driver hell that was such a reality back in 2006. When I booted the disk, I was presented with an installer (back then, I believe it was still a command line installer of sorts…it was graphical, but not like today’s installers; there was a lot of room for error). Before I started the installation, I had printed out a “how to install Ubuntu” guide, but I didn’t read very much of it previous to my first attempt. At that point, I had not had much trouble in figuring out how to install, configure, and use software in the Windows world.</p>
<p><img alt="The modern Ubuntu installer is nearly foolproof" loading="lazy" src="/posts/linux-frustrated-loving/ubuntu-installer.jpeg#center"></p>
<p>During my first attempt, I completely wiped out my Windows partition and deleted everything I had on my computer, because I had no idea what “format” meant, or what a partition even was. I also did not successfully install Linux on my first attempt. I thought I had destroyed my computer.</p>
<p><img alt="&ldquo;Lesson learned&hellip;read about what I&rsquo;m doing before I do it.&rdquo;" loading="lazy" src="/posts/linux-frustrated-loving/frustrated-lady-computer.jpeg#center"></p>
<p>I had no idea what I did wrong, but I read through the guide I printed, and realized retroactively that I should have done that initially. I learned about partitions, and realized with my skillset, there was no way to get the data I had lost back. I accepted that truth, and realized I had no hope of restoring a Windows install, because I had not been sent a recovery disk with the computer. I wiped the tears off my face and resolved to get Linux installed on my computer, no matter how difficult the task was.</p>
<p>I put the Ubuntu CD I had made back in the tray, cracked my knuckles like I saw people on TV do, and braced for impact. The installation window came back up, and this time, armed with my handy installation guide, managed to install Ubuntu. I didn’t do any fancy partitioning or anything, seems how I had just learned partitioning was even a thing, so Ubuntu was on my entire disk. I booted up my computer, and all was right in the world.</p>
<p>Actually, that’s not the case at all. Back in 2006, Linux was awesome, but had a ton of driver problems. My computer was a low end Dell with an Ethernet driver that was not included in the installation image. After many hours of developing my troubleshooting skills without an Internet connection, I used my brother’s working Windows computer (and I’d be lying if I said I wasn’t envious of his working computer, or that he had not decided to be adventurous with me) to research installing the driver for Ubuntu. I found some documentation for the model of Dell I was using, and found that there were a few other driver problems as well. I downloaded the drivers, put them on a USB stick, and printed the guide for installing the drivers on Ubuntu (what a year 2006 was, where we didn’t have smartphones to just pull up the guides when we needed them. So much wasted paper…).</p>
<p><img alt="YOU WILL NOT GET THE BEST OF ME, YOU STUPID MACHINE!" loading="lazy" src="/posts/linux-frustrated-loving/guy-screaming-at-monitor.jpeg#center"></p>
<p>Let’s recap. At this point, I had a CD burnt with the Ubuntu live image on it, a USB stick with a few drivers, and about 25 pages worth of documentation. I had failed the installation initially, destroyed my Windows partition, lost some data that was pretty important at the time (I had to redo a few assignments), and scared myself a little into thinking Linux might not be for me. Luckily, I am a bit (ok, very) defiant when it comes to computers besting me, and I was not about to let this low end Dell tell me who’s boss. This stupid computer was going to run Linux whether it liked it or not, and based on my experience trying to install Linux and getting the proper drivers, it did <em>not</em> want to run Ubuntu. Tough luck, you stupid Optiplex.</p>
<p>Back to the story! I popped the USB stick into the computer to install the drivers I had found online…only to find that in 2006, Ubuntu did not support FAT formatted drives out of the box. I punched myself in the face in frustration, and went back to my brother’s computer to learn about different file systems. I didn’t understand any of what I was reading, but it boiled down to me learning what the “Format” option does when right clicking a drive on Windows. I formatted the drive, and in my young stupidity, forgot to back up the drivers. After curling up in a ball and rolling around the floor in frustration, on the verge of giving up and accepting that my computer was currently as useful as an uncomfortable footrest, I decided this was all a learning process, and shifting my mentality from “why in the world do so many people love Linux” to “there must be a reason so many people love Linux,” I got back on my brother’s computer, downloaded the drivers again, and took my newly formatted EXT3 drive back to my room. I sat at the computer, prayed to any gods who were listening, and put the USB stick into the slot on my computer.</p>
<p><img alt="&ldquo;Computer god, please let this work, because if it doesn&rsquo;t, I&rsquo;m jumping out the window with this PC in my arms" loading="lazy" src="/posts/linux-frustrated-loving/guy-praying-to-monitor.jpeg#center"></p>
<p>A wave of relief rushed over me as I saw a neat little animation pop up on my desktop, confirming that my disk had properly mounted (of course, I didn’t know what “mounting” a disk was, but thankfully I didn’t have to play around in my FSTAB file, or else I would probably not be enjoying Linux today, stuck instead with Windows in Microsoft Land). I opened the drive, found my drivers, and moved them to my Downloads folder in Ubuntu. I double clicked them to install, just like you would on Windows, right?</p>
<p><img alt="WRONG" loading="lazy" src="/posts/linux-frustrated-loving/wrong.jpeg#center"></p>
<p>Of course it wouldn’t be that simple! What was I even thinking? Linux is a complex beast, and until you know what you’re doing and see the efficiency, Linux’s simplicity feels like complexity. Through my tears of frustration, I consulted the guide I had printed for installing the drivers on Ubuntu. Some thoughts running through my head while reading were “Shell? Terminal? Why do I have to type something in this weird program, just to install a driver?! What have I gotten myself into…” I decided not to think, and just to follow the guide. I found the Terminal program, opened it up, and noticed it looked a lot like the command prompt on Windows. I previously used the command prompt for the ping command exclusively, because on my Windstream connection (little plug: Windstream sucks. Don’t use.), pinging could help me determine if my computer was the problem, or if our connection was out, and based on past experiences with Windstream, it was usually the latter.</p>
<p>Anyway, with the terminal opened, the guide told me to “cd” to the Downloads folder, and use a command called “chmod +x” to tell the computer the driver files were executable. “Hold up…the computer doesn’t just know I want to run something? I have to actually tell it this file can be executed? Good lord, I hate Linux.” I think that is a more common thought for newbies than people realize/remember. It’s honestly a miracle and a testament to how great Linux is that any converters actually stay with it.</p>
<h2 id="getting-back-on-the-internet-and-next-steps">Getting Back on the Internet, and Next Steps</h2>
<p>So, my computer was finally ready to install the drivers. I had to type some weird command with a dot in front of it (./[driver name]) to get the driver to install. I later learned that the “./” portion of the command tells the computer to run whatever input you’re giving it through the shell.</p>
<p>A bunch of text started flying through the terminal, and I thought for sure I had either broken my computer again, or that I had learned how to hack, since this is how it looked in the movies. Of course, neither of those scenarios were my reality, but I let the commands run with faith that my computer would not catch fire in front of my eyes.</p>
<p><img alt="This looks pretty intimidating to anyone new to a terminal shell" loading="lazy" src="/posts/linux-frustrated-loving/terminal-output.jpeg#center"></p>
<p>When the command finally stopped running and the terminal was dutifully waiting for more commands, my network connection icon in the statusbar started spinning, and I got a little popup that my network connection was active. I sat stunned for a minute that I’d Forrest Gump’d my way into a working Linux installation. I opened an Internet browser (I had been using Firefox on Windows, so this was <em>one</em> thing I was familiar with in this new land), went to Google, and danced with joy that I had a working Internet connection.</p>
<p>I could go into more detail about the troubles I had learning Linux, but suffice it to say, learning Linux coming from never even hearing about it before was a long, arduous process of trial and extremely frustrating error. The thought of adjusting to the command line alone was daunting enough to keep me wondering if I had made a huge mistake, but after a couple days of using Ubuntu, I was captivated by the uniqueness and simultaneous familiarity. I love change, and I love experimenting with computers, and this was a whole new world for me. I got comfortable with the command line, and found it to be far superior to the click and drag interface I was used to, although people new to Linux will be happy to hear you can do pretty much everything graphically, especially on Ubuntu. No scary terminal window necessary!</p>
<h2 id="my-time-with-linux-since-then">My Time with Linux Since Then</h2>
<p>It didn’t take me very long to learn that Linux was just the core of the operating system, and there were loads of different “flavors,” or “distributions,” of Linux. I had started on Ubuntu, which I learned was an offshoot of the great Debian distribution, plus a few enhancements. Once I traced Ubuntu’s roots back to Debian, I found that there were many, <em>many</em> different offshoots of Debian, and even some that came from Ubuntu. The more I searched, the more I found that Linux is one hell of an ecosystem, with distributions for just about anything you can want. There are distributions crafted to be run solely as a server; distributions that value security and stability over cutting edge technology; distributions like Arch Linux that are always up to date, to the point of being unstable at times; and distributions with a nice mix of stability and freshness, like Fedora.</p>
<p><img alt="A graphic with a very small sampling of different Linux distributions" loading="lazy" src="/posts/linux-frustrated-loving/varied-linux-distros.jpeg#center"></p>
<p>I decided to try Fedora out, and found that Gnome 3 looked much nicer than Ubuntu’s desktop, which was based on the older Gnome 2. Ubuntu eventually created their own entirely unique environment, called Unity, and caused <em>quite</em> a divide in the community: some people loved the fresh, unique take on the desktop, and some people despised the fact that Ubuntu tried to pigeon hole them into change. One thing I’ve learned about the Linux community is that people really, really value their freedom of choice, and when a distribution makes an executive decision, they will hear loud and clear the displeasure of the people they pissed off.</p>
<p><img alt="Ubuntu’s “Unity” desktop was a major shock to some people with how different it was from their previous desktop" loading="lazy" src="/posts/linux-frustrated-loving/ubuntu-unity-desktop.jpeg#center"></p>
<p>Since trying Ubuntu and Fedora out, I have become what the community dubbed a “distro hopper.” Distro hoppers want to get their feet wet with as many distributions as they can find. I’ve tried ’em all, starting with Ubuntu, moving to Fedora, and from there travelling swiftly through <a href="https://linuxmint.com/">Linux Mint</a>, to <a href="http://www.debian.org/">Debian</a> when I felt I had learned enough about Linux to try the mother distribution; I tried <a href="http://zorinos.com/">ZorinOS</a> because it promised to be familiar to people coming from Windows, and they were right; I installed <a href="https://www.opensuse.org/">openSUSE</a> because I liked the green lizard, but I eventually made my way back to Debian-land and settled on Linux Mint. It felt fresh and clean, but still offered the simplicity and idiot-proof nature of Ubuntu. I stuck with Mint for a long time, but eventually the itch hit me again, and I started hopping around.</p>
<p><img alt="Linux Mint was my home for a long time, mostly because it had all the drivers I needed pre-packaged with the image" loading="lazy" src="/posts/linux-frustrated-loving/linux-mint-desktop.jpeg#center"></p>
<p>When I got bored of the safeness of Linux Mint, I branched out into fringe and new distributions. I found <a href="http://elementary.io/">elementaryOS</a>, which was, at the time, a very new branch from Ubuntu. Its main offering was a positively <em>gorgeous</em> interface, taking obvious inspiration from Apple’s Mac OS X (they deny that they were inspired by Apple’s OS, but it’s pretty apparent to anyone that they at least valued OS X’s clean, consistent interface and color scheme). I bounced from elementaryOS over to <a href="https://www.deepin.org/">DeepinOS</a>, which is a Chinese distribution that’s doing a lot of new and exciting things to the Ubuntu desktop world.</p>
<p>Eventually I got tired of hopping around and not finding the one distribution that fit all my needs. I wanted something that was stable, had up to date packages so I could try new things, was easy to manage, and was modifiable in a way I had not found yet. I am a compulsive modder, changing things just for the sake of change. I had heard of <a href="https://www.archlinux.org/">Arch Linux</a>, a distribution that the community likes to tout as “for hardcore users only.” There is surely a steep learning curve, but the documentation and community is so passionate about their distribution that the entire process of installing and maintaining an Arch Linux install is very demystified at this point. I had read about how customizable Arch Linux is, and decided this was surely the next step in my journey.</p>
<p><img alt="The main reason I picked Arch was their pretty logo" loading="lazy" src="/posts/linux-frustrated-loving/archlinux-logo.jpeg#center"></p>
<p>I downloaded the net install (a <em>very</em> small image that requires you to be connected to the Internet, where it then downloads the rest of the installation) and set to work. And good lord did I mess a lot of things up. Setting Arch Linux up is to this day one of the hardest, most time consuming, and frustrating things I have ever done. It took me <em>weeks</em> to get a working installation. After failing the first 5 times, I decided to try in a virtual machine. I would go through the Arch installer, get a system up and running, delete the virtual machine, and try again. I probably did this 20 times before I felt comfortable trying again on my computer.</p>
<p>And I still failed. I’ve only ever had 1 working Arch install, and at that point I was too scared to mess with it and screw things up even more. I read around, and a lot of people suggested <a href="https://antergos.com/">Antergos</a>, which is essentially a simple, graphical installer for Arch Linux that adds a repository for themes.</p>
<p><img alt="Antergos’s motto is very true, unlike 2006 Ubuntu’s “Linux for humans”" loading="lazy" src="/posts/linux-frustrated-loving/antergos-logo.jpeg#center"></p>
<p>The Cnchi graphical installer for Antergos was beautiful, and worked like a charm. I had a working Arch Linux install on my computer within 15 minutes. And as it turns out, Arch Linux is the absolute perfect distribution for me. It’s stable, it’s <em>faaaast</em>, it’s customizable beyond belief, and most importantly for me, it’s as up to date as you can be. As soon as a program is updated, it gets added to Arch Linux’s central repository, or its indescribably useful User Repository (AUR), and you can download it with a simple <em>pacman -S</em> command. I could write a whole post about my journey with Antergos, and perhaps I will…but for now, suffice it to say I had found my home.</p>
<p>With Arch Linux, I had everything I wanted. No more distro hopping to get a package update I wanted, or a theme that only worked with a certain base (Debian or Redhat, for instance). No more obliterating my installation with a simple update, which happened far too frequently in my time with openSUSE. Antergos was <em>it</em> for me. I grew to love how configurable it was, which initially was a very daunting task (pretty much everything is configured through the command line by editing text files).</p>
<h2 id="where-i-am-now-with-linux">Where I Am Now with Linux</h2>
<p>This post is getting long, as I forgot how passionate I am about Linux until I took this nostalgic trip down Linux-Hell memory lane.</p>
<p>Since I first accidentally wiped out my Windows installation, I have been treading deeper and deeper into the Linux forest. I have learned more about computers and their inner workings in my ~10 years with Linux than I ever knew with a Windows installation. I have lost data, formatted my hard drive every which way hundreds of times, found new uses for Linux in my everyday life, and set up my own home server using an ESXi host running a few Linux virtual machines (another post for another time).</p>
<p>I took the plunge and decided to use Linux as my main operating system a couple years ago, booting into Windows infrequently to use a couple of programs I simply can’t use to their full potential in Linux (namely FL Studio and a couple of Windows only games). It’s true that at first, Linux is scary. You have to learn the command line if you want to do anything besides basic Internet browsing and word processing; you have to read a lot of documentation and forum posts to learn how you stupidly messed up your installation, and you have to develop a tough skin to deal with the community, which is made up of some really grumpy old timers and a disproportionately large group of newcomers who ask a <em>lot</em> of questions. It’s a testament to Linux’s greatness that so many people put up with all of that, just to use the operating system on their computer.</p>
<p>Because of my love for Linux, I am now working towards learning what I need to learn to become a Linux Systems Administrator, where I will get to work with Linux computers and servers for a living, and learn even more about this wonderful software project that has broken so much ground over the years. I’m sure there will be many more long nights spent figuring out where I went wrong in an update, or why my configuration files aren’t working as intended, but I look forward to being tired the next day from staying up too late playing on my computer, feeling satisfied that I fixed whatever went wrong, and that I am still loving the Linux Life.</p>
<p><img alt="Got Linux?" loading="lazy" src="/posts/linux-frustrated-loving/got-linux.jpeg#center"></p>
]]></content:encoded>
    </item>
  </channel>
</rss>
