<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Tom Williams</title>
    <description>The latest articles on Forem by Tom Williams (@tomwilliamscloud).</description>
    <link>https://forem.com/tomwilliamscloud</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1468915%2Ffd5ae630-0f0d-4155-9ac9-70df4133e2a5.png</url>
      <title>Forem: Tom Williams</title>
      <link>https://forem.com/tomwilliamscloud</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/tomwilliamscloud"/>
    <language>en</language>
    <item>
      <title>Turning a Mac mini Into a Home Server for Self-Hosted Services</title>
      <dc:creator>Tom Williams</dc:creator>
      <pubDate>Sun, 17 May 2026 13:59:41 +0000</pubDate>
      <link>https://forem.com/tomwilliamscloud/turning-a-mac-mini-into-a-home-server-for-self-hosted-services-4pi2</link>
      <guid>https://forem.com/tomwilliamscloud/turning-a-mac-mini-into-a-home-server-for-self-hosted-services-4pi2</guid>
      <description>&lt;p&gt;The Mac mini is one of the more underrated pieces of homelab hardware you can buy. It's small, near-silent, sips power, and ships with an absurd amount of CPU per watt thanks to Apple Silicon. I've been running mine as a 24/7 home server for a while now, and this post is the writeup I wish I'd had when I started.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why a Mac mini and not a Raspberry Pi or a NUC?
&lt;/h2&gt;

&lt;p&gt;The honest answer: I already had one sitting on a shelf. But there are a few reasons it turned out to be a great fit:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Performance per watt.&lt;/strong&gt; An M-series Mac mini will idle around 4–7W and still rip through container workloads that would make a Pi cry.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It's silent.&lt;/strong&gt; No fans spinning up when Plex transcodes or when a backup job kicks off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;macOS is a real Unix.&lt;/strong&gt; You get Homebrew, launchd, ZFS via OpenZFS if you really want it, and a familiar shell environment.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It just stays up.&lt;/strong&gt; Mine has been running for months without a reboot beyond the occasional OS update.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The trade-offs are real though — you're paying Apple prices, you don't get ECC RAM, and storage expansion means hanging things off USB or Thunderbolt. If you need 40TB of spinning rust, build a NAS. For everything else, the Mac mini is excellent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Initial macOS setup
&lt;/h2&gt;

&lt;p&gt;A few settings to change before anything else:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Disable sleep.&lt;/strong&gt; In System Settings → Energy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Prevent automatic sleeping when the display is off: &lt;strong&gt;on&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Start up automatically after a power failure: &lt;strong&gt;on&lt;/strong&gt;
&lt;/li&gt;
&lt;li&gt;Wake for network access: &lt;strong&gt;on&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Enable auto-login&lt;/strong&gt; so the machine comes back up unattended after a power blip. Yes, this trades some security for uptime — make sure the box lives somewhere physically safe.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Turn on Remote Login (SSH)&lt;/strong&gt; in System Settings → General → Sharing. While you're there, give the machine a sensible hostname.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Install the command line tools&lt;/strong&gt; and Homebrew:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;xcode-select &lt;span class="nt"&gt;--install&lt;/span&gt;
/bin/bash &lt;span class="nt"&gt;-c&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh&lt;span class="si"&gt;)&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Container runtime: OrbStack
&lt;/h2&gt;

&lt;p&gt;I run almost everything in containers, and on Apple Silicon, &lt;strong&gt;OrbStack&lt;/strong&gt; beats Docker Desktop handily. It's faster to start, uses less RAM, has better filesystem performance, and the networking just works.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; orbstack
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Set it to start at login and you're done. The &lt;code&gt;docker&lt;/code&gt; and &lt;code&gt;docker compose&lt;/code&gt; CLIs work exactly as you'd expect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Reverse proxy: Caddy
&lt;/h2&gt;

&lt;p&gt;For routing traffic to services and handling TLS, Caddy is the path of least resistance. A single &lt;code&gt;Caddyfile&lt;/code&gt; and you have automatic HTTPS from Let's Encrypt for every service.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;jellyfin.home.example.com {
    reverse_proxy localhost:8096
}

