# Darren Ma > Notes on technology, cooking, running, and whatever I am learning. Public Ghost content for AI and LLM tooling. This file includes a bounded export of public pages first, then recent public posts. Append `.md` to any post or page URL to get the content in Markdown (for example, `/example-post.md`). ## Pages ### About URL: https://www.darrenma.eu/about/ Last updated: 2026-07-06T12:42:53.000Z My name is Darren Ma. Outside work, I enjoy computer games, especially Hollow Knight, card games such as Magic: The Gathering when I get the chance, running, and cooking. ![Darren taking a selfie beside a giant Pikachu plushie](https://storage.ghost.io/c/ef/f9/eff9a584-1186-4e96-a736-2d1a0418ce7e/content/images/2026/07/about-selfie-pika.png) about selfie pikachu Professionally, I am currently the software architect for [XuniPlay](https://xuniplay.com/en/?ref=darrenma.eu), an Italian digital-signage platform that was recently acquired and brought into the Orvian group. Earlier in my career, I led the team that built the [Virtual Trading Game for the Johannesburg Stock Exchange](https://virtualtradinggame.jse.co.za/vtg/public/Home?ref=darrenma.eu), where people can learn about investing by trading with simulated money. I am also a father. More recently, I piloted a personal project called Storyverse to explore whether AI could help children stay creative rather than simply giving them finished answers. That project will probably become an article of its own. I also make short videos about [English expressions on Instagram](https://www.instagram.com/madarrenma/?ref=darrenma.eu). They are deliberately bite-sized, although I may occasionally use this site to explore an expression with a history or story worth spending more time on. Overall, I am just a regular John who enjoys automating what I can and removing friction between myself and whatever I actually want to do. I also spend a fair amount of time trying to understand why I enjoy the things I do. I started this site because I had built up a large stack of private notes. I wanted to turn some of them into something more presentable and, perhaps, useful to other people. Most of my writing will probably focus on software design, automating systems at home, homelabbing, and AI. I will also write about running, cooking, and anything else I happen to be interested in. Think of this site as a public notebook offering a glimpse into my Obsidian vault. Instead of leaving all my scrappy notes in the dark, I can share what I tried, what went wrong, what worked, and the common gotchas along the way. I am very much a hands-on person. Sometimes being given a solution is only half the answer. I also want to understand how someone reached it, why they made certain choices, and what compromises came with them. If I had found writing like this during some of my own projects, I think I would have been grateful. --- ## Posts ### Setting up the storage my homelab needed URL: https://www.darrenma.eu/setting-up-the-storage-my-homelab-needed/ Last updated: 2026-10-09T13:07:58.000Z When the SER5 arrived, I imagined the next steps would be fairly simple. Install an operating system, set up Docker, and start running my services. I had two external hard drives from the Raspberry Pi setup, one holding the content and another acting as a backup. I kept using them while I figured out storage. Then I thought about importing more than 400 GB of family photos and videos from Google Photos. The movies were replaceable. Those photos were what I cared about. Storage suddenly needed more attention than I had given it. ## The old drives were still working Both external drives were working. I had read about NAS drives and started looking at WD Red, hoping for something better suited to staying on. I had no evidence that the old drives were about to die. I have had two drives die suddenly in twenty years. Probably a decent rate. Apparently enough to send me researching replacements for two perfectly working ones. If I was doing the homelab, I wanted to do this part properly. ## Two 4 TB drives would have been cheaper I had about 3.7 TB of data. Two 4 TB drives were tempting on price, but I would almost immediately be deciding what to delete. I bought two 8 TB drives instead: one for data, one for backups. Today I have roughly 4.1 TB stored. I could have pruned things to fit smaller drives, but I did not want my first activity on the new storage to be making space. ## I went around the RAID question too I researched RAID before coming back to one disk with my data and another with a backup. Keeping services running through a disk failure mattered less than getting things back. A RAID mirror would repeat my accidental deletions too. Older versions needed separate backups. I eventually used Restic and rsync, which protect different parts of the setup. ## Discovering what a DAS was I considered a rack, a NAS appliance, and individual drive enclosures. Then I discovered a DAS, or direct-attached storage enclosure: four bays, one power supply, and one USB-C cable to the SER5. I worried about bandwidth. What if four people were streaming while downloads and backups ran? In reality, it is usually just me. I planned for a small crowd and mostly built somewhere for myself to watch a movie. Everything has worked as expected, although I have never tested that imaginary peak load. Installing the drives was dead simple, and two bays are still empty. ## Making the drives part of the server Formatting as ext4 was easy. Configuring `/etc/fstab` was familiar territory that I still did not feel confident about. ChatGPT helped with the UUID entries and options so both drives would be available after a restart. Instead of the usual Windows drive letters, I mounted them at folders: `/srv/nas` for data and `/srv/backup` for the second drive. I rebooted to test the setup. ChatGPT also helped configure Samba for access from other devices on the LAN. Copying the old data across was straightforward. I think I used rsync, although I cannot remember precisely. ## Restic took days to get comfortable with My old backup scripts were primitive. ChatGPT suggested Restic, which became the biggest learning curve. I had never used it. Restic keeps daily, weekly, and monthly snapshot history for my configurations, database dumps, documents, and other selected files. It reuses data already stored in its encrypted repository rather than saving another complete copy each time. Rsync mirrors the large photo and media libraries nightly. That is one refreshed copy, with no older versions. My script mirrors deletions too. If I delete a photo, the next run can remove it from the second drive. The Restic snapshots do not preserve the photo library itself. Getting comfortable with this took days. I tested restoring a snapshot into another location, browsing it, and retrieving files. I wrote plenty of notes because I knew I would forget the commands. About nine or ten months later, I looked at the snapshots out of curiosity. The list was longer than expected. With ChatGPT's help, I corrected the retention rules. They seem to work better now. So the backups were running. They were just more enthusiastic about keeping things than I intended. ## I would choose the same setup again I would buy the same two drives and DAS again. QNAP could package more of it for me, but I prefer this setup. Restic still gives me a few shivers. I forget the restore steps, check my notes, and make sure I have the repository password. It feels a little like looking after a crypto wallet's private key. I am happy with it. I just occasionally need to remind myself how the thing I set up actually works. ## Related posts - [The two reasons I built a homelab](https://www.darrenma.eu/the-two-reasons-i-built-a-homelab/) - [The first time I changed my home network, I took down the internet](https://www.darrenma.eu/the-first-time-i-changed-my-home-network-i-took-down-the-internet/) ## References - [Restic repository design](https://restic.readthedocs.io/en/stable/100%5Freferences.html?ref=darrenma.eu) - [Restic snapshot retention](https://restic.readthedocs.io/en/stable/060%5Fforget.html?ref=darrenma.eu) ### The first time I changed my home network, I took down the internet URL: https://www.darrenma.eu/the-first-time-i-changed-my-home-network-i-took-down-the-internet/ Last updated: 2026-09-24T06:55:02.000Z The network was down. Family members were waiting to send emails and print documents. My kid was shouting that YouTube was not working on the TV or iPad. A drop of sweat fell across my brow. I had just read through a lot of instructions and put OpenWrt firmware onto a Xiaomi router. But the network settings were not right yet. This was the first time I had changed the home network properly. I knew there would be downtime. I had not really understood what it feels like to take the internet away from an entire house. ## The ISP router had been annoying me for years The original router came from TIM, my ISP, around 2020\. It connected to the strange egg-shaped antenna used for the fixed-wireless connection where I live in a smaller town in Italy. I can confidently say the connection was underwhelming, and so was the router. It ran proprietary firmware, I could not put anything else on it, and I could not even update it myself. Connections did not drop every day, but it was not rare either. Sometimes the internet would slow down to a snail's pace, then suddenly work again after a restart. Restarting the router became such a normal part of life that even my non-technical mother knew to do it. There was no single disaster that made me decide to replace it. Things worked, but not well. I had no control over the router, so there was not much I could do when they didn't. ## I staged a coup in two phases I did not want to replace the internet connection immediately. My first move was to put another router behind the TIM one and make it the router for the home network. The TIM box could provide the internet connection, but the devices in the house would no longer depend on it for Wi-Fi and local networking. I had wanted to do this for years. I was also thinking ahead. If I changed internet provider later, I wanted to be able to do that without rebuilding the whole home network at the same time. If I am honest, I chose the Xiaomi AX3200 because it was the cheapest router I could find on Vinted that I could put OpenWrt onto. I had gone through a long list of options, mostly because prices were much higher than I expected. It also had Wi-Fi 6 and looked like it would give me better range, but OpenWrt support was the important part. I wanted a router I could update and control myself. ## Flashing it was more stressful than I expected There was one catch. The international RB01 version of the Xiaomi AX3200 had a more involved OpenWrt process than other versions of the router. I remember reading model numbers, firmware versions, downgrade instructions, and hoping I had ordered the right thing. The method I found used a community Python tool to get access to the router before installing OpenWrt. It seemed reasonable after reading the documentation, but I was still about to run a script from the internet against my new router. I ran the script through ChatGPT for some peace of mind, then went for it. Luckily, that part worked first time. Now I had a second router. So what? I set up a separate private network behind the TIM router. The TIM router became the internet gateway, while the Xiaomi handled the home network. I disabled the Wi-Fi on the TIM router and moved the main devices across to the Xiaomi. That was the point when the rest of the household realised there was construction happening. I had mentioned there could be downtime, but when the internet is off it feels like the house is staring at you and counting every second. Most things came back quickly. The HP Deskjet printers, as usual, required their own special battle. But eventually the main devices were connected and the internet worked again. There was no real recovery plan. If it had not worked, I would have needed to put everyone back on the TIM router and try again another day. Luckily, it did not get there. ## Once it worked, everyone noticed The home network was noticeably more stable, and the furthest room had better Wi-Fi range. We no longer needed to restart the router itself. The TIM connection was still not perfect. Every now and again I needed to restart the antenna plugged into the TIM router. But that was a different problem. The new router had at least stopped being another source of trouble in the house. The moment it felt worth it for me was logging into the OpenWrt admin interface and seeing how many options I now had. I knew a future homelab would use some of them. That was already enough. The next day I set up ad blocking and bandwidth reports. We paid TIM each month for an "unlimited" connection, but the reports showed that the connection would slow after roughly 800 to 900 GB of usage. It was never a fixed number. When I called TIM about it, they were vague too. That was one of the things that eventually pushed me to replace the connection with Starlink. ## The bit that mattered later Local DNS names came later, once I was properly deploying homelab services such as Immich. But from the start, I was in control of the router. I could update it and decide which useful things to add next. When I switched the internet connection to Starlink about a year later, it was laughably easy because the home network was already in place. It was also only then that the restart routine disappeared completely. The router was not the homelab itself, but it became the backbone for it. The SER5 came next, followed by Immich, Jellyfin, and the rest. ## What I would do differently I would exaggerate the likely downtime to the family and have a mobile-hotspot backup plan ready. I started this at about 7pm, right in the middle of family home time. If I did it again, I would plan it for when everyone was out or asleep. Still, I am glad I did it. The router fixed some annoying problems immediately and gave me a part of the home setup that felt like mine. ## Related posts - [The two reasons I built a homelab](https://www.darrenma.eu/the-two-reasons-i-built-a-homelab/) ### The two reasons I built a homelab URL: https://www.darrenma.eu/the-two-reasons-i-built-a-homelab/ Last updated: 2026-09-14T07:04:30.000Z I had been interested in having a homelab for years, but hardware is not cheap and I had never found a reason strong enough to commit to it. Then I had two. I wanted another copy of my family photos and videos that I controlled. And I wanted to replace Kodi. That was really it at the beginning. I did not set out to build a platform full of containers, local DNS names, backups, and a little server that quietly runs parts of my home. I wanted my own photos available locally and a media player that did not keep asking to be fixed. ## Google Photos was not the problem I still like Google Photos. In many ways it is the perfect photo service. It is convenient, works well, and gives me access to years of photos and videos without thinking about where they are stored. The problem was that it was the only copy I could easily use. Everything depended on Google and an internet connection. Maybe that is a little paranoid. But if the internet was down, or I was somewhere without access to Google, I wanted to know that my own family photos were still available to me. I also liked the idea that I could store and share family photos locally for people who did not use Google Photos themselves. I briefly considered Apple Photos, but that would have meant relying on another large cloud service. I was not trying to leave Google Photos behind. I just wanted another copy that lived at home and was under my control. That is what led me to Immich. ## Kodi had done its time The other reason was Kodi. For a long time, Kodi was my local media setup. The Xiaomi smart box connected to the TV ran Kodi, and the media lived on an external hard drive connected to a Raspberry Pi 3B+. MySQL kept things in sync. It had given me a good decade of service, so this is not me saying Kodi was bad. But the setup had become tiring. An update could break something and leave me setting up or scraping metadata again. Over time, some videos stopped playing nicely or the sound would stutter. I assume newer codecs and the ageing hardware had something to do with that. I did not need every feature Kodi offered. I wanted a cleaner media server that was easier to keep running. Jellyfin looked like it could do that. It also had transcoding if I ever needed it, although that has turned out to matter far less than I expected. I even installed Jellyfin on the Raspberry Pi to give it a real try. Every now and again the Pi would just crash. Good times. ## I tested the idea on my main PC first Before buying anything, I ran both Immich and Jellyfin in Docker on my main PC. Docker was already part of my work, so it was an easy way to try them without immediately buying a server and hoping for the best. I put a lot of photos into Immich and used it like that for months. Updates made me nervous at first because I knew some changes could be breaking. These were actual family photos, after all. But after going through a few updates, I started to understand the process and realised it was not as scary as it looked. Google Photos was still there too, so I was not placing all my trust in a new setup overnight. Jellyfin also looked modern and simple when I used it locally. The media-server model made more sense to me than trying to keep the old TV box and Kodi setup alive forever. After using them for a while, I realised I was no longer just playing with two applications. I wanted to rely on them. ## The router came first The first physical homelab step was not the server. It was the router. I had wanted to replace the default ISP router for years. It was not very good, the Wi-Fi range was weak, and connections would drop for no obvious reason. I found a cheap second-hand Xiaomi router that could run OpenWrt. It also gave me Wi-Fi 6, better range, and the chance to make the home network independent from whatever the ISP supplied. I bought it about a week before the server and immediately started flashing it and rebuilding the home network. That was more stressful than I expected. The internet went down for the whole house while I was doing it, so everyone was waiting to get back to YouTube or print work emails. Eventually I had the old network working through the new router. It was more stable, the furthest room had a better connection, and I had a network that could survive a future ISP change without making me rebuild everything. That later mattered when I moved to Starlink. The router story deserves its own article. But getting it working made the server idea feel more real. ## I went in circles over the hardware The obvious first idea was to reuse the Raspberry Pi I already had. It was cheap to run and already part of the old Kodi setup. But it was too weak for what I wanted next. Immich needed more CPU, GPU, and RAM for facial recognition. I also wanted the possibility of two or three Jellyfin streams at once. The Pi had already shown me what happened when it was asked to do too much. So I went in circles for a while. A Raspberry Pi 5\. Maybe an Intel NUC. Maybe separate devices. Then NAS appliances from QNAP and Synology, because I would also need plenty of storage. The NAS appliances looked convenient, but expensive. I wanted to run the homelab myself instead of buying a box that did everything for me. I wanted more control over the services and how they were put together. Eventually I found a Beelink SER5\. It looked like the right balance: low enough power use for something that would stay on, enough CPU and GPU for Immich and Jellyfin, and enough headroom to add things later. I also ran the requirements through ChatGPT. It went through the specifications and agreed that the SER5 should be able to handle what I wanted. That gave me some confidence that I was not buying another device I would outgrow immediately. Then I found one on Vinted at a reasonable price. At that point, it felt worth taking the risk. ## It was not just plug in hard drives and go The SER5 started with Debian. I set up Docker and Samba, worked through making storage available on the network, and put together backups and Restic scripts. I had the existing external hard drive to begin with. The DAS came later, almost as the last addition. I had imagined storage in a very simple way: put hard drives somewhere, then have space. There was more to it than that. Formatting, mounting, Samba shares, `fstab` entries, permissions, backups. I ran through each part one at a time. It was more than I had bargained for, but it also meant that I gradually understood what the server was doing instead of treating it like a magic box. Immich and Jellyfin were the first two services. I already knew there could be more later. I had been looking at things like the Arr stack and Gitea, but those were not the reason I bought the machine. Once the first pattern was in place, though, adding another service started to feel a bit like plugging another Lego block into something that was ready for it. ## It grew from two ordinary needs I did not have a full homelab architecture in my head when I started. I wanted another copy of my photos, and I wanted a better way to watch my local media. Those two things gave me a reason to learn the rest: networking, storage, Docker, backups, and the boring parts that make a small server useful at home. The system is much bigger now than I imagined at the time. But it started with those two needs, then grew a little bit at a time. The next articles will cover the network, the server, Immich, Jellyfin, storage, and the other services that found a place there. ## Related posts - [I only wanted to sync a few recipes](https://www.darrenma.eu/i-only-wanted-to-sync-a-few-recipes/) ### What made Linux stick URL: https://www.darrenma.eu/what-made-linux-stick/ Last updated: 2026-08-25T10:51:02.000Z # What made Linux stick I first tried Ubuntu in 2008 with Hardy Heron. I still remember the background. I did not have a problem with Windows. I was interested in computers, and trying another operating system looked fun. Almost twenty years later, after a lot of tinkering, dual boots, distro hopping, and returns to Windows, I now use Bazzite as my daily operating system. Gaming was the thing that kept pushing me back to Windows. Curiosity kept bringing me back to Linux. ## I kept coming back without knowing why My first Ubuntu installation was only something to play with. Dual booting was a must because I still played a lot of games and Windows was my real operating system. Even so, there was something about Linux that held my attention. I spent hours on the Ubuntu forums reading about problems and trying to solve the most mundane things. I remember the different colours, sounds, and the strange satisfaction of logging in and browsing the web through Firefox from a completely different operating system. I did not have a good reason for putting in that effort. I just enjoyed it. There was also a philosophy around Linux that interested me. I have always found it natural to cheer for the community-driven underdog, in the same way I chose Mozilla over Internet Explorer when I could. The idea of having more control over my own computer appealed to me too, although I was not doing anything particularly meaningful with that control. I just liked knowing it was there. Beyond that, there was simply an affinity I could not explain. I was curious, so I kept coming back. ## Linux became useful A few years later, when I started getting into web development, I repurposed an older laptop with Linux Mint. By then Linux already felt much easier to use. I set up an Apache web server on my LAN and felt like the ultimate hacker. Windows was still my daily operating system at home and at work, but Linux had become more than an experiment. It was now useful for the things I wanted to learn. Around 2011, that balance finally reversed. Instead of a Windows computer with a hobby Linux partition, I had a Linux Mint machine that I worked on every day. I think I kept a second Windows laptop that was just powerful enough to play StarCraft II. That was enough to get me by. My daily work involved Django, Python, and the usual surrounding development tools. Following programming documentation was often simpler because I could use the Bash commands directly instead of translating them into a Windows setup. At the same time, many of the web developers around me were buying Macs. Using Linux felt like a statement in its own small way. I was not following everyone else onto Apple. ## Gaming always won in the end My business venture ended around 2013, and I was no longer as devoted to the Django stack. I had more free time and felt the gaming itch again. I bought a powerful new PC and returned to Windows to take full advantage of it. Linux had come a long way, but there was still a hard line: use Linux, or play all the games I wanted to play. For me, gaming won. I stayed fully on Windows for years. Then, around 2019, I got crushed in Warzone one night and decided that installing Arch Linux sounded like a good response. There was no practical reason for this. It was the same old tech itch. I followed the Arch installation guide, completed the setup, and found the result a little underwhelming. I immediately wiped it and installed Manjaro instead. Pacman and rolling releases were the new ideas I wanted to try. Manjaro was nice, but I still did almost everything in Windows. Apart from tasting another distribution, I had not given Linux a real job to do. ## Debian gave Linux a real job That changed in 2020 when I built a machine for Ethereum staking. This time the choice of operating system carried some weight. Until then, I had usually tried whatever distribution was new or receiving attention. Choosing one based on the requirements of the job was different. I wanted something stable, familiar enough to operate, and popular enough that I could find help when needed. That was the beginning of my relationship with Debian. I ran the dedicated staking machine for roughly two years. I did not want to tinker with it. I wanted to apply security updates and have it keep working. It did exactly that, without ever giving me a problem. This was a different kind of Linux experience. Until then I had chosen distributions because I was curious about them. Debian was chosen for a job, and it did that job without giving me problems. That made me trust it. Linux was no longer interesting only because I could change everything. It was impressive because I could leave it alone. By 2022, I was also using Debian through Windows Subsystem for Linux on my work machine. My Node.js development moved into WSL, while Windows and Docker handled the rest. Documentation became easier to follow because paths and commands behaved as expected. There may also have been a slight feeling that using Linux made me better than everyone else. I mean that as at least half a joke. ## The emotional switch to Pop!\_OS At home, gaming remained the blocker until a phishing incident in 2022 pushed me into making a more emotional decision. I opened a malicious Excel document that ran macros or something similar. My credentials were stolen, two-factor authentication alerts kept appearing, and a MetaMask hot wallet was drained of its Ethereum and NFTs. Thankfully, the valuable assets had already been moved to a cold wallet, but the experience still sucked. It was enough to make me move my home PC fully onto Linux because I felt safer there. Linux would not have protected me from every part of what happened, but after the incident I wanted a cleaner break from the machine and system I had been using. I chose Pop!\_OS because of its out-of-the-box Nvidia support and reputation for making gaming easier. By then, many of my old desktop blockers had already disappeared. I replaced Photoshop with Krita, which was enough for what I needed, even if it would not be enough for everyone. There were also plenty of alternatives to Microsoft Office. Pop!\_OS showed me that I could handle most of my normal computer use without Windows. I used it for about a year and was mostly happy. Eventually the interface started to look dated compared with the desktops I was seeing elsewhere. Updates also felt slow while System76 worked on its new COSMIC desktop. I was interested in COSMIC, but I was not in the mood to alpha or beta test it on my main computer. I had been curious about Linux for decades, but by then it mattered more that my PC simply worked. The installation itself had also become bloated. I had copied commands from guide after guide, including a memorable attempt to get Nvidia CUDA working through Docker. Packages and repositories accumulated until formatting the machine felt like the only real way to make it clean again. I returned to Windows temporarily to try the WSL setup at home. That worked well. I was even self-hosting Immich, Gitea, and Jellyfin from the PC. Still, I knew the return to Windows was temporary. ## Bazzite removed the compromise At the end of 2025 or beginning of 2026, I watched a JayzTwoCents video about Bazzite. They were playing games straight out of the box, so I had to try it. Bazzite was different from the Linux systems I already knew. I had not used an image-based desktop operating system before, and I knew very little about Homebrew or Distrobox. But if games really worked, I was willing to learn the rest. They did work. Steam was already there, and the games I tried simply launched. Much of the credit belongs to Valve's work on Proton, but Bazzite packaged the whole experience in a way that felt polished. I could play Final Fantasy VII Remake Intergrade. I could play Street Fighter 6\. I could play almost every game in my Steam library. I should point out that I no longer play competitive online games, so I do not run into the anti-cheat problems that still block other Linux gamers. I cannot say every Windows game works for everyone. The games I wanted to play worked for me. Some games even worked better when I ran their Windows versions through Proton instead of using the native Linux builds. I had better performance in Borderlands 2, while the native build of Silksong gave me strange controller-layout problems. Pop!\_OS had already shown me that I could replace most of the everyday desktop software I once needed Windows for. Bazzite took away my last reason to go back: the games. This was what I had been waiting for. Linux, polished and ready for games. The image-based design, often described as immutable, turned out to be another reason to stay. I had not realised how much that mattered to me until I used Bazzite. On Pop!\_OS, I had accumulated repositories and packages by following guides, Reddit comments, and whatever else solved the problem in front of me. Eventually it became impossible to remember what belonged to the original system and what I had added later. Bazzite encourages a cleaner separation. Distrobox lets me create containerised development environments without filling the host with their packages and dependencies. I can experiment with another distribution when I need to while keeping the base system relatively clean. Learning that approach was heavily smoothed out by ChatGPT, Claude, and Gemini. I have spent countless hours asking them why a game that worked yesterday no longer starts, or how to arrange my development setup without modifying the host. Proton Experimental, I am looking at you. ## What finally made it stick There was no single conversion moment. Linux became useful for more parts of my life and, eventually, I ran out of reasons to go back to Windows. It had already worked for web development, a staking machine, and my professional workflow through WSL. Gaming was always the part I could not replace. Steam, Proton, and Bazzite changed that. Bazzite is now the full-time operating system on my home PC. I still have a company-issued Windows laptop with WSL, mainly because I need it for work involving legacy .NET codebases. Outside that work, I do not use Windows at all. I also do not miss paying for a Windows licence. I do not want operating-system telemetry that feels more like spyware, and I do not want AI features shoved into my computer when I did not choose to put them there. I use AI heavily, but I want that to be my choice. I like the cleaner design and feeling more in control of my own computer. More than anything, though, I simply enjoy using Linux. That was true even when I had no practical reason to install it. Years ago, I was on the Ubuntu forums with an avatar that displayed a few coffee beans. Now I have long conversations with AI assistants about Distrobox and Proton. The way I find help has changed, but the interest has not. I do not know which distribution I will use next, or whether Bazzite will remain the answer forever. I am just happy with where I have landed after all this time. The year of the Linux desktop may finally be here. At least it is in my house. ## References - [Ubuntu release history](https://wiki.ubuntu.com/Releases?ref=darrenma.eu) - [Bazzite terminology](https://docs.bazzite.gg/General/terms/?ref=darrenma.eu) - [Distrobox on Bazzite](https://docs.bazzite.gg/Installing%5Fand%5FManaging%5FSoftware/Distrobox/?ref=darrenma.eu) - [Bazzite gaming introduction](https://docs.bazzite.gg/Gaming/gaming-intro/?ref=darrenma.eu) - [Borderlands 2 on Steam](https://store.steampowered.com/app/49520/Borderlands%5F2/?ref=darrenma.eu) - [Hollow Knight: Silksong on Steam](https://store.steampowered.com/app/1030300/Hollow%5FKnight%5FSilksong/?ref=darrenma.eu) - [My post about the 2022 phishing incident](https://x.com/regularjohn2023/status/1519586357387091970?ref=darrenma.eu) ### AI did not teach me to cook. It removed the friction URL: https://www.darrenma.eu/ai-did-not-teach-me-to-cook-it-removed-the-friction/ Last updated: 2026-08-07T07:29:52.000Z *How I use AI to adapt recipes, plan the shopping, and make cooking three times a week sustainable.* ## Too much inspiration, not enough dinner When I started cooking more seriously, I did what I usually do: I collected too much information. I had cookbooks and would occasionally find something I wanted to make. Online there were even more options: YouTube videos, recipe websites, PDFs, and snippets saved from chats. The willingness was there, but it was often followed by the same sequence: > I don't have that. I don't have this. What even is that, and where am I supposed to buy bok choi? Then I would close the book and cook nothing. Recipes vary a lot. Some included ingredients I did not like or could not easily identify in Italy. Others were designed for six people when I needed three or four. None of these were big problems. They were still enough to make me close the book and give up for the night. For me, the planning was hard. The cooking was easy. AI helped remove enough of the planning friction that I could get to the part I actually wanted to do, cooking! I do sometimes use AI to generate recipes too. But the useful part for building the habit was taking recipes I already wanted to cook, making them fit my kitchen, and making sure I had everything ready. ## What I ask AI to do I keep my recipes as Markdown notes in an Obsidian vault. Because I work with the vault in VS Code, assistants such as Codex or Claude can read the source recipe and the examples already stored beside it. I might start with a PDF, a recipe URL, or something copied from a video. I then ask the assistant to: - turn it into a clean recipe note - adjust the number of portions - convert everything to metric - remove sections I do not need - remove ingredients I dislike - suggest locally available alternatives - keep the instructions short and easy to reference while cooking This is not complicated or clever. It is mostly a conversation about making somebody else's recipe fit my kitchen. ## A real example One recipe that caught my eye was HelloFresh's [Sriracha Pork Chops with Bánh Mì Lunch](https://www.hellofresh.com/recipes/sriracha-pork-chop-for-dinner-5b106d44ae08b539993371f2?ref=darrenma.eu). The original was designed as a cook-once, eat-twice recipe. You make pork chops for dinner, then use the leftovers in bánh mì sandwiches the next day. I only wanted the dinner. I asked the assistant to remove the lunch section, convert the imperial measurements to metric, and remove the cilantro because I do not like it. I also needed help making sense of the pork cut. A “pork chop” is not always labelled in the same obvious way when I am shopping in Italy, so I could check whether the cut available to me would work and whether the cooking method needed to change. The adapted recipe specified `680 g` of pork. I bought the closest package I could find, about `700 g`, cooked all of it, and the amount worked well. The result was no longer a PDF I had to reinterpret each time. It was a familiar recipe note in English, using metric measurements, with only the meal I intended to cook. ## The shopping list was the real win Adapting individual recipes helped, especially while I was building the library. The bigger improvement was what happened after I had several recipes ready. I could select a few meals that might suit the family that week and ask AI to create one shopping list for all of them. The list is grouped into sections such as produce, meat and seafood, dairy, grains, and pantry ingredients. Shared ingredients are combined instead of appearing several times. I can check off what is already at home, then shop for the rest. When the week arrives and I decide to cook one of those meals, the ingredients are there. I open the recipe on my phone, glance through the method, and start. I wrote separately about [how I sync that Obsidian vault between Bazzite and iPhone](https://www.darrenma.eu/i-only-wanted-to-sync-a-few-recipes/). That is the friction that was worth removing. Inspiration no longer collapses at the first missing ingredient. ## I use AI less now I have been cooking roughly three times a week for about six months. At the beginning, while I was building the recipe library, I used AI for this process practically every day. I needed more help converting recipes, understanding ingredients, and getting everything into a format I liked. Now I mostly use it for the weekly shopping list. Some recipes are familiar enough that I already know what to do, although I still usually glance at the saved note. The heavy assistance became lighter as my own experience grew. I think that is a good sign. I needed a lot of help while I built the library and became more comfortable cooking. Now that the habit is established, I do more of it myself. ## Getting consistent output AI has not made any meaningful mistakes in my ingredients or shopping lists so far. The problem I did have was inconsistency. One recipe would arrive as plain text. Another would include emojis. Sometimes the shopping list used checkboxes, sometimes it did not. The information was correct, but the growing library looked like it had been assembled by several different people who had never spoken to one another. I fixed that by creating examples in the format I wanted. I have one recipe that acts as my recipe template and one shopping list that acts as the shopping-list template. Instead of explaining every formatting decision again, I point the assistant at the relevant example and ask it to follow that structure. My permanent rules are simple: - always use English - always use metric measurements - follow the referenced template - keep everything easy to read while cooking or shopping - use checkboxes in shopping lists - do not use emojis I do not actually copy and paste one magic prompt. I normally ask in natural language because every recipe needs something slightly different. The consistency comes from showing the assistant what good output looks like. ## The prompts, if I had to write them down These are cleaned-up versions of how I ask naturally, not prompts I have been copying word for word. You do not need a complicated template. Mine is just quick facts, metric ingredients, a few useful notes, and numbered steps. The shopping list links the selected recipes, combines quantities, groups items by section, and uses checkboxes. ### Adapt a recipe ```text Read the attached recipe, PDF, or URL and create a recipe note using [recipe template] as the exact structure and style. Adapt it for [number] people. Use English and metric measurements. Remove [unwanted section or ingredient]. Replace ingredients that are difficult to buy in Italy with obvious local alternatives, but tell me when a substitution could materially change the dish. Keep the method easy to scan while cooking. Use no emojis. Save the finished recipe at [vault path]. ``` ### Build the weekly shopping list ```text Create one shopping list for these recipes: - [recipe] - [recipe] - [recipe] Use [shopping-list template] as the exact format. Deduplicate shared ingredients, calculate the combined quantities, group everything by shopping section, and keep likely pantry basics separate. Use English, metric measurements, checkboxes, and no emojis. ``` I have used the same idea for larger meals too. If several dishes need to be ready at a specific time, I can provide the recipes and ask for a low-stress preparation timeline that works backwards from serving time. It accounts for prep, cooking, resting, shared appliances, and anything that can be done earlier. What matters is giving it the recipes, showing it the format I use, and being clear about what I need back. ## What changed I enjoy cooking for my family. It takes some pressure off everyone else and gives me a practical way to contribute at home. I also enjoy improving at it. Cooking is a useful skill, and one I want to keep developing. AI did not create that motivation and it obviously did not do the cooking for me. It helped with the part that used to make me give up before I started. Six months later, I am still cooking about three times a week and I need less help than I did at the beginning. That is a pretty good result for me. ## References - [HelloFresh: Sriracha Pork Chops with Bánh Mì Lunch](https://www.hellofresh.com/recipes/sriracha-pork-chop-for-dinner-5b106d44ae08b539993371f2?ref=darrenma.eu) ### If the context is clear, the prompt can be simple URL: https://www.darrenma.eu/if-the-context-is-clear-the-prompt-can-be-simple/ Last updated: 2026-07-24T11:00:18.000Z I thought I needed better prompts. That took me longer to understand than it should have. I have used AI assistants heavily for about two years now. ChatGPT, Codex, Claude, Kiro IDE, and whatever else happened to be useful at the time. At first I thought the main skill was learning how to prompt better. Better prompts do help. But at some point I realised I was spending more time explaining the same background than asking the actual question. The problem was not always the prompt. The problem was scope. ## What do I mean by scope? I do not only mean “more files”. I mean the assistant can see how parts of the system interact with one another. The invisible parts. The bits a human on the project carries around in their head without noticing. For example, if an assistant only sees one backend service, it may not know that shared TypeScript types live in a separate internal module. So it creates new local types inside the service. That is a reasonable answer from the assistant's point of view. It is also wrong for the system. Then, after waiting for the answer, I have to explain the missing context and ask it to move the types into the correct module. The work gets done eventually, but the first answer was solving the smaller problem it could see instead of the actual problem I had. That is the kind of thing I mean by scope. Scope is relationships, conventions, runtime feedback, examples, and boundaries. It is not just file count. ## Where did I first notice this? The first place I really noticed it was not at work. It was in StoryVerse, a private project of mine. StoryVerse is a web app I started building to help kids interact with AI properly without losing their creativity. I will probably write about it properly another time. Because I was the only developer, I had full control over the shape of the project. From the start I built it with AI assistance in mind. It was not a microservice system. It had a Fastify backend and a Vue frontend, but they lived together in one workspace. That made a big difference. The assistant could see both sides of a change. If I needed a backend API change and a matching frontend update, I did not have to paste both halves into the chat. The files were already there. If I was creating a data contract, the assistant could see how the backend returned it and how the frontend consumed it. I was not doing “context engineering” properly yet. I was not using steering files well. The results were still inconsistent. Sometimes even inside the same Vue frontend, I would get different styles of implementation. But it was the first taste of how much better AI felt when it could see more of the system. ## What was the professional pain point? At work, the situation was almost the opposite. The system had become very modular. Each service had its own repository. Shared modules had their own repositories. Internal libraries, backend services, frontend applications, and infrastructure all lived in different places. That structure made sense for normal development. It was painful for AI-assisted development. If I opened one service repo and asked for help, I first had to explain all the things that were not in that repo: - where shared types lived - which module handled service communication - which casing rules mattered at the API boundary - which patterns were standard - which things should not be recreated locally - how the old system behaved - how the new service was supposed to replace it Before I could ask the question, I had to rebuild the world around the question. Then the chat context would eventually run out and I would have to do it again. Or I would forget one small detail, and the assistant would produce something that looked fine in isolation but did not fit the platform. This became more annoying as the system matured. More services meant more shared modules, and more shared modules meant more invisible context. Opening one repo at a time no longer matched how the system actually worked. ## Was the answer just a monorepo? Kind of. But not a real one. A real monorepo was not the point, and it was not the shape the organisation had. Each service and module already had its own repo, history, permissions, pipelines, and habits around it. So I built what I started thinking of as a synthetic monorepo. It looks like a monorepo in the editor, but it is not one Git repository. Each project keeps its original repo. The workspace simply places them next to each other so the editor and AI assistant can see them together. The distinction is roughly this: ```text Real monorepo one-git-repo/ service-a/ service-b/ shared-types/ Synthetic monorepo my-workspace/ service-a/ -> its own Git repo service-b/ -> its own Git repo shared-types/ -> its own Git repo ``` For example: ```text my-workspace/ legacy-monolith/ services/ service-a/ service-b/ modules/ shared-types/ service-clients/ apps/ admin-ui/ infrastructure/ nginx/ deployment/ docs/ ``` That is not my real folder structure. It is the shape of the idea. The first version was much smaller. It was just for me. I put the newer backend services and shared modules together. No UI, no DevOps infrastructure, no old monolith yet. Even that was already better. Later I added the old C# monolith solution so the assistant could see both sides of the porting work: the thing we were moving away from and the new services we were moving into. Then I added infrastructure files, which helped with things like routing. Then UI projects, even though I do not mainly work on them, because backend API changes eventually affect frontend integration too. It started as a personal convenience. It became the way the team works on the product. ## Why did the workspace help? Because many questions are not local. If I ask the assistant to port an endpoint, the useful answer may depend on: - the old C# implementation - the new service structure - shared data contracts - existing converters - service communication patterns - frontend expectations - routing rules - logs from the running services When those things are scattered across separate repos and only one repo is open, the assistant has to guess unless I explain everything. When they are in the same workspace, the assistant has a chance to inspect the surrounding system. Debugging became easier too, especially once infrastructure files were included. Sometimes the issue is not only in the service code. It might be routing, configuration, or how another part of the system calls into it. Onboarding also became easier. Instead of sending a new developer through pages of separate Git repositories, the workspace gives one navigable view of the platform. That matters even without AI. With AI, it matters more. ## Why was scope still not enough? Seeing the files is not the same as understanding the rules. This is where steering files became important. The workspace shows the assistant what exists. Steering tells it how to behave there. The assistant still needs guidance on: - coding standards - design principles - architectural choices - preferred patterns - boundaries between modules and services - what should not be repeated locally That sounds simple, but if you do not get it right early, the drift can become real. An assistant can produce code that works locally while slowly pushing the platform away from its intended patterns. So the steering files stay intentionally slim. I do not want huge theoretical documents that nobody wants to maintain. I prefer short guidance that points at real examples. Something like: ```text my-workspace/ .kiro/ steering/ workspace-map.md backend-patterns.md shared-modules.md migration-rules.md ``` And the guidance itself should be practical: ```md # Shared modules - Shared API contracts live in `modules/shared-types`. - Do not recreate shared types inside individual services. - Service-to-service calls should use the shared client module. - Preserve external API casing when replacing legacy endpoints. - Prefer existing examples over inventing a new pattern. ``` That is a made-up example, but it shows the point. The assistant does not need an essay. It needs enough direction to avoid solving the wrong problem. ## What are the trade-offs? The first trade-off is Git. Because this is not a real monorepo, a single logical change can touch several actual repositories. Each repo still needs its own commit, branch, push, and review. The workspace view is unified. The Git model is not. That friction is real. There is also maintenance. The workspace has to stay reproducible. If repos move, names change, origins change, or the team moves from one Git instance to another, the setup scripts need to be updated. Steering files can also go stale. I have not had that become a major problem yet, but it is obviously possible. If the architecture changes and the steering files do not, the assistant may keep following old instructions. There is also context overload. More scope is not automatically better. A huge workspace with no structure can become noise. That is why the steering files need to help the assistant navigate the workspace rather than treat everything as equally relevant. Privacy matters too. This worked for us because the teams are small and the product context is shared enough that this kind of visibility is acceptable. In a stricter corporate setting, a synthetic workspace may need much clearer permission boundaries. I also have not properly tested how portable the idea is across different IDEs. We considered a mixed setup at one point, but in practice we stayed with Kiro. So no, this is not magic. It is another thing to maintain. But it has been worth maintaining. ## What did I actually learn? I used to think a lot of AI mistakes were prompting mistakes. Some are. But many of the frustrating ones were scope mistakes. The assistant could not see the relationship between the service, the shared module, the old implementation, the frontend, and the infrastructure. So it answered the question it could see. That is not stupidity. That is missing context. Better prompts can help, but there is a limit to how many times I want to paste the same architecture explanation into a chat window. At some point, the better answer is to change the workspace. If the assistant cannot see the relationships in your system, it will keep solving the wrong smaller problem. That is the real lesson for me. I did not need one perfect prompt. I needed more scope. ## Related posts - [I thought I needed to build a content application](https://www.darrenma.eu/i-thought-i-needed-to-build-a-content-application/) ### When VS Code belongs in the Distrobox URL: https://www.darrenma.eu/when-vs-code-belongs-in-the-distrobox/ Last updated: 2026-07-17T08:14:47.000Z Suddenly, the Codex extension would not initialise in VS Code or show my sessions. It still worked directly in the CLI. I thought the Codex extension was broken. Then I thought the OpenAI VS Code extension was broken. Then I thought my Distrobox was broken. Then I tried Flatpak VS Code and remembered why Flatpak IDEs can be a special kind of pain. The eventual fix was simpler than all of that: stop running VS Code on the host and attaching it to the Distrobox. Install VS Code inside the Distrobox and run the editor where the development environment already lives. The annoying part is that the Codex extension had been inconsistent for at least a couple of weeks. It was not completely broken every day, but it was unreliable enough that I kept circling back to it. Once I finally sat down to fix it properly on 16 July 2026, the whole move to the final setup took about two hours. Naturally, on the morning of 17 July 2026, Bazzite DX shipped an image update that included VS Code `1.129.0` anyway. I am still keeping VS Code inside the Distrobox. Partly to avoid being stuck on a version pinned to the Bazzite image, and partly to get some value from the effort I spent getting there. ## The setup before it broke My machine runs Bazzite DX. I use Distrobox for development because I want the host to stay boring and the dev environment to be disposable. The original workflow looked like this: ```text Bazzite host VS Code -> attach to Distrobox -> VS Code server inside container ``` That worked for a while. The host owned the GUI and the container owned the development tools. Then the Codex extension started showing invalid extension errors. The sidebar would not load properly. The usual fix would be to clear caches, reinstall the extension, or recreate the container. So I started there. This burned more time than it should have because every layer looked guilty. Codex had symptoms. VS Code had logs. Distrobox was involved. Flatpak offered a newer build, then introduced a different problem. ## The first false lead: Codex itself The first visible symptom was Codex, so I looked at Codex. I checked the CLI: ```bash codex --version ``` The CLI was fine. I also backed up `~/.codex`, because Codex sessions and auth live there and I did not want to delete useful history while chasing an extension problem. The important files were: ```text ~/.codex/sessions ~/.codex/session_index.jsonl ~/.codex/history.jsonl ~/.codex/auth.json ~/.codex/config.toml ``` Reinstalling the Codex npm package did not fix the VS Code sidebar. That was the first sign the problem was higher up the stack. ## The second false lead: the current Distrobox Next I assumed the Distrobox had become messy. After all, I had used this Distrobox for every experiment or tool in the past few months with much less care than usual because I knew it could be reassembled easily. I created a new Distrobox and attached VS Code to it. The same class of extension errors appeared. That mattered, because a clean container should have removed broken package state inside the development environment. If a fresh container has the same issue, the problem is probably not the container. ## The real clue: VS Code extension host logs The extension host logs showed this: ```text PendingMigrationError: navigator is now a global in nodejs ``` The error appeared in built-in or host-side extensions before Codex was even the interesting part, including GitHub authentication and Remote Containers. To be clear, I do not fully understand all the logs that are output in VS Code. Sometimes I see what looks like errors but everything works, and sometimes I struggle to even find the logs for what I am looking for. At this point I asked Codex via CLI to analyse the logs for me. That changed the diagnosis. The issue was not really Codex. It was the host VS Code version and its extension host. I had recently updated Bazzite, and the host VS Code was on `1.128.0`. That version appeared to bring extension-host behaviour related to Node 24, and the `navigator` migration error was the clue. Once the extension host was unstable, Codex just happened to be the extension where I noticed the pain. ## The Flatpak attempt I cannot emphasise how irritating it is to have Flatpak VS Code look so close to being the perfect solution, then have quirky errors pop up about not seeing folders, files, or usually accessible commands like `podman`. I went so far as to try enabling every single permission using Flatseal, but even that was not enough. I remembered again why I had turned down this option a few months back. I tried Flatpak VS Code because it was available at a newer version. It launched as `1.129.0`, which was promising. But Flatpak also sandboxes the editor. That immediately created a new problem: VS Code could not find `podman` when trying to attach to the container. I tried adding permissions with Flatseal, but Flatpak refused direct access to paths like: ```text /usr/bin/podman /usr/bin/docker /usr/bin/distrobox ``` It also complained about `/var/run` because Flatpak manages that namespace itself. That was enough. Flatpak VS Code might be fine for editing files, but for this workflow it was fighting the tools I actually use. The host VS Code was layered into Bazzite DX, so it did not update like a normal apt package in a normal Ubuntu install. I had to wait for the image to update. That is fine when the host is just the host. It is less fine when the host editor is suddenly part of my daily development path. ## The solution: install VS Code inside the Distrobox The better model was: ```text enter Distrobox -> run VS Code inside Distrobox ``` No host attach. No VS Code remote server layer. No Flatpak sandbox. The editor runs where Node, npm, Codex, apt, project tools, and terminals already are. Inside the Ubuntu Distrobox, I added the Microsoft apt repo and checked available VS Code versions: ```bash apt list -a code ``` At the time, the available versions included: ```text 1.129.0 1.128.1 1.128.0 1.127.0 ``` I installed `1.129.0`: ```bash sudo apt install -y code=1.129.0-1784074987 ``` Then I launched it with isolated user data and extensions: ```bash code \ --user-data-dir ~/.config/code-dev-2604 \ --extensions-dir ~/.vscode-dev-2604/extensions ``` One important detail: Distrobox shares the home directory with the host, so plain `code` could reuse the same `~/.config/Code` and `~/.vscode/extensions` state that had already become messy. The separate directories keep this VS Code install clean. I made an alias: ```bash alias code-dev="code --user-data-dir ~/.config/code-dev-2604 --extensions-dir ~/.vscode-dev-2604/extensions" ``` Now the workflow is: ```bash distrobox enter dev-2604 code-dev ~/projects/my-workspace ``` Surprisingly, VS Code launched normally from inside the Distrobox. The GUI looked right, the terminal was where I expected it to be, and Codex worked from the same environment as the rest of my tools. ## A small trap: VS Code terminals as root During one of the attach attempts, the integrated terminal opened as `root` even though normal `distrobox enter` used my user. I cannot recall with certainty, but I think this only occurred when using the original direction: VS Code on host -> attaching to container. The checks were: ```bash whoami echo "$HOME" echo "$XDG_RUNTIME_DIR" ps -p $$ -o user,comm,args ``` The broken terminal showed: ```text root /home/my-user /run/user/0 ``` That explained the `ble.sh` warnings too. The shell was reading my home directory but running as root, so ownership and runtime paths were wrong. The real fix was to stop attaching through Dev Containers and run VS Code inside the Distrobox as the normal user. If using Dev Containers anyway, the container config needs: ```json { "remoteUser": "my-user" } ``` But for this setup I no longer need that path. ## Restoring Codex sessions Codex sessions live under: ```text ~/.codex/sessions ``` Because Distrobox shares my home directory, the VS Code inside the Distrobox can use the same `~/.codex` directory. To restore sessions from a backup: ```bash mkdir -p ~/.codex tar --no-same-owner --no-same-permissions \ -C ~/.codex \ -xzf ~/backups/codex-sessions-backup-2026-07-16.tgz \ sessions ``` I did not blindly restore old `auth.json` or `config.toml`. Fresh auth is cleaner if the new extension is already working. ## What changed Before: ```text host VS Code -> Remote Containers -> VS Code server -> Distrobox ``` After: ```text Distrobox -> VS Code -> Codex and dev tools ``` The second model has fewer layers. It also gives me direct control over the VS Code version through apt inside the Distrobox, instead of waiting for Bazzite packaging to move past a bad release. ## Current runbook Inside the Distrobox: ```bash code --version codex --version find ~/.codex/sessions -type f | wc -l ``` My working versions after the fix were: ```text VS Code 1.129.0 codex-cli 0.144.5 ``` The alias: ```bash alias code-dev="code --user-data-dir ~/.config/code-dev-2604 --extensions-dir ~/.vscode-dev-2604/extensions" ``` The launch command: ```bash distrobox enter dev-2604 code-dev ~/projects/my-workspace ``` ## What I learned The mistake was assuming the editor was outside the dev environment. On an immutable desktop, that assumption becomes expensive. If the host editor version is bad, every container you attach to inherits the pain. If the editor runs inside the Distrobox, it updates with the development environment and sees the same tools the project sees. I still like Bazzite as the host. If anything, this made the Bazzite idea clearer for me: keep the host stable, boring, and focused on being the workstation base. Put the development tools, including the editor, where the development work actually happens. The cost now is that I have to enter two commands via terminal to launch VS Code instead of just clicking the VS Code icon. I believe there is a way to export a desktop launcher but I haven't done it as I'm on the terminal often anyway. ### I thought I needed to build a content application URL: https://www.darrenma.eu/i-thought-i-needed-to-build-a-content-application/ Last updated: 2026-07-13T07:57:57.000Z *How an idea for a TypeScript content factory turned into a collection of files, an `AGENTS.md`, and a real Instagram post in one morning.* ## I had let the account go quiet Rooibos Italia is a small business we run on the side. We import BOS Rooibos from South Africa and sell it in Italy. We also have an [Instagram account](https://www.instagram.com/bositaly/?ref=darrenma.eu) that I had left mostly untouched for a couple of years. Marketing is its own skill and it takes time to do well. Images and videos need to be made, the copy needs to be written, and someone has to keep doing it after the first burst of enthusiasm wears off. BOS supplies us with some content, but not everything we need for the Italian account. Eventually the account went quiet through a combination of limited time and a lack of enthusiasm. One evening I was sitting at a table listening to other people talk about how they keep posting to their social accounts regardless. They had simply stuck with it. I felt a little guilty about leaving ours alone for so long. I had already wondered whether AI could help me revive it. Not with the expectation of gaining thousands of followers. I mainly wanted the account to look alive again. If I could look back after a year and see more than 40 useful posts, that would feel like a real effort. That night I started imagining the system I could build. ## Naturally, I imagined an application In my head it was a proper content factory. An idea would enter one side. A Node.js and TypeScript application would expand it into content, store everything in PostgreSQL or SQLite, let me review the result, and eventually publish it automatically. The proposed pipeline covered articles, Instagram posts, Reels, carousels, Facebook, Pinterest, emails, and image prompts. It was quite a lot for an Instagram account I had not posted to in years. I was already hesitant. I build software professionally, so I know that creating an application also means creating something to maintain. There would be dependencies, database migrations, integrations, credentials, and all the other things that arrive after the fun first version. My recent experience with file-based AI systems had also made me question whether an application was necessary. I had already used an `AGENTS.md`, a ledger, and some templates to manage my English Expressions project. It could respond to natural-language instructions and move work through a defined process without a custom interface. Instead of asking Codex to build my proposed application, I asked it to interview me first. ## I let the assistant question the requirement When I plan software at work, the requirement normally develops through discussion with several people. Someone asks a question, someone else spots an assumption, and the shape changes before we build it. That discussion is missing when I work alone. I had found one-question-at-a-time AI interviews useful while setting up this blog, so I tried the same method here. I gave the assistant the broad idea and asked it to quiz me for the context it needed. The questions were simple: - What do we actually sell in Italy? - What should Instagram achieve? - How often could I realistically post? - Did I want to appear in videos? - What images and brand material did I already have? - Where should a customer go if they wanted to buy something? My answers quickly made the original design look silly. I was not trying to operate a multi-channel media company. I wanted to publish an Instagram image every week or two without each post becoming a small marketing project. I did not want to appear in Reels. I already had a useful collection of product and lifestyle images. Publication volume was low, and I was happy to approve everything manually. The huge content factory in my head became a much smaller requirement: > Help me produce one good Instagram post with as little friction as possible. Once we said it that plainly, most of the application disappeared. ## The files became the application The finished first version is a Git repository containing Markdown, YAML, images, templates, and a detailed `AGENTS.md`. There is no Node.js application, database server, Docker container, scheduler, chatbot interface, Instagram API, or automated publishing. The basic structure looks like this: ```text AGENTS.md assets/ data/ docs/ drafts/ ready/ published/ templates/ ``` The important files are: - a product catalogue containing what Rooibos Italia actually sells - a register of sources - controlled factual claims and approved wording - a ledger containing 100 possible content ideas - a template for Instagram posts - a working brand guide - rules for handling and approving images The workflow is still the same one I had wanted from the application: ```text idea -> verified claims -> draft -> human approval -> ready -> published ``` I can use natural instructions such as: ```text Create a Rooibos Italia post about homemade iced rooibos. ``` The assistant selects an idea, follows its linked claims and sources, inspects the available assets, creates the post bundle, and moves it through the lifecycle only when I approve it. It is less deterministic than a compiled application. It depends on a capable assistant correctly following the instructions. In exchange, there is almost no application code to maintain, and I can change the process while I am still learning what it needs to be. I originally put it in a separate repository because it is commercial and asset-heavy. Now that it consists mostly of plain files, I am not entirely convinced it needs to remain separate from my Obsidian vault. I have left it where it is for now. ## One fact can support many posts I initially remembered the idea as building a database of 100 rooibos facts. What we built was slightly different: ```text sources -> controlled claims -> editorial ideas -> posts ``` That separation matters. One supported fact, such as rooibos being naturally caffeine-free, can lead to several posts: an evening drink, an alternative coffee break, an after-dinner cup, or an explanation of what caffeine-free means. The system currently contains: - 100 content ideas - 39 controlled claims - 13 source records - 85 curated images The number 100 is not magic. It was simply large enough to remove the blank page and force some variety. The links between sources, claims, ideas, and finished posts are more useful than the number itself. The product catalogue also stops the assistant from blindly following global BOS material. Rooibos Italia currently sells the original rooibos tin and cardboard pack. We do not stock every flavoured product shown by the global brand, and we do not sell the iced-tea cans that appear in some assets. That is ordinary business context, but the assistant needs it written down if it is going to act on my behalf. ## We had to correct each other The assistant kept warning me about health claims in existing promotional images supplied by BOS marketing. It became irritating quite quickly. I understood what it was trying to prevent, but I did not need the same warning every time it encountered approved parent-company creative. I explained that context and changed the rules. Existing BOS promotional material can be treated as approved brand creative without repeated warnings. That does not give the AI permission to invent new health claims, strengthen the wording, or detach a claim from its original context. The correction also went the other way. For a summer post, I suggested that prepared rooibos keeps well because it does not oxidise. I had heard that explanation, but I could not defend it at a scientific level. The assistant found support for the useful part: rooibos can be prepared, refrigerated, and enjoyed later. It did not find support for the absolute oxidation claim, so we removed it. I was fine with that. I wanted to tell people they could make the drink in advance. I did not need a questionable chemistry explanation to do it. The relationship worked because neither side was automatically right. I had business context the assistant lacked. It checked a claim I had accepted without really verifying. ## The first post found the real problems Once the system existed, I gave it a real command: ```text Create a Rooibos Italia post. I'd like to lean on the make your own ice idea. ``` The assistant chose an existing summer photograph from the asset library. It showed two glasses of rooibos with lemon and mint, a jug, and a yellow BOS tin. I had expected it to generate a new image, or at least use an existing image as a reference to create something different. The selected photo already looked good, so there was no reason to manufacture something new merely because AI could. There was one problem. The photograph contained an old 50-tea-bag package, while the current Italian product contains 40 tea bags. The assistant used the lifestyle image as the scene and a current product cutout as the reference. It replaced the old package while leaving the rest of the photograph largely intact, then prepared the result as a 1080 by 1350 pixel Instagram image. The Italian caption explained that rooibos is not only a hot drink and suggested serving it cold with ice, lemon, and mint. I added a few emojis because the first version looked too plain for Instagram. Walking the post all the way to publication exposed one more piece of friction. The Markdown production record separated the caption, call to action, hashtags, alt text, sources, and review notes. That was useful for managing the post, but not for publishing it. I wanted the final physical process to be: 1. Choose the image. 2. Copy all the text. 3. Post. We added a plain-text caption file to every ready post bundle. It contains only the final copy in publishing order. That small improvement only became obvious when I was sitting in front of Instagram ready to use the system. Planning the entire workflow in advance probably would not have found it. ## A morning to build, two minutes to publish The complete first run took about three hours. I had the idea the previous night, spent a morning building the system with Codex, and ended with a finished Instagram post. I should qualify that. I already had Codex, VS Code, and Git set up, and I am very familiar with this kind of workflow. This is not a promise that anyone can reproduce the system in three hours. Once the post was ready, publishing it through Instagram on the same PC took about two minutes. The assistant had already checked that the selected image was Instagram-friendly, so it sat neatly in the finished post without any unexpected cropping. That made the decision not to automate publishing look even more sensible. I could build a Meta integration, handle authentication and tokens, host the image somewhere accessible, and maintain the API. Or I could spend two minutes posting it myself every week or two. For now, I will take the two minutes. The [first post went live on 8 July 2026](https://www.instagram.com/p/DahzLTQDM5z/?ref=darrenma.eu). ## What this has and has not proved The first caption sounds more professional and more like conventional marketing than my older posts. I am fine with that for now. What matters most is that something useful is there. The Rooibos Italia voice can develop through use. The likely weak link is imagery. Ideas are not the problem. The first post benefited from a good lifestyle image already being in the library. I do not yet know what will happen when a worthwhile idea has no matching asset and we need to create something from nothing. Most importantly, one post is not consistency. The system has completed its whole designed lifecycle, from an idea in the ledger to a real public post. That proves it can produce something usable. It does not prove that I will keep using it for three months, never mind a year. I plan to revisit that honestly after three months. By then I should know whether it reduced enough friction to keep the account active, whether the images became a bottleneck, and which parts of the workflow survived contact with repeated use. ## The pattern I would reuse This is not a universal replacement for building software. It fits a low-volume system operated by one person who is already comfortable working with files, Git, and AI assistants. Still, there is a pattern here that I expect to use again: 1. Give the assistant durable instructions rather than one enormous prompt. 2. Put domain truth into small source files. 3. Define clear draft, ready, and published states. 4. Let the assistant interview me before deciding what to build. 5. Build only enough structure to produce one real item. 6. Take that item all the way to the external action. 7. Fix the friction found at the end. I do not see this as a prototype waiting to become a Node.js application. The files, instructions, Git history, and assistant are the system I want to maintain. The main thing that could change that is cost. I currently have a Codex Pro account and the usage is sustainable. If pricing or token usage made this approach unreasonable, I would have to reconsider it. Supporting several social networks might also change the calculation, although that is not currently on my radar. ![](https://storage.ghost.io/c/ef/f9/eff9a584-1186-4e96-a736-2d1a0418ce7e/content/images/2026/07/image-2.png) Codex you're out of messages warning For now, I have one new post, a backlog of ideas, and a process that can make the next one. That is further than the application in my head ever got. ### I only wanted to sync a few recipes URL: https://www.darrenma.eu/i-only-wanted-to-sync-a-few-recipes/ Last updated: 2026-07-06T14:49:20.000Z *How I ended up using WebDAV, Docker, and my homelab to sync Obsidian between Bazzite and iPhone.* Trying to up my home-dad game, I decided to take on a bit of cooking 101 with heavy assistance from ChatGPT. I soon found myself asking the same questions multiple times or scrolling back through chats to find that one response I wanted to replicate. I started saving notes directly onto my iPhone while I was in the kitchen. When I was on my PC, I would browse YouTube videos and HelloFresh recipes that inspired me or that I wanted to try. So I had the opposite problem on each device. The notes I wanted in the kitchen were on my PC, while the rough notes I made in the kitchen were on my phone. The friction was getting in the way of simply walking into the kitchen and cooking. Creating a new habit is hard enough without small obstacles getting in the way. What I wanted sounded simple: keep an Obsidian vault on my Linux PC and iPhone, then move notes and recipe PDFs between them. What followed was three days of trying increasingly elaborate ways to avoid spending about $5. In the end, the answer came from infrastructure I already had at home. This is how I now sync Obsidian between Bazzite and iPhone using WebDAV, Docker, Caddy, and my NAS. ## This should have been simple If you use a Mac and iPhone, iCloud is an obvious option. Obsidian Sync is the straightforward cross-platform answer and its price is entirely reasonable. I did not want another subscription if I could help it. I am actively trying to slim down the number I already have. One-time purchases are fine when I expect to use an app heavily or it solves something critical, but this use case felt too small even for that. Paying would obviously have been the sensible thing to do. It would have saved me at least three days of trial and error. But I also enjoy finding my own solution and learning along the way. Whether that was proportional to the problem is debatable. ## The solutions that nearly worked ### Obsidian Sync and iCloud Obsidian Sync was the easy answer: > Pay money = fix your problem. I did not want to pay for another subscription. iCloud would also have been easy if my devices all lived in Apple's world. Mine do not. My main PC runs Bazzite Linux. ### Syncthing and Möbius Sync Syncthing looked very promising. The proposed setup was: - Syncthing on Bazzite - Möbius Sync Pro on iPhone Quite deep into setting it up, I realised that the “Pro” in Möbius Sync Pro meant a paid app. To be fair, it was only a small one-time purchase, around $5 when I looked, but by then I was convinced there must be a free solution for such a simple use case. ### Git and Working Copy At this point I asked ChatGPT for help. It recommended Git. This sounded like a great idea because I was already using Git to store my Obsidian vault, with remotes in Bitbucket and my self-hosted Gitea instance. I could use Working Copy on the iPhone and keep the same repository-based workflow. I would need to pull and push manually, but that did not bother me. ChatGPT was convinced: this was the best free solution. ![](https://storage.ghost.io/c/ef/f9/eff9a584-1186-4e96-a736-2d1a0418ce7e/content/images/2026/07/image-1.png) ChatGPT recommending Git and Working Copy as the best totally free option I set up the PC side in minutes, installed Working Copy on the iPhone, and discovered that pushing required its paid unlock. The price shown to me was around $40. I had not been willing to pay $5 for the previous app, so you can imagine where I landed with this one. My confidence in ChatGPT was also starting to dwindle. I had now been assured twice that I was following the best free option, only to find the limitation after doing the setup. Let's recap. All I wanted to do was move notes between my PC and iPhone. Was that really so hard? ### Joplin and LocalSend I even considered moving away from Obsidian entirely and using Joplin with Dropbox. But I did not feel like migrating my notes and changing applications merely to exchange a few recipes. For a while, I used LocalSend. When both devices were on the same Wi-Fi network, I could send text and PDFs between them. It was not streamlined, but it got a few recipes into the kitchen for a few nights. It still was not the experience I wanted. ## The answer was already at home Until this point I had been using ChatGPT through its web interface. It knew the products I mentioned, but it did not know anything about the infrastructure I already had. One night, I opened my Obsidian vault with Codex and asked it the same question. This time the assistant could see my homelab notes: an always-on SER5 mini PC, Docker, NAS storage, local DNS, Caddy, and Tailscale. With that context, it suggested using the existing server as a WebDAV sync point. The answer suddenly seemed obvious. ChatGPT knew the products I mentioned. Codex could see what I already had at home, and that changed the answer. My final setup looks like this: ```text Bazzite: Obsidian + Remotely Save ─┐ ├── obsidian.lan ── Caddy ── rclone WebDAV ── /srv/nas/obsidian iPhone: Obsidian + Remotely Save ──┘ ``` Each device still has its own local Obsidian vault. The NAS is the middle point used to exchange changes; I am not opening a live network-mounted vault from either device. The pieces are: - The `Remotely Save` Obsidian community plugin on Bazzite and iPhone - `rclone serve webdav` running in Docker on my SER5 - NAS-backed storage under `/srv/nas/obsidian` - Caddy at `http://obsidian.lan` - An OpenWrt local DNS record that points `obsidian.lan` to the SER5 This fits my setup because: - There is no additional subscription or app purchase. - Markdown remains local on each device. - I avoid an iOS Git workflow. - The remote copy is made of ordinary files on my NAS. - Tailscale lets the same private setup work away from home, although it is not required for local sync. This is not free in the same way for everyone. I already owned and operated the homelab. If you do not have an always-on server, paying for a finished sync service is almost certainly cheaper than building one just for this. ### Why it uses HTTP This WebDAV service is local and intentionally uses HTTP. It is available only on my trusted LAN or through Tailscale, and is not exposed directly to the public internet. There is an obvious catch. WebDAV basic-auth credentials sent over HTTP are not encrypted on the LAN. I accept that inside my own network, but I would not publish this endpoint to the internet or use the same setup on an untrusted network. Adding local HTTPS is something I may do later. The reverse proxy and network design deserve articles of their own. ## What using it actually looks like On my PC, I might download a HelloFresh recipe PDF and put it into my vault. I then ask Codex to adapt it: change six portions to three, convert the measurements to metric, prepare a shopping list, and replace difficult-to-find ingredients with obvious local substitutes. The adapted recipe and shopping list go into the food section of my vault. In Obsidian, I press `Ctrl+P` and run `Remotely Save: Start Sync`. Later, in the kitchen, I open Obsidian on my iPhone and run the same command. The recipe and shopping list are there when I need them. The flow also works in reverse. I sometimes ask ChatGPT for cooking help while I am on my phone, save the rough result as a note, and sync it. When I return to the PC, I pull the note down and clean it up properly. In practice, I do the fiddly work on the PC and use the phone for cooking and quick notes. WebDAV joins the two together. ## Five months later I have used this setup almost every weekday for around five months. So far it has worked without conflicts, missing attachments, overwritten notes, or data loss. It is not perfect. I still trigger sync manually, and I have occasionally forgotten to run it on the PC before leaving the room. That means reaching for the phone later and discovering the note is not there yet. Is it 100% seamless? No. Is it good enough for my recipes? Absolutely. It has also grown beyond recipes. I now use the same flow for personal notes, technical documentation, ideas, and even a gaming walkthrough containing many pages. Once sync worked, I started putting more of my life into the vault. One Remotely Save setting caught me out. In version `0.5.25`, which I currently use, the plugin aborts a sync by default if at least 50% of the vault would be modified or deleted. It is there to stop a bad sync from doing too much damage. I set the threshold to 100, which effectively disables that protection. This is deliberate: I trust my workflow and do not want a legitimate large sync, such as that multi-page walkthrough, to be blocked. It also means the safety net is gone, so backups matter even more. ## Runbook: my Docker, Caddy, and WebDAV setup This is my actual setup. It assumes: - An always-on Linux server with Docker Compose - Storage mounted at `/srv/nas` - Caddy already running in Docker - An external Docker network named `proxy` shared with Caddy - Control of local DNS, in my case through OpenWrt - Obsidian installed on the PC and iPhone Tailscale is optional and is not required for sync while at home. ### 1\. Create the server directories ```bash sudo mkdir -p /srv/nas/obsidian /srv/docker/obsidian-sync sudo chown -R "$USER":"$(id -gn)" /srv/nas/obsidian /srv/docker/obsidian-sync cd /srv/docker/obsidian-sync ``` ### 2\. Create the environment file Create `/srv/docker/obsidian-sync/.env`: ```ini webdav_user=obsidian webdav_pass=replace-with-a-long-random-password tz=Etc/UTC ``` Protect it from other local users: ```bash chmod 600 .env ``` These are example credentials. Do not reuse them, commit the `.env` file, or publish its real contents. ### 3\. Run rclone WebDAV in Docker Create `/srv/docker/obsidian-sync/docker-compose.yml`: ```yaml services: obsidian-webdav: image: rclone/rclone:latest container_name: obsidian-webdav restart: unless-stopped command: - serve - webdav - /data - --addr - :8080 - --user - ${webdav_user} - --pass - ${webdav_pass} - --disable-dir-list - --dir-cache-time - 10s - --poll-interval - 10s environment: - TZ=${tz} volumes: - /srv/nas/obsidian:/data networks: - proxy networks: proxy: external: true ``` Create the shared network if it does not already exist, then start the container: ```bash docker network inspect proxy >/dev/null 2>&1 || docker network create proxy docker compose up -d docker compose logs -f obsidian-webdav ``` ### 4\. Add the Caddy route My Caddy container is attached to the same `proxy` Docker network and uses this reusable helper: ```caddyfile (proxy_to) { encode zstd gzip reverse_proxy {args[0]} log { output stdout format console } } ``` Add the WebDAV host to the `Caddyfile`: ```caddyfile http://obsidian.lan { import proxy_to obsidian-webdav:8080 } ``` I reload Caddy from its Compose stack: ```bash cd /srv/docker/caddy docker compose up -d docker compose logs -f caddy ``` When I first added `obsidian.lan`, Caddy continued serving its previous configuration even though the updated file existed on disk. A full container recreation fixed it: ```bash docker compose down docker compose up -d ``` ### 5\. Add local DNS In OpenWrt, add a local DNS record that points `obsidian.lan` to the LAN address of the Docker host. Verify from a client: ```bash curl -I http://obsidian.lan ``` An unauthenticated `401 Unauthorized` response is expected. It confirms that the endpoint is reachable and requesting credentials. ### 6\. Configure Remotely Save on both devices On Bazzite and iPhone: 1. Open Obsidian's community plugin browser. 2. Install and enable `Remotely Save`. 3. Choose `WebDAV` as the remote service. 4. Set the server URL to `http://obsidian.lan`. 5. Enter the username and password from `.env`. 6. Keep the same vault name on both devices when using the plugin's default remote-folder behaviour. 7. Leave `.obsidian` configuration-folder sync disabled initially. I do not want every desktop plugin and layout copied to my phone. I use manual sync through the command palette: ```text Remotely Save: Start Sync ``` Remotely Save can also run on a schedule while Obsidian is open, but it cannot sync while the mobile app is suspended in the background. ### 7\. Perform the first two-way test Back up any existing vault before the first sync. 1. Create a test note on Bazzite. 2. Run `Remotely Save: Start Sync`. 3. Confirm that files appear below `/srv/nas/obsidian` on the server. 4. Open the matching vault on iPhone and run the same command. 5. Confirm that the test note arrives. 6. Create a second test note on iPhone and sync again. 7. Sync on Bazzite and confirm that the phone-created note arrives. If the first full sync is blocked because at least 50% of the vault would change, review the proposed operation carefully before raising the modification-protection threshold. I use 100 because I intentionally allow large changes, but that disables the protection rather than solving an error. ### 8\. Keep the vault in Git Sync is not backup. A mistaken deletion can propagate between devices. I do not separately back up the WebDAV copy under `/srv/nas/obsidian`. My actual versioned backup is the Git repository on the PC. The vault's notes and attachments, including recipe PDFs, are committed and pushed to two remotes: - My self-hosted Gitea instance - Bitbucket, which provides an off-site copy I intentionally exclude machine-specific Obsidian state, installed community plugins, caches, and workspace files under `.obsidian`. If the WebDAV mirror disappears, I can recreate it from a client vault. If I accidentally sync a bad deletion, I can restore the tracked files from Git history. ## What I would do differently If I had known that `rclone serve webdav` and Remotely Save could use my existing homelab, I would have started there and called it a day. I would not call this the best free way to sync Obsidian. It is my way, built from infrastructure I already had and wanted to understand. $5 would have saved me three days. I cannot pretend this was the sensible way to solve the problem. Still, I ended up with a working system, learned more about infrastructure I already owned, and now use the vault for much more than recipes. I am happy with the trade. ## References - [Obsidian pricing](https://obsidian.md/pricing?ref=darrenma.eu) - [Remotely Save on GitHub](https://github.com/remotely-save/remotely-save?ref=darrenma.eu) - [rclone WebDAV server documentation](https://rclone.org/commands/rclone%5Fserve%5Fwebdav/?ref=darrenma.eu) - [Caddy reverse\_proxy documentation](https://caddyserver.com/docs/caddyfile/directives/reverse%5Fproxy?ref=darrenma.eu) - [Working Copy](https://workingcopy.app/?ref=darrenma.eu)