36 Commits

Author SHA1 Message Date
bf1498d3c5 Merge pull request 'Update config' (#2) from config into main
Reviewed-on: #2
2025-11-20 20:01:33 +00:00
0ffe8c334e add email address 2025-11-20 12:03:13 -07:00
9bb243e2ca add google verification ID 2025-11-20 12:02:11 -07:00
43b2c213e4 Merge pull request 'Publish "New Home Network Architecture"' (#1) from newhomenet into main
Reviewed-on: #1
2025-11-20 16:53:18 +00:00
b198db2725 edits to "New Home Network Architecture" 2025-11-20 08:00:48 -07:00
13372066c3 add "New Home Network Architecture" article 2025-11-19 11:29:40 -07:00
6d54cbef61 pull hugo-builder from harbor, fix version tag 2025-11-18 15:08:05 -07:00
3062cba541 upgrade Caddy to v2.10.2 2025-11-14 13:40:39 -07:00
f0810eb8e5 upgrade blowfish, golang 2025-11-14 13:37:18 -07:00
c8684d2d96 additional edits: new-website 2025-06-12 11:49:39 -06:00
550524cb1a add email address to website contact links 2025-06-12 10:19:52 -06:00
4918cef5e6 final edit: new-website 2025-06-10 09:41:15 -06:00
bbb21b34dd enable smartTOC 2025-06-09 15:35:08 -06:00
24d66c2fea enable menu highlight 2025-06-09 15:34:53 -06:00
df143c30cc upgrade blowfish to v2.86.0 2025-05-27 07:33:12 -06:00
9df2de9207 edit: new-website 2025-05-16 13:26:50 -06:00
217779d89a publish "new-website" post 2025-05-12 11:05:15 -06:00
18b99e91b3 accept all hosts in Caddy 2025-05-11 13:18:42 -06:00
6d7d641bc2 add /tls volume to Dockerfile 2025-05-10 14:50:43 -06:00
c67fe08a33 update Dockerfile
- base it on the custom git.brds.ca/d-b.ca/hugo-builder (as the gohugo container doesn't really work for this).
2025-05-10 09:56:27 -06:00
0218c47f2b update top-level README.md 2025-05-09 14:31:13 -06:00
dac497d685 add Dockerfile and Caddyfile for deployment container builds 2025-05-09 14:19:26 -06:00
1e6a3d2596 update default Gitea server 2025-05-08 16:09:57 -06:00
a63267ffc2 show article summaries by default 2025-05-08 13:45:24 -06:00
b470196abb adjust styling for lists and articles 2025-05-08 13:40:40 -06:00
30815a3408 add recent items to home page 2025-05-08 13:39:10 -06:00
0fce11557a remove the "bio" element 2025-05-08 11:39:37 -06:00
09ffa7afb0 add "Posts" menu item 2025-05-08 11:06:09 -06:00
789c5b434d add "posts" archetype for new posts 2025-05-08 11:02:43 -06:00
e8bce810a7 enable analytics with Umami 2025-05-08 09:07:16 -06:00
f4c596862f change layout, add background image 2025-05-07 08:44:00 -06:00
cf08d1cb08 disable theme attribution
- I'll cover this in an article
2025-05-07 07:44:07 -06:00
951de2e2b2 add favicon set 2025-05-07 00:04:06 -06:00
83293947f2 add headline and description 2025-05-07 00:03:26 -06:00
d73fe8a87a add logo 2025-05-06 10:25:49 -06:00
899e385b29 theme setting isn't necessary when using modules 2025-05-06 09:22:32 -06:00
27 changed files with 518 additions and 36 deletions

9
Caddyfile Normal file
View File

@@ -0,0 +1,9 @@
{
default_sni web
}
https:// {
tls /tls/tls.crt /tls/tls.key
root * /srv
file_server
}

16
Dockerfile Normal file
View File

@@ -0,0 +1,16 @@
# Package versions
ARG HUGO_VERSION="0.152.2"
ARG CADDY_VERSION="2.10.2"
# Stage 1: Build
FROM core.harbor.brds.ca/d-b.ca/hugo-builder:${HUGO_VERSION} AS builder
WORKDIR /project
COPY . .
RUN hugo --minify build
# Stage 2: Package
FROM docker.io/caddy:${CADDY_VERSION}
COPY Caddyfile /etc/caddy/Caddyfile
COPY --from=builder /project/public /srv
# tls.crt and tls.key should be mounted here at runtime.
VOLUME /tls

View File

@@ -1,3 +1,3 @@
# web # Drew Bowering's Website
My personal website at https://www.d-b.ca/ My personal website at [https://www.d-b.ca/](https://www.d-b.ca/)

6
archetypes/posts.md Normal file
View File

@@ -0,0 +1,6 @@
---
date: '{{ .Date }}'
draft: true
title: '{{ replace .File.ContentBaseName "-" " " | title }}'
description: ''
---

BIN
assets/img/BG_Jasper.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

View File

@@ -2,7 +2,6 @@
# Refer to the theme docs for more details about each of these parameters. # Refer to the theme docs for more details about each of these parameters.
# https://blowfish.page/docs/getting-started/ # https://blowfish.page/docs/getting-started/
theme = "github.com/nunocoracao/blowfish"
baseURL = "https://d-b.ca/" baseURL = "https://d-b.ca/"
languageCode = "en" languageCode = "en"
defaultContentLanguage = "en" defaultContentLanguage = "en"

View File

@@ -9,20 +9,20 @@ title = "Drew Bowering"
isoCode = "en" isoCode = "en"
rtl = false rtl = false
dateFormat = "2 January 2006" dateFormat = "2 January 2006"
# logo = "img/logo.png" logo = "img/d-bca_logo_light.png"
# secondaryLogo = "img/secondary-logo.png" secondaryLogo = "img/d-bca_logo_dark.png"
# description = "My awesome website" description = "Drew Bowering's personal website"
# copyright = "Copy, _right?_ :thinking_face:" # copyright = "Copy, _right?_ :thinking_face:"
[params.author] [params.author]
name = "Drew Bowering" name = "Drew Bowering"
# email = "drew@d-b.ca" email = "drew@d-b.ca"
image = "img/DrewBowering.jpg" image = "img/DrewBowering.jpg"
# imageQuality = 96 # imageQuality = 96
# headline = "I'm only human" headline = "IT Architect, Developer, Canoeist, Tubist"
bio = "A little bit about me" # bio = "A little bit about me"
links = [ links = [
# { email = "mailto:hello@your_domain.com" }, { email = "mailto:drew@d-b.ca" },
# { link = "https://link-to-some-website.com/" }, # { link = "https://link-to-some-website.com/" },
# { amazon = "https://www.amazon.com/hz/wishlist/ls/wishlist-id" }, # { amazon = "https://www.amazon.com/hz/wishlist/ls/wishlist-id" },
# { apple = "https://www.apple.com" }, # { apple = "https://www.apple.com" },

View File

@@ -10,10 +10,10 @@
# overridden by providing a weight value. The menu will then be # overridden by providing a weight value. The menu will then be
# ordered by weight from lowest to highest. # ordered by weight from lowest to highest.
#[[main]] [[main]]
# name = "Blog" name = "Posts"
# pageRef = "posts" pageRef = "posts"
# weight = 10 weight = 10
#[[main]] #[[main]]
# name = "Parent" # name = "Parent"

View File

@@ -21,14 +21,14 @@ disableImageOptimization = false
disableTextInHeader = false disableTextInHeader = false
# backgroundImageWidth = 1200 # backgroundImageWidth = 1200
# defaultBackgroundImage = "IMAGE.jpg" # used as default for background images defaultBackgroundImage = "img/BG_Jasper.jpg" # used as default for background images
# defaultFeaturedImage = "IMAGE.jpg" # used as default for featured images in all articles # defaultFeaturedImage = "IMAGE.jpg" # used as default for featured images in all articles
# highlightCurrentMenuArea = true highlightCurrentMenuArea = true
# smartTOC = true smartTOC = true
# smartTOCHideUnfocusedChildren = true smartTOCHideUnfocusedChildren = false
giteaDefaultServer = "https://git.fsfe.org" giteaDefaultServer = "https://git.brds.ca"
forgejoDefaultServer = "https://v8.next.forgejo.org" forgejoDefaultServer = "https://v8.next.forgejo.org"
[header] [header]
@@ -37,20 +37,20 @@ forgejoDefaultServer = "https://v8.next.forgejo.org"
[footer] [footer]
showMenu = true showMenu = true
showCopyright = true showCopyright = true
showThemeAttribution = true showThemeAttribution = false
showAppearanceSwitcher = true showAppearanceSwitcher = true
showScrollToTop = true showScrollToTop = true
[homepage] [homepage]
layout = "profile" # valid options: page, profile, hero, card, background, custom layout = "background" # valid options: page, profile, hero, card, background, custom
#homepageImage = "IMAGE.jpg" # used in: hero, and card #homepageImage = "IMAGE.jpg" # used in: hero, and card
showRecent = false showRecent = true
showRecentItems = 5 showRecentItems = 5
showMoreLink = false showMoreLink = false
showMoreLinkDest = "/posts/" showMoreLinkDest = "/posts/"
cardView = false cardView = false
cardViewScreenWidth = false cardViewScreenWidth = false
layoutBackgroundBlur = false # only used when layout equals background layoutBackgroundBlur = true # only used when layout equals background
[article] [article]
showDate = true showDate = true
@@ -60,8 +60,8 @@ forgejoDefaultServer = "https://v8.next.forgejo.org"
showDateUpdated = false showDateUpdated = false
showAuthor = true showAuthor = true
# showAuthorBottom = false # showAuthorBottom = false
showHero = false showHero = true
# heroStyle = "basic" # valid options: basic, big, background, thumbAndBackground heroStyle = "background" # valid options: basic, big, background, thumbAndBackground
layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground
layoutBackgroundHeaderSpace = true # only used when heroStyle equals background layoutBackgroundHeaderSpace = true # only used when heroStyle equals background
showBreadcrumbs = false showBreadcrumbs = false
@@ -74,7 +74,7 @@ forgejoDefaultServer = "https://v8.next.forgejo.org"
showPagination = true showPagination = true
invertPagination = false invertPagination = false
showReadingTime = true showReadingTime = true
showTableOfContents = false showTableOfContents = true
# showRelatedContent = false # showRelatedContent = false
# relatedContentLimit = 3 # relatedContentLimit = 3
showTaxonomies = false showTaxonomies = false
@@ -84,12 +84,12 @@ forgejoDefaultServer = "https://v8.next.forgejo.org"
showZenMode = false showZenMode = false
[list] [list]
showHero = false showHero = true
# heroStyle = "background" # valid options: basic, big, background, thumbAndBackground heroStyle = "background" # valid options: basic, big, background, thumbAndBackground
layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground layoutBackgroundBlur = true # only used when heroStyle equals background or thumbAndBackground
layoutBackgroundHeaderSpace = true # only used when heroStyle equals background layoutBackgroundHeaderSpace = true # only used when heroStyle equals background
showBreadcrumbs = false showBreadcrumbs = false
showSummary = false showSummary = true
showViews = false showViews = false
showLikes = false showLikes = false
showTableOfContents = false showTableOfContents = false
@@ -138,8 +138,8 @@ forgejoDefaultServer = "https://v8.next.forgejo.org"
# domain = "llama.yoursite.com" # domain = "llama.yoursite.com"
[umamiAnalytics] [umamiAnalytics]
# websiteid = "ABC12345" websiteid = "5ff2ac75-9399-4aae-a0c2-c2799674ebcb"
# domain = "llama.yoursite.com" domain = "analytics.brds.ca"
# dataDomains = "yoursite.com,yoursite2.com" # dataDomains = "yoursite.com,yoursite2.com"
# scriptName = "" # scriptName = ""
# enableTrackEvent = true # enableTrackEvent = true
@@ -156,7 +156,7 @@ forgejoDefaultServer = "https://v8.next.forgejo.org"
# globalWidgetPosition = "Right" # globalWidgetPosition = "Right"
[verification] [verification]
# google = "" google = "w3v6pJQgijQRPkhgVV6SIOJEPIpAR9ase26ri4tPkS8"
# bing = "" # bing = ""
# pinterest = "" # pinterest = ""
# yandex = "" # yandex = ""

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 349 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 307 KiB

View File

@@ -0,0 +1,241 @@
---
title: "New Home Network Architecture"
date: "2025-11-20T07:30:00-07:00"
description: "The architecture of my current home network"
summary: "I decided to rearchitect my home network, and this is the result."
---
Almost two years ago, I decided to undertake another major overhaul of my home
network. It is the latest step in the long evolution of my personal systems,
and is now in a state that I'm fairly happy with.
## History
I've been networking my computers together for decades. The first example that
could reasonably be called a network was around 1996, where I connected my
Amiga 1200 and Amiga 3000T together over a null-modem serial cable and ran IP
between them. I eventually got some Ethernet hardware and ran mostly simple
flat networks for a while.
### Orthodoxy
After working professionally designing and building networks and IT systems, I
had learned a few rules. Networks in particular always consisted of several
key elements:
- **Three Tiers**. You needed Core, Distribution, and Access switches. This
helps to scale the network and keep things well balanced.
- **VLANs**. Every packet needs to go through a VLAN. VLANs keep the network
segregated for security, and allow for smaller broadcast domains.
- **Firewalls**. The network has to protect the vulnerable endpoints by
blocking packets that aren't permitted by policy.
- **Virtualization**. Virtualize everything to decouple the systems from the
underlying infrastructure, keeping them portable.
Naturally, I took these ideas home and built my personal networks accordingly.
### Something's not right
Eventually, I had built myself a network that looked something like the
diagram below. I kept to the principles I was familiar with, and this was the
result.
![First Network Diagram](first_net.svg 'My last "orthodox" network architecture')
I had VLANs for everything coming from the VM hosts to the physical switches.
Traffic would loop through the tiers (in, out, and back in), routing
everything like it's supposed to, but this redundancy introduced unecessary
complexity. I had more VMs acting as routers than I had VMs doing productive
activity.
When I decided that I would like to start using IPv6 in my network, everything
doubled. I kept the IPv4 and IPv6 traffic on separate VLANs, and had separate
routers for everything, doubling what's in that diagram. It didn't take me
long to notice that this wasn't working out, and started to think about a new
approach.
## New Design
When I started to think about what I really needed in my home network, I
came up with several principles:
- **IPv6 First**. Using IPv6 makes so many things simpler. No NAT. Subnets
don't run out of addresses. Link-local addressing that works, and is useful.
No DHCP. *No NAT!*
- **Zero Trust**. I'm a fan of zero-trust architectures. When you place the
responsibility for security on the network, it tends to get complicated. You
make physical design choices around isolating packets on the right segments
and getting them to the right firewalls, where the network should focus on
moving traffic through it as quickly as possible. This principle of simply
getting packets to where they need to be is how the Internet scales so well.
We can keep physical LANs simple and efficient like this, and leave the
security concerns to the endpoints. The endpoints need to be secure anyways,
and we now have more modern and effective tools to help us.
- **Network Fabric**. Rather than redoing the same 3-tier model I was used to,
I wanted to do something more efficient. I was inspired by an article
written at Facebook about their
["data center fabric"](https://engineering.fb.com/2014/11/14/production-engineering/introducing-data-center-fabric-the-next-generation-facebook-data-center-network/)
architecture. This is obviously much larger than what anyone needs in a home
network, but I thought that these were good ideas that I could use.
- **Routing Protocols**. I've traditionally used [OSPF](https://en.wikipedia.org/wiki/Open_Shortest_Path_First)
in the networks I've operated. When I decided to implement IPv6 in the
network, I was using [Quagga](https://www.nongnu.org/quagga/) for the
routing software. It doesn't support OSPF areas in OSPFv3 for IPv6, which
made me reconsider. I settled on [IS-IS](https://en.wikipedia.org/wiki/IS-IS),
as it supports both IPv4 and IPv6 at the same time, and could do everything
that I needed it to do.
### Refresh, Version 1
My first refreshed network design looked like this:
![New Network Diagram, Version 1](first_new_net.svg 'My first updated network architecture')
I did away with all of the traditional complexity, and established two network
"fabrics" that all of the traffic would pass through. The fabrics do not
connect directly at layer 2, each side is separate. Each fabric is a single
flat network, there are no VLANs.
These were the key design decisions:
- **Routing over switching**. Every physical device connecting into the fabric
switches would conform to a basic set of requirements:
1. It will act as a router, running IS-IS to communicate with every other
device on the fabric switches.
2. Each endpoint uses a single loopback address as its network identity. It
advertises this address to the other nodes, as well as the subnets that
it can route to.
3. Routes are advertised over both fabrics, enabling [ECMP](https://en.wikipedia.org/wiki/Equal-cost_multi-path_routing)
for higher availability and bandwidth.
- **IPv6 first**. The access routers and Wifi routers only had IPv6 subnets
available for client devices. This allowed me to do away with DHCP services
on the network, only using [SLAAC](https://en.wikipedia.org/wiki/IPv6_address#Stateless_address_autoconfiguration_(SLAAC)).
Access to IPv4-only resources was through the use of
[DNS64 and the NAT64 gateway](https://en.wikipedia.org/wiki/IPv6_transition_mechanism#NAT64).
## Next Generation
At this point, I was fairly happy with the result. The network was efficient
and much easier to maintain. It was faster, thanks to ECMP and having fewer
hops. As I was using it however, I started to think about the next set of
improvements.
- **Single point routers**. I had only single devices acting as my edge,
access, and Wifi routers. I wanted some redundancy in case one failed, and
to make maintenance more transparent with the ability to fail over.
- **Virtual Machines**. Most of my workloads were set up as virtual machines.
I wanted to migrate to [Kubernetes](https://kubernetes.io/) as everything
I was running could be run there, along with many other benefits.
- **NAT64**. Here I was running IPv6 to get away from needing NAT, but I still
needed NAT. This setup was mostly working fine, but there were a few small
irritations:
- There are not very many NAT64 implementations. I was using [JooL](https://jool.mx/);
it's a non-standard Linux kernel module, and it's not really actively
developed anymore.
- The path from the NAT64 gateway out the Edge router is still IPv4, and I
still need to do NAT for IPv4 at the edge.
- Applications connecting directly to an IPv4 address weren't able to do so.
I could use [464XLAT](https://en.wikipedia.org/wiki/IPv6_transition_mechanism#464XLAT)
on endpoints that supported it, but it's yet another thing to set up.
- There's the occasional device that still doesn't support IPv6, or doesn't
support it properly.
- **BGP**. I was purely using IS-IS throughout the network, but Kubernetes
CNIs that work on bare metal systems like mine rely on BGP to advertise
routes into the network. I'd have to work out how to incorporate this.
- **Easier WiFi**. I was using a WiFi router running [OpenWRT](https://openwrt.org/),
connecting to both fabrics and running IS-IS just like everything else.
OpenWRT is great, but it is challenging to keep devices up-to-date.
- **Load Balancing**. I didn't have any solution for establishing network
load balancing for scale and availability.
### Refresh, Version 2
Incorporating the improvements I wanted to make, here is the resulting network
architecture:
![New Network Diagram, Version 2](second_new_net.svg 'My current network architecture')
The key changes are:
- **Redundant Routers**. I doubled up the edge and access routers. They can
effectively divide the load and fail over when needed.
- **Anycast for Load Balancing**. I've standardized on making use of
[Anycast](https://en.wikipedia.org/wiki/Anycast) addressing for creating
load balanced and redundant network services. I'm using this a few ways:
- The API server for my Kubernetes cluster is on an anycast address. This
address is advertised from the three control plane nodes.
- Kubernetes `LoadBalancer` type services allocate an address from a pool
and advertise it out from any node that can accept traffic for the
service.
- My recursive DNS servers providing DNS lookups for the network are on two
anycast addresses. Each edge router runs an instance and advertises one of
the addresses; this is so I can "bootstrap" the network from the edge
routers. I also run the DNS service under Kubernetes, which advertises the
same anycast addresses using ordinary `LoadBalancer` services.
- **IS-IS and BGP**. I took a few passes at getting this right. I first tried
to move fully from IS-IS to BGP only. This meant setting up peering
using IPv6 link local addresses, which worked, but it was a bit flaky under
[FRR](https://frrouting.org/). I settled on using IS-IS on the fabric
interfaces only to exchange the IPv6 loopback addresses of each node. I use
the globally routable loopback addresses for the BGP peering, which is much
easier in practice. All of the other routes (access subnets, Kubernetes
networks, anycast addresses, defaults from the edge routers) are exchanged
using BGP.
- **No NAT64**. I decided to do away with NAT64 and provide dual-stack
connectivity to the access networks. I set up [Kea](https://www.isc.org/kea/)
as a cluster on the two access routers, which is thankfully rather low
maintenance.
- **BGP Extended-Nexthop**. An added bonus to using BGP the way that I am is
that I could make use of the [BGP extended-nexthop](https://datatracker.ietf.org/doc/html/rfc8950)
capability. The old network with only IS-IS still required me to define IPv4
subnets on the switching fabrics, nodes used IPv4 addresses as the next hop
gateway addresses for IPv4 routes. With the extended-nexthop capability in
BGP, it uses the IPv6 link-local addresses for the next hop under both IPv4
and IPv6.
### High Availability
To migrate from single routers to redundant pairs, I needed to figure out a
few things.
#### Default Routes
With a single edge router, this was easy. With two, it's a bit of a puzzle. My
ISP doesn't actually provide fully routed IPv6 connectivity with my class of
service. I do get static IPv4 addresses, however. I've been using Hurricane
Electric's [tunnel broker](https://tunnelbroker.net/) service to get a routed
`/48` IPv6 subnet.
With a pair of edge routers, I've set them up with four static IPv4 addresses
on their Internet-facing interfaces. Each router gets one address. I then have
two [VRRP](https://en.wikipedia.org/wiki/Virtual_Router_Redundancy_Protocol)
interfaces, one that I use to terminate the IPv6 tunnel, and the other I use
for all IPv4 traffic. When both routers are up and running, one will have the
IPv6 tunnel and the other will have the IPv4 interface. Each one advertises a
default route for the address family it's taking care of. If one goes down,
the interface will fail over and everything reconverges rather quickly. IPv6
connections are unaffected, as the routing is stateless and traffic continues
to flow normally. IPv4 connections may get interrupted as the NAT state is
lost.
#### Access Routers
The interfaces facing the client machines provide connectivity for both IPv4
and IPv6.
The IPv6 configuration is much simpler. FRR can be configured to send router
advertisements to the subnets. Both routers are configured to advertise their
presence, as well as the subnet prefixes and DNS information. Client machines
will pick these up, and then have both routers as their default gateways.
While IPv6 configuration is seamless, IPv4 relies on VRRP to share a `".1"`
default gateway address, which, though functional, lacks the elegance of
IPv6's stateless design.
## It Works
After I got this all in place, it was finally possible to build myself a
working Kubernetes cluster and migrate all of my old services over to it. The
transition to Kubernetes not only streamlined service management but also laid
the foundation for future scalability and automation. I'll get into that
adventure in the next series of articles.

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 234 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 KiB

View File

@@ -0,0 +1,198 @@
---
title: "New Website"
date: "2025-06-12T08:00:00-06:00"
description: "There's finally a new website at d-b.ca."
summary: "I've published my personal website. A brief history of some past endeavours, and some details on the technology behind the new site."
---
I've finally published a proper website at [https://d-b.ca/](https://d-b.ca/).
The last time I had anything live on this domain was over 20 years ago,
according to the [Wayback Machine](https://web.archive.org/). What took so
long? The truth is, my interests have shifted over the years, and I've been
learning a lot. This site, while useful in its own right, is really a
culmination of a personal platform I've developed over time.
## History
My first personal website was developed while I was a student at the
[University of Alberta](https://ualberta.ca), near the end of the previous
century. The web was still in its early stages, but the university provided
students with the means to publish web content. It was mostly a novelty at the
time and didn't last beyond my time at school, but it sparked my interest in
Internet technologies and their applications.
That early site included one interesting feature. I developed a mechanism to
automatically update a page every time I logged into one of the school's
computers, so my friends could find me if they wanted to. At the time,
dynamically updating websites was a tricky thing. The most common way was
using [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface), which is
like having a tiny program run every time someone requested a page. The
university did not allow student sites to use it. So, I wrote some shell
scripts that were called as part of my login and logout scripts that would
generate the static HTML file and write it to my web content directory. It
needed to handle cases like multiple logins, and the logout script didn't
always get triggered, so I'd have to keep an eye on it for stale entries.
### Self-Hosting
I've always been an avid self-hoster. Learning by getting a system up and
running works well for me. I also value the privacy and control that it
provides. Plus, it's much cheaper for me in the long run!
It really began when I was working at a local Internet service provider. I was
able to get a special deal on a good broadband connection at home, which
included a small network block (a `/28`, consisting of 16 IP addresses) that I
could use. I dedicated my largest machine as my server and developed several
services, including a new website.
My website at that time wasn't fancy, and was geared primarily towards
experimentation. I developed a simple content management system from scratch
in PHP, which I used to publish a blog. It also integrated with mailing
lists, another area I was exploring at the time.
### D-B.CA
I hadn't registered a domain name of my own in those early days, so everything
resided under a friend's domain. In late 2002, I decided to finally register
one of my own, primarily so I could have a stable email address. I wanted to
incorporate the initials in my name, so I came up with `d-b.ca`. This was a
compromise because someone was squatting on `db.ca` at the time, and, to my
knowledge, they still are.
Early on, I focused mostly on operating my email services and other
experiments, with little attention paid to a website. There were a few test
pages at times, but nothing substantial.
## Modern Technology
One of the projects I've been following is [Hugo](https://gohugo.io/). I've
seen and worked with various web content management systems in the past, and
they often feel cumbersome and present security concerns. Hugo is an example
of a "Static Site Generator." Think of it like this: instead of creating web
pages on the fly every time someone visits, Hugo takes all the raw content and
turns it into a set of ready-to-serve files, much like a compiler turns code
into an executable program. The resulting static resources can be served as
regular files from any web service, without the need for dynamically
generating content upon request from a database, as traditional CMS systems
do.
Using Hugo is much easier with a solid base template. There are
[many to choose from](https://themes.gohugo.io/), including the one I've
selected here called ["Blowfish"](https://blowfish.page/). I like how it
looks, and it supports the style of site that this is very well.
Another benefit of a static site generator is that all the sources for the
site can be treated like software code, making it simple to use development
tools like [Git](https://git-scm.com/) for version control. I keep the sources
for this site in a public repository on my own Git server. Feel free to take a
look:
{{< gitea repo="d-b.ca/web" >}}
### CI/CD
I've also set up a CI/CD pipeline to build and deploy the site whenever
changes are made to the source repository. What does this mean?
**CI** = *Continuous Integration*
> This is the practice of frequently integrating changes into a source
> repository. The changes are checked, assembled, and packaged through
> automated processes. [More information](https://martinfowler.com/articles/continuousIntegration.html)
**CD** = *Continuous Delivery*
> This is the capability of being able to take new changes (such as the
> outputs of the CI process) and getting them deployed and running
> automatically. [More information](https://continuousdelivery.com/)
The CI portion is triggered by a push to the
[`web`](https://git.brds.ca/d-b.ca/web) repository. It runs an automated
workflow that builds the site and packages the resulting artifacts into a
container image based on the [Caddy](https://caddyserver.com/) web server.
A container image is like a pre-packaged software environment that ensures the
website runs consistently regardless of the underlying infrastructure. The
resulting image contains everything the website needs to operate and can run
on any infrastructure that can support it, such as my own laptop or my server
cluster.
The build container with Hugo is another image that is only used to create the
website container. I maintain it in this repository:
{{< gitea repo="d-b.ca/hugo-builder" >}}
After the workflow has built the container image for running the website, it
updates the CD GitOps repository to deploy this new version immediately to a
private staging site. Another definition:
> **GitOps** refers to the practice of managing infrastructure automation by
> keeping machine-readable descriptions of the intended infrastructure in a
> version-controlled Git repository. A CD system will monitor the repository
> for changes, immediately adding, modifying, or removing infrastructure to
> bring the state of the operational system into alignment with the source
> description.
There are many benefits to managing infrastructure this way. Changes are
automatically tracked because everything is stored in an existing code
repository system that's specifically designed for managing and tracking
change. Problematic changes can be reverted easily by reverting the change in
the repository. Automation keeps things in sync at all times - any changes
made manually outside of this system are immediately spotted and removed.
When I want to publish the new version as the production website, I use my
regular private production GitOps repository to update the image version tag,
and the rest happens automatically. The CD repository for the staging site is
public, you're welcome to check it out here:
{{< gitea repo="d-b.ca/db-cd" >}}
#### Pipeline Diagram
{{< mermaid >}}
flowchart TB
subgraph GIT [Git Repository]
WR[(Web)]
CDR[(CD)]
end
WP(Push Web Updates)-->WR
WP ~~~ HBI
subgraph CI [CI Workflow]
CIP[Pull Source
Repository]-->BWI[Build Web
Image]
BWI-->PWI[Push Web
Image]
PWI-->UCD[Update CD
Repository]
end
PWI-->WI
WR-->CIP
subgraph DOCKER [Image Repository]
HBI((Hugo
Build))
WI((Web))
end
HBI-->BWI
UCD-->CDP(Push Image
Update)
CDP-->CDR
CDR-->CDPull
subgraph CD [CD Process]
CDPull[Pull Source Repository]-->DWI[Deploy Web Image]
end
WI-->DWI
{{< /mermaid >}}
## Underlying Platform
In future articles, I'll describe the evolution of the physical network and
systems this site is running on, and how they enabled me to build a
[Kubernetes](https://kubernetes.io/) cluster to scale this site and run other
services, all on hardware I assembled myself!

4
go.mod
View File

@@ -1,5 +1,5 @@
module git.brds.ca/drew/web module git.brds.ca/drew/web
go 1.24.2 go 1.25.4
require github.com/nunocoracao/blowfish/v2 v2.85.1 // indirect require github.com/nunocoracao/blowfish/v2 v2.92.0 // indirect

4
go.sum
View File

@@ -1,2 +1,2 @@
github.com/nunocoracao/blowfish/v2 v2.85.1 h1:+wdHZ6kozmybTprY8RHFdumeg4lB8r5xmRy2FfQcweo= github.com/nunocoracao/blowfish/v2 v2.92.0 h1:1EgHMRaY6VI438TIAN/5luNx16lg1e0Lrbi+6kDxpdA=
github.com/nunocoracao/blowfish/v2 v2.85.1/go.mod h1:4SkMc+Ht8gpQCwArqiHMBDP3soxi2OWuAhVney+cuyk= github.com/nunocoracao/blowfish/v2 v2.92.0/go.mod h1:4SkMc+Ht8gpQCwArqiHMBDP3soxi2OWuAhVney+cuyk=

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

BIN
static/apple-touch-icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.2 KiB

BIN
static/favicon-16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 592 B

BIN
static/favicon-32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
static/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

1
static/site.webmanifest Normal file
View File

@@ -0,0 +1 @@
{"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"}