homeassistant.home.example.com {
    reverse_proxy localhost:8123
}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I run Caddy in a container alongside everything else, with the config mounted from &lt;code&gt;~/srv/caddy/Caddyfile&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Remote access without opening ports: Tailscale
&lt;/h2&gt;

&lt;p&gt;This is the single most important piece of the setup. &lt;strong&gt;Do not&lt;/strong&gt; port-forward your home router to expose services to the internet. Instead, install Tailscale on the Mac mini and on every device you want to reach it from:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;--cask&lt;/span&gt; tailscale
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now your Mac mini has a stable &lt;code&gt;100.x.x.x&lt;/code&gt; IP that's only reachable by your own devices. Combine it with &lt;a href="https://tailscale.com/kb/1081/magicdns/" rel="noopener noreferrer"&gt;MagicDNS&lt;/a&gt; and you can hit &lt;code&gt;http://mac-mini:8123&lt;/code&gt; from your phone, anywhere in the world, with no firewall changes.&lt;/p&gt;

&lt;p&gt;For services I want family members to reach, Tailscale's Funnel feature exposes a single service to the public internet through their edge, with TLS handled for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  The services I actually run
&lt;/h2&gt;

&lt;p&gt;Here's the current lineup, all in Docker Compose:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Service&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Jellyfin&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Media server. Hardware transcoding works on Apple Silicon with the right flags.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Home Assistant&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Smart home brain. Runs in a container, talks to Zigbee via a USB stick.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Pi-hole&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Network-wide ad blocking. DNS for the whole house points here.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Paperless-ngx&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;OCR'd document archive. Scan once, search forever.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Vaultwarden&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Self-hosted Bitwarden-compatible password manager.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Uptime Kuma&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Tells me when something I forgot about has fallen over.&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Syncthing&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Folder sync across all my machines without going through the cloud.&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each one is a folder in &lt;code&gt;~/srv/&lt;/code&gt; with its own &lt;code&gt;docker-compose.yml&lt;/code&gt; and persistent volumes underneath. Boring, predictable, easy to back up.&lt;/p&gt;

&lt;h2&gt;
  
  
  Storage and backups
&lt;/h2&gt;

&lt;p&gt;My internal SSD holds the OS, container images, and small databases. For media and bulk data I have a Thunderbolt enclosure with a couple of SSDs in it, mounted at &lt;code&gt;/Volumes/data&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;For backups I run two layers:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Time Machine&lt;/strong&gt; to a separate external drive, for the whole system.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Restic&lt;/strong&gt; to Backblaze B2 for the irreplaceable stuff — Vaultwarden, Paperless, Home Assistant configs, photos. Encrypted, deduplicated, cheap.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;A nightly &lt;code&gt;launchd&lt;/code&gt; job kicks off the Restic run and pings a healthcheck URL when it succeeds. If the ping doesn't arrive, Uptime Kuma yells at me.&lt;/p&gt;

&lt;h2&gt;
  
  
  Monitoring
&lt;/h2&gt;

&lt;p&gt;Nothing fancy: &lt;strong&gt;Uptime Kuma&lt;/strong&gt; for "is the service responding," and &lt;strong&gt;Beszel&lt;/strong&gt; for lightweight host metrics (CPU, RAM, disk, container health). Both run in containers, both have web UIs I can hit over Tailscale.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Buy the bigger SSD.&lt;/strong&gt; Container images, databases, and Time Machine snapshots eat space faster than you think.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Get the Thunderbolt enclosure sooner.&lt;/strong&gt; USB 3 is fine until you're moving real volumes of data.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Document the compose files in a git repo from day one.&lt;/strong&gt; Past-me thought he'd remember which env vars he set. Past-me was wrong.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Wrapping up
&lt;/h2&gt;

&lt;p&gt;A Mac mini won't replace a rack of Dell servers, but for a home setup that quietly hosts a dozen useful services and disappears into a shelf, it's hard to beat. The combination of low power draw, silent operation, and a real Unix environment makes it a genuinely lovely machine to run things on.&lt;/p&gt;

&lt;p&gt;If you've got one gathering dust, give it a job.&lt;/p&gt;

</description>
      <category>infrastructure</category>
      <category>performance</category>
      <category>sideprojects</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Lessons from Migrating 9TB of File Shares to FSx</title>
      <dc:creator>Tom Williams</dc:creator>
      <pubDate>Sun, 22 Mar 2026 14:00:29 +0000</pubDate>
      <link>https://forem.com/tomwilliamscloud/lessons-from-migrating-9tb-of-file-shares-to-fsx-4230</link>
      <guid>https://forem.com/tomwilliamscloud/lessons-from-migrating-9tb-of-file-shares-to-fsx-4230</guid>
      <description>&lt;p&gt;Migrating a Windows file server sounds straightforward until you're staring at 9TB of data across 14 shares and trying to work out what's actually worth moving.&lt;/p&gt;

&lt;p&gt;This is what I learned doing exactly that — moving a legacy EC2-hosted Windows file server to FSx for Windows File Server, with a detour through S3 Glacier for the data nobody was using.&lt;/p&gt;

&lt;h2&gt;
  
  
  Start with discovery, not migration
&lt;/h2&gt;

&lt;p&gt;The temptation is to spin up FSx, robocopy everything across, and call it done. Resist that. You'll end up paying FSx prices for terabytes of data that hasn't been touched in years.&lt;/p&gt;

&lt;p&gt;I wrote a PowerShell script to scan every share and classify files by age. This immediately surfaced that a significant portion of the data was cold — files that hadn't been written to in over two years.&lt;/p&gt;

&lt;h2&gt;
  
  
  The LastAccessTime trap
&lt;/h2&gt;

&lt;p&gt;Here's the gotcha that cost me a day: the server had &lt;code&gt;DisableLastAccess&lt;/code&gt; set to &lt;code&gt;1&lt;/code&gt;. This is a common Windows performance optimisation, but it means &lt;code&gt;LastAccessTime&lt;/code&gt; is unreliable — it wasn't being updated when files were read.&lt;/p&gt;

&lt;p&gt;That left &lt;code&gt;LastWriteTime&lt;/code&gt; as the only trustworthy timestamp. It's a reasonable proxy (if nobody's modified a file in two years, it's probably cold), but it's not perfect. A file that's read daily but never edited would appear cold.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The fix&lt;/strong&gt;: I enabled &lt;code&gt;LastAccessTime&lt;/code&gt; tracking early in the project timeline and let it run for a few weeks before the final classification scan. This gave us a more accurate picture before committing to the archival decisions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lesson&lt;/strong&gt;: Check &lt;code&gt;fsutil behavior query DisableLastAccess&lt;/code&gt; on day one of any file migration project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Archive before you migrate
&lt;/h2&gt;

&lt;p&gt;With the data classified, the approach was:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Archive cold data to S3 Glacier (cheap, still retrievable if needed)&lt;/li&gt;
&lt;li&gt;Migrate only active data to FSx&lt;/li&gt;
&lt;li&gt;Keep the original EC2 instance read-only for a transition period&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This significantly reduced the FSx storage footprint and brought the monthly cost down to something sensible.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things I'd do differently
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Automate the archival pipeline end-to-end&lt;/strong&gt;: I used a semi-manual process with AWS DataSync. Next time I'd script the full workflow including verification and cleanup.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Set up monitoring on FSx from day one&lt;/strong&gt;: Storage growth on FSx can surprise you. CloudWatch alarms on free storage space are essential.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Communicate the archive process to users early&lt;/strong&gt;: People get nervous when they hear "we're archiving your files." Setting expectations about retrieval times and the safety net of Glacier avoids unnecessary panic.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Was FSx worth it?
&lt;/h2&gt;

&lt;p&gt;Yes. Automated backups, native AD integration, no more patching a Windows Server instance, and the storage scales without us managing disks. The migration was a few weeks of focused work, but the operational overhead dropped permanently.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>fsx</category>
      <category>migration</category>
      <category>powershell</category>
    </item>
    <item>
      <title>Why Event-Driven Infrastructure Beats Cron Jobs</title>
      <dc:creator>Tom Williams</dc:creator>
      <pubDate>Sun, 22 Mar 2026 12:51:30 +0000</pubDate>
      <link>https://forem.com/tomwilliamscloud/why-event-driven-infrastructure-beats-cron-jobs-1l8d</link>
      <guid>https://forem.com/tomwilliamscloud/why-event-driven-infrastructure-beats-cron-jobs-1l8d</guid>
      <description>&lt;p&gt;If you've spent any time managing infrastructure at scale, you've probably written a cron job that polls for something. Maybe it checks for untagged resources every hour, or scans for missing CloudWatch alarms on a schedule. It works. It's simple. And it's almost always the wrong long-term answer.&lt;/p&gt;

&lt;p&gt;I recently rebuilt one of these systems — a compliance remediation tool that ensures every EC2 instance in our multi-account AWS organisation has CloudWatch CPU alarms — and the shift from scheduled polling to event-driven architecture made a surprising difference.&lt;/p&gt;

&lt;h2&gt;
  
  
  The cron approach
&lt;/h2&gt;

&lt;p&gt;The original setup ran a Lambda on a CloudWatch Events schedule every 30 minutes. It would:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Assume a role into each member account&lt;/li&gt;
&lt;li&gt;List all EC2 instances&lt;/li&gt;
&lt;li&gt;Check for the existence of CloudWatch alarms&lt;/li&gt;
&lt;li&gt;Create any that were missing&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This worked, but had problems:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Latency&lt;/strong&gt;: A new instance could run for up to 30 minutes without monitoring&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cost&lt;/strong&gt;: Every run scanned every instance, even if nothing had changed&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Complexity&lt;/strong&gt;: The Lambda needed to handle pagination across dozens of accounts, manage rate limiting, and deal with partial failures gracefully&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Noise&lt;/strong&gt;: CloudWatch Logs filled up with successful "nothing to do" runs&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The event-driven approach
&lt;/h2&gt;

&lt;p&gt;The replacement uses EventBridge rules deployed to each member account via StackSets. When an EC2 instance launches or has its tags modified, the event is forwarded to a central event bus where a Lambda evaluates and applies alarms.&lt;/p&gt;

&lt;p&gt;The reconciliation Lambda still exists — it runs daily as a safety net — but it catches edge cases rather than doing the heavy lifting.&lt;/p&gt;

&lt;h2&gt;
  
  
  What changed
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Remediation time&lt;/strong&gt;: From up to 30 minutes to under 60 seconds&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Lambda invocations&lt;/strong&gt;: Dropped significantly — we only run when something actually happens&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Code complexity&lt;/strong&gt;: The event-driven Lambda handles one instance at a time, not a full cross-account sweep&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Terraform&lt;/strong&gt;: The module became simpler because each component has a single, clear responsibility&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  When cron still wins
&lt;/h2&gt;

&lt;p&gt;Event-driven isn't always the answer. Use scheduled runs when:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;There's no reliable event source for the change you care about&lt;/li&gt;
&lt;li&gt;You need a full reconciliation sweep (drift detection, for example)&lt;/li&gt;
&lt;li&gt;The event volume would be higher than the polling cost&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;But for "react when something changes" — which is what most compliance automation is doing — EventBridge is the better tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting started
&lt;/h2&gt;

&lt;p&gt;If you're currently running a polling Lambda and want to shift:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Identify the AWS API action that triggers the change you care about&lt;/li&gt;
&lt;li&gt;Create an EventBridge rule matching that event pattern&lt;/li&gt;
&lt;li&gt;Keep your existing Lambda as a daily reconciliation fallback&lt;/li&gt;
&lt;li&gt;Deploy the rule to member accounts via StackSets&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The two patterns complement each other. Events handle the real-time path, scheduled runs handle the "trust but verify" path.&lt;/p&gt;

</description>
      <category>aws</category>
      <category>eventbridge</category>
      <category>automation</category>
      <category>terraform</category>
    </item>
  </channel>
</rss>
