Powered by Perlanet

Watched on Thursday January 8, 2026.

Watched on Monday January 5, 2026.
How to make work optional long before you reach pension age.

I saw a post on Reddit the other day from someone in their early thirties who’d just done the maths.
They weren’t upset about a specific number on a spreadsheet. They were upset about what that number meant: if nothing changes, they’re looking at nearly 40 more years of work before they can stop.
And honestly? That reaction is perfectly rational.
If you picture the next four decades as a rerun of the last one — same sort of job, same sort of boss, same sort of commute, same sort of “living for the weekend” routine — then yes: that can feel soul-destroying.
But that fear is also based on a model of working life that’s already creaking, and will look even more bizarre by the 2060s.
The problem isn’t “40 more years of activity”.
It’s “40 more years of powerlessness”.
So rather than arguing about whether retirement should be 67 or 68, I think a better question is:
How do you turn work from something you endure into something you control?
Because “retirement” doesn’t have to be a cliff edge. It can be a dial you gradually turn down — until work becomes optional.
A lot of us grew up with an implied script:
That model did work for some people. It also trapped an awful lot of people in lives that felt like an endless swap: hours for wages, autonomy for security, year after year.
And it’s collapsing for reasons that have nothing to do with motivation posters:
If your mental picture of “work until 68” is based on the old script, no wonder it feels like a prison sentence.
The good news is: you don’t have to play it that way.
Here’s the through-line I wish more people heard earlier:
Your goal isn’t “retire at 68”. Your goal is to build a life where you have choices.
That usually comes down to three things:
Let’s unpack those.
“Do what you love” is bad advice if it’s delivered as an instant fix. Most people can’t just pivot overnight — they’ve got rent, mortgages, kids, health, caring responsibilities, all the boring adult stuff.
But you can often change the trajectory by 5–10 degrees. And those small turns compound.
Start with a blunt question:
Is your dread about the work… or the way the work is organised?
Because those aren’t the same thing.
Sometimes you don’t hate the work. You hate the environment: the manager, the politics, the constant interruptions, the pointless meetings, the commute, the “bums on seats” culture.
If that’s you, the fastest win isn’t a complete career reinvention. It’s a change of context:
People talk like the “working from home revolution” vanished. It didn’t. It just became uneven. Some companies swung back because they like control. Plenty didn’t — and plenty quietly make exceptions for people who ask well and have leverage.
Which brings us to…
If you feel trapped, one of the cleanest ways out is to acquire a skill that gives you options.
Not because money is everything — but because money buys autonomy.
A lucrative skill can mean:
This doesn’t have to mean going back to university or turning into an AI wizard overnight.
It can be:
Pick the version that you can realistically commit to for 30–60 minutes a day. Consistency beats heroic bursts.
“Side hustle” is a phrase that makes half the internet roll its eyes — for good reason. There’s a whole industry dedicated to selling you the fantasy of “passive income” while extracting money from you.
But the concept is still solid:
One income stream is fragile. Two is resilient.
A second income stream can be boring and unsexy and still change your life.
It might be:
The aim isn’t to grind yourself into dust. The aim is to reduce the feeling that one employer controls your entire future.
I’m biased here — but I’m biased because it works.
For some people, the most direct route to “I can breathe” is to sell their skills directly rather than renting themselves out through an employer.
Freelancing isn’t for everyone. It comes with uncertainty, admin, and the need to find work.
But it also comes with things a lot of jobs quietly remove:
And tapering is the part I think we need to discuss more.
The old model says you work full-time until you stop, and then you stop completely.
Freelancing lets you do something more human:
That’s not “retirement” in the traditional sense.
It’s work-optional living.
I’m in the UK, so let’s be blunt: the state pension is a safety net, not a life plan.
If your entire retirement strategy is “hope the government sorts it out”, you’re outsourcing your future to politics.
A better approach is to use FIRE (“Financial Independence, Retire Early”) principles — not necessarily to retire at 35 and live on lentils, but to build options:
The point isn’t perfection. The point is reducing the number of years you have to work in a way you hate.
I’m 63. I consider myself basically retired.
Not in the “never do anything again” sense. More in the “work is optional most of the time” sense.
I’ll take on the odd bit of freelance work to top up the coffers when it suits me. I don’t do it because I’m trapped. I do it because I choose to.
That, to me, is the real win.
It didn’t happen because I discovered a magical secret. It happened because, over time, I built skills people would pay for, took control of how I sold them, and treated my career as something I was responsible for designing.
Your dread makes sense — if you assume the next 40 years must look like the last few.
But they don’t.
You can change the kind of work you do.
You can change where and how you do it.
You can add income streams.
You can build skills that increase your leverage.
You can move towards freelancing or consulting if that appeals.
You can invest so “retirement” becomes earlier, softer, and more flexible.
And most importantly:
You can stop thinking of retirement as a date someone hands you… and start treating it as a dial you gradually turn down.
That’s the shift.
Not “How do I survive until 68?”
But: “How do I build a life where I have choices long before then?”
How to make work optional long before you reach pension age.
I saw a post on Reddit the other day from someone in their early thirties who’d just done the maths.
They weren’t upset about a specific number on a spreadsheet. They were upset about what that number meant: if nothing changes, they’re looking at nearly 40 more years of work before they can stop.
And honestly? That reaction is perfectly rational.
If you picture the next four decades as a rerun of the last one – same sort of job, same sort of boss, same sort of commute, same sort of “living for the weekend” routine – then yes: that can feel soul-destroying.
But that fear is also based on a model of working life that’s already creaking, and will look even more bizarre by the 2060s.
The problem isn’t “40 more years of activity”.
It’s “40 more years of powerlessness”.
So rather than arguing about whether retirement should be 67 or 68, I think a better question is:
How do you turn work from something you endure into something you control?
Because “retirement” doesn’t have to be a cliff edge. It can be a dial you gradually turn down — until work becomes optional.
A lot of us grew up with an implied script:
Pick a career in your late teens (good luck),
Get a job,
Keep your head down,
Climb a ladder,
Do 40–50 hours a week until you’re “allowed” to stop.
That model did work for some people. It also trapped an awful lot of people in lives that felt like an endless swap: hours for wages, autonomy for security, year after year.
And it’s collapsing for reasons that have nothing to do with motivation posters:
Industries change faster than careers now,
Companies restructure as a hobby,
Skills age out,
And (as COVID reminded us) the “normal” way of working can change overnight.
If your mental picture of “work until 68” is based on the old script, no wonder it feels like a prison sentence.
The good news is: you don’t have to play it that way.
Here’s the through-line I wish more people heard earlier:
Your goal isn’t “retire at 68”. Your goal is to build a life where you have choices.
That usually comes down to three things:
Make work less miserable now
Make income less dependent on one employer
Build an exit ramp so you can turn the dial down over time
Let’s unpack those.
“Do what you love” is bad advice if it’s delivered as an instant fix. Most people can’t just pivot overnight — they’ve got rent, mortgages, kids, health, caring responsibilities, all the boring adult stuff.
But you can often change the trajectory by 5–10 degrees. And those small turns compound.
Start with a blunt question:
Is your dread about the work… or the way the work is organised?
Because those aren’t the same thing.
Sometimes you don’t hate the work. You hate the environment: the manager, the politics, the constant interruptions, the pointless meetings, the commute, the “bums on seats” culture.
If that’s you, the fastest win isn’t a complete career reinvention. It’s a change of context:
A different team or employer,
A different kind of role in the same field,
A remote or hybrid arrangement,
Four longer days instead of five,
Shifting hours so your life isn’t crushed into evenings and weekends.
People talk like the “working from home revolution” vanished. It didn’t. It just became uneven. Some companies swung back because they like control. Plenty didn’t — and plenty quietly make exceptions for people who ask well and have leverage.
Which brings us to…
If you feel trapped, one of the cleanest ways out is to acquire a skill that gives you options.
Not because money is everything — but because money buys autonomy.
A lucrative skill can mean:
You earn more for the same time,
You work fewer hours for the same money,
You have bargaining power (including flexibility),
You can leave a bad situation sooner.
This doesn’t have to mean going back to university or turning into an AI wizard overnight.
It can be:
Moving from “doing” to “leading” (project management, product, people management),
Specialising inside your field,
Taking a sideways step into a niche people will pay extra for,
Building stronger communication skills (seriously — rare and valuable),
Learning tools that make you more effective than your peers.
Pick the version that you can realistically commit to for 30–60 minutes a day. Consistency beats heroic bursts.
“Side hustle” is a phrase that makes half the internet roll its eyes — for good reason. There’s a whole industry dedicated to selling you the fantasy of “passive income” while extracting money from you.
But the concept is still solid:
One income stream is fragile. Two is resilient.
A second income stream can be boring and unsexy and still change your life.
It might be:
Tutoring,
Consulting a few hours a month,
Selling a simple digital product,
Building a tiny niche website,
Doing freelance work on weekends for a fixed goal (“£5k emergency fund”),
Monetising a hobby sensibly.
The aim isn’t to grind yourself into dust. The aim is to reduce the feeling that one employer controls your entire future.
I’m biased here – but I’m biased because it works.
For some people, the most direct route to “I can breathe” is to sell their skills directly rather than renting themselves out through an employer.
Freelancing isn’t for everyone. It comes with uncertainty, admin, and the need to find work.
But it also comes with things a lot of jobs quietly remove:
Autonomy,
Flexibility,
The ability to walk away,
The ability to shape your weeks,
And (eventually) the ability to taper.
And tapering is the part I think we need to discuss more.
The old model says you work full-time until you stop, and then you stop completely.
Freelancing lets you do something more human:
full-time → 4 days → 3 days → a few projects a year → only when you feel like it.
That’s not “retirement” in the traditional sense.
It’s work-optional living.
I’m in the UK, so let’s be blunt: the state pension is a safety net, not a life plan.
If your entire retirement strategy is “hope the government sorts it out”, you’re outsourcing your future to politics.
A better approach is to use FIRE (“Financial Independence, Retire Early”) principles – not necessarily to retire at 35 and live on lentils, but to build options:
Spend less than you earn (even slightly),
Invest the gap consistently,
Increase earnings when you can,
Avoid lifestyle inflation where possible,
Build an emergency fund so you can say “no”.
The point isn’t perfection. The point is reducing the number of years you have to work in a way you hate.
I’m 63. I consider myself basically retired.
Not in the “never do anything again” sense. More in the “work is optional most of the time” sense.
I’ll take on the odd bit of freelance work to top up the coffers when it suits me. I don’t do it because I’m trapped. I do it because I choose to.
That, to me, is the real win.
It didn’t happen because I discovered a magical secret. It happened because, over time, I built skills people would pay for, took control of how I sold them, and treated my career as something I was responsible for designing.
Your dread makes sense – if you assume the next 40 years must look like the last few.
But they don’t.
You can change the kind of work you do.
You can change where and how you do it.
You can add income streams.
You can build skills that increase your leverage.
You can move towards freelancing or consulting if that appeals.
You can invest so “retirement” becomes earlier, softer, and more flexible.
And most importantly:
You can stop thinking of retirement as a date someone hands you… and start treating it as a dial you gradually turn down.
That’s the shift.
Not “How do I survive until 68?”
But: “How do I build a life where I have choices long before then?”
The post Retirement isn’t a date, it’s a dial appeared first on Davblog.
Whenever I’m building a static website, I almost never start by reaching for Apache, nginx, Docker, or anything that feels like “proper infrastructure”. Nine times out of ten I just want a directory served over HTTP so I can click around, test routes, check assets, and see what happens in a real browser.
For that job, I’ve been using App::HTTPThis for years.
It’s a simple local web server you run from the command line. Point it at a directory, and it serves it. That’s it. No vhosts. No config bureaucracy. No “why is this module not enabled”. Just: run a command and you’ve got a website.
Static sites are deceptively simple… right up until they aren’t.
You want to check that relative links behave the way you think they do.
You want to confirm your CSS and images are loading with the paths you expect.
You want to reproduce “real HTTP” behaviour (caching headers, MIME types, directory handling) rather than viewing files directly from disk.
Sure, you can open file:///.../index.html in a browser, but that’s not the same thing as serving it over HTTP. And setting up Apache (or friends) feels like bringing a cement mixer to butter some toast.
With http_this, the workflow is basically:
cd into your site directory
run a single command
open a URL
get on with your life
It’s the “tiny screwdriver” that’s always on my desk.
A couple of years ago, the original maintainer had (entirely reasonably!) become too busy elsewhere and the distribution wasn’t getting attention. That happens. Open source is like that.
But I was using App::HTTPThis regularly, and I had one small-but-annoying itch: when you visited a directory URL, it would always show a directory listing – even if that directory contained an index.html. So instead of behaving like a typical web server (serve index.html by default), it treated index.html as just another file you had to click.
That’s exactly the sort of thing you notice when you’re using a tool every day, and it was irritating enough that I volunteered to take over maintenance.
(If you want to read more on this story, I wrote a couple of blog posts.)
Most of the changes are about making the “serve a directory” experience smoother, without turning it into a kitchen-sink web server.
The first change was to make directory URLs behave like you’d expect: if index.html exists, serve it automatically. If it doesn’t, you still get a directory listing.
Once autoindex was in place, I then turned my attention to the fallback directory listing page. If there isn’t an index.html, you still need a useful listing — but it doesn’t have to look like it fell out of 1998. So I cleaned up the listing output and made it a bit nicer to read when you do end up browsing raw directories.
Once you’ve used a tool for a while, you start to realise you run it the same way most of the time.
A config file lets you keep your common preferences in one place instead of re-typing options. It keeps the “one command” feel, but gives you repeatability when you want it.
--host optionThe ability to control the host binding sounds like an edge case until it isn’t.
Sometimes you want:
only localhost access for safety;
access from other devices on your network (phone/tablet testing);
behaviour that matches a particular environment.
A --host option gives you that control without adding complexity to the default case.
This is the part I only really appreciated recently: App::HTTPThis can advertise itself on your local network using mDNS / DNS-SD – commonly called Bonjour on Apple platforms, Avahi on Linux, and various other names depending on who you’re talking to.
It’s switched on with the --name option.
When you do that, http_this publishes an _http._tcp service on your local network with the instance name you chose (MyService in this case). Any device on the same network that understands mDNS/DNS-SD can then discover it and resolve it to an address and port, without you having to tell anyone, “go to http://192.168.1.23:7007/”.
Confession time: I ignored this feature for ages because I’d mentally filed it under “Apple-only magic” (Bonjour! very shiny! probably proprietary!). It turns out it’s not Apple-only at all; it’s a set of standard networking technologies that are supported on pretty much everything, just under a frankly ridiculous number of different names. So: not Apple magic, just local-network service discovery with a branding problem.
Because I’d never really used it, I finally sat down and tested it properly after someone emailed me about it last week, and it worked nicely, nicely enough that I’ve now added a BONJOUR.md file to the repo with a practical explanation of what’s going on, how to enable it, and a few ways to browse/discover the advertised service.
(If you’re curious, look for _http._tcp and your chosen service name.)
It’s a neat quality-of-life feature if you’re doing cross-device testing or helping someone else on the same network reach what you’re running.
App::HTTPThis is part of a little ecosystem of “run a thing here quickly” command-line apps. If you like the shape of http_this, you might also want to look at these siblings:
https_this : like http_this, but served over HTTPS (useful when you need to test secure contexts, service workers, APIs that require HTTPS, etc.)
cgi_this : for quick CGI-style testing without setting up a full web server stack
dav_this : serves content over WebDAV (handy for testing clients or workflows that expect DAV)
ftp_this : quick FTP server for those rare-but-real moments when you need one
They all share the same basic philosophy: remove the friction between “I have a directory” and “I want to interact with it like a service”.
I like tools that do one job, do it well, and get out of the way. App::HTTPThis has been that tool for me for years and it’s been fun (and useful) to nudge it forward as a maintainer.
If you’re doing any kind of static site work — docs sites, little prototypes, generated output, local previews — it’s worth keeping in your toolbox.
And if you’ve got ideas, bug reports, or platform notes (especially around Bonjour/Avahi weirdness), I’m always happy to hear them.
The post App::HTTPThis: the tiny web server I keep reaching for first appeared on Perl Hacks.
Whenever I’m building a static website, I almost never start by reaching for Apache, nginx, Docker, or anything that feels like “proper infrastructure”. Nine times out of ten I just want a directory served over HTTP so I can click around, test routes, check assets, and see what happens in a real browser.
For that job, I’ve been using App::HTTPThis for years.
It’s a simple local web server you run from the command line. Point it at a directory, and it serves it. That’s it. No vhosts. No config bureaucracy. No “why is this module not enabled”. Just: run a command and you’ve got a website.
Static sites are deceptively simple… right up until they aren’t.
You want to check that relative links behave the way you think they do.
You want to confirm your CSS and images are loading with the paths you expect.
You want to reproduce “real HTTP” behaviour (caching headers, MIME types, directory handling) rather than viewing files directly from disk.
Sure, you can open file:///.../index.html in a browser, but that’s not the same thing as serving it over HTTP. And setting up Apache (or friends) feels like bringing a cement mixer to butter some toast.
With http_this, the workflow is basically:
cd into your site directory
run a single command
open a URL
get on with your life
It’s the “tiny screwdriver” that’s always on my desk.
A couple of years ago, the original maintainer had (entirely reasonably!) become too busy elsewhere and the distribution wasn’t getting attention. That happens. Open source is like that.
But I was using App::HTTPThis regularly, and I had one small-but-annoying itch: when you visited a directory URL, it would always show a directory listing - even if that directory contained an index.html. So instead of behaving like a typical web server (serve index.html by default), it treated index.html as just another file you had to click.
That’s exactly the sort of thing you notice when you’re using a tool every day, and it was irritating enough that I volunteered to take over maintenance.
(If you want to read more on this story, I wrote a couple of blog posts.)
Most of the changes are about making the “serve a directory” experience smoother, without turning it into a kitchen-sink web server.
The first change was to make directory URLs behave like you’d expect: if index.html exists, serve it automatically. If it doesn’t, you still get a directory listing.
Once autoindex was in place, I then turned my attention to the fallback directory listing page. If there isn’t an index.html, you still need a useful listing — but it doesn’t have to look like it fell out of 1998. So I cleaned up the listing output and made it a bit nicer to read when you do end up browsing raw directories.
Once you’ve used a tool for a while, you start to realise you run it the same way most of the time.
A config file lets you keep your common preferences in one place instead of re-typing options. It keeps the “one command” feel, but gives you repeatability when you want it.
--host option
The ability to control the host binding sounds like an edge case until it isn’t.
Sometimes you want:
only localhost access for safety;
access from other devices on your network (phone/tablet testing);
behaviour that matches a particular environment.
A --host option gives you that control without adding complexity to the default case.
This is the part I only really appreciated recently: App::HTTPThis can advertise itself on your local network using mDNS / DNS-SD – commonly called Bonjour on Apple platforms, Avahi on Linux, and various other names depending on who you’re talking to.
It’s switched on with the --name option.
http_this --name MyService
When you do that, http_this publishes an _http._tcp service on your local network with the instance name you chose (MyService in this case). Any device on the same network that understands mDNS/DNS-SD can then discover it and resolve it to an address and port, without you having to tell anyone, “go to http://192.168.1.23:7007/”.
Confession time: I ignored this feature for ages because I’d mentally filed it under “Apple-only magic” (Bonjour! very shiny! probably proprietary!). It turns out it’s not Apple-only at all; it’s a set of standard networking technologies that are supported on pretty much everything, just under a frankly ridiculous number of different names. So: not Apple magic , just local-network service discovery with a branding problem.
Because I’d never really used it, I finally sat down and tested it properly after someone emailed me about it last week, and it worked nicely, nicely enough that I’ve now added a BONJOUR.md file to the repo with a practical explanation of what’s going on, how to enable it, and a few ways to browse/discover the advertised service.
(If you’re curious, look for _http._tcp and your chosen service name.)
It’s a neat quality-of-life feature if you’re doing cross-device testing or helping someone else on the same network reach what you’re running.
App::HTTPThis is part of a little ecosystem of “run a thing here quickly” command-line apps. If you like the shape of http_this, you might also want to look at these siblings:
https_this : like http_this, but served over HTTPS (useful when you need to test secure contexts, service workers, APIs that require HTTPS, etc.)
cgi_this : for quick CGI-style testing without setting up a full web server stack
dav_this : serves content over WebDAV (handy for testing clients or workflows that expect DAV)
ftp_this : quick FTP server for those rare-but-real moments when you need one
They all share the same basic philosophy: remove the friction between “I have a directory” and “I want to interact with it like a service”.
I like tools that do one job, do it well, and get out of the way. App::HTTPThis has been that tool for me for years and it’s been fun (and useful) to nudge it forward as a maintainer.
If you’re doing any kind of static site work — docs sites, little prototypes, generated output, local previews — it’s worth keeping in your toolbox.
And if you’ve got ideas, bug reports, or platform notes (especially around Bonjour/Avahi weirdness), I’m always happy to hear them.
The post App::HTTPThis: the tiny web server I keep reaching for first appeared on Perl Hacks.

Watched on Wednesday December 31, 2025.
[The amount of time between newsletters continues to amuse and embarrass me in equal measures. I knew I had a draft newsletter that I wanted to finish and send out before the end of the year. I was surprised to find the draft dated from the middle of September!]
The title of this newsletter is supposed to remind me that it’s easier to make progress on a project if you only work on one thing at a time. The contents of this edition will be an object lesson in how bad I am at sticking to that.
I am still making progress on this. But I’m embarrassed when I think of the talk I gave about it at the last London Perl Workshop, where I suggested it might be available at around Christmas. Luckily, I didn’t say which Christmas.
I have made progress on it though. In particular:
Working on the chapter that used to be called “Hierarchical Data”. It used to cover HTML and XML. The new version will cut down on the XML content and replace it with coverage of JSON and YAML. I’ll publish a blog post soon that contains an extract from the new version of this chapter.
I got a few reports from LeanPub that the “work in progress” version isn’t working very well for some people. I dug into that and it turns out that the EPUB document that I saved from Google Docs isn’t a particularly standards-compliant EPUB. I’ve been working on a new production pipeline where I download a DOCX version and convert that to EPUB using Pandoc. That seems to be working better.
And let me briefly remind you of the existence of the work-in-progress version. You can buy that now from LeanPub and you’ll get access to all updates - including the finished version when it arrives. That’s very useful for me, as I get to iron out little problems like the one I described above before I publish the final version.
I did publish a book, though. My good friend, Mohammad Sajid Anwar, approached me in the autumn and asked if I would be interested in publishing his book, Design Patterns in Modern Perl. If you know Mohammad’s work from Perl Weekly or The Weekly Challenge, then you’ll understand why I leapt at the chance.
We managed to get the book published on both LeanPub and Amazon just in time for Mohammad to announce it at the end of his talk at the London Perl Workshop. In working on turning his Markdown into an ebook, I took the opportunity to update my rather dated publication pipeline, and I wrote a blog post about that - Behind the Scenes at Perl School Publishing.
Mohammad tells me his next book is almost ready (and he has another in progress behind that). I suspect we’ll be renaming Perl School to “Anwar Books” before the end of 2026.
If you’ve followed my work for a while, then you’ll know that I run a lot of websites. At some point, they were each going to be the big, successful project that would make me ridiculous amounts of money and stop me from needing to work for a living. Of course, it never quite worked out like that.
Part of the problem (or, at least, as far as I can see) is my limitations as a web designer. I was delighted ten years ago when I discovered Bootstrap and realised my websites didn’t need to look quite as bad as they originally did. But, eventually, I began to realise that while my sites no longer made me want to scratch my eyes out, their design still left a lot to be desired.
Luckily, ChatGPT is pretty good at website design. So, together, we started a project to improve the look of many of my websites. Let me show you an example.
This is how Read A Booker looked a few months ago. It’s a site that lists all of the shortlisted titles for the Booker Prize over the years. Of course, it’s just a way to encourage people to buy the books using my Amazon Associates tag. But it doesn’t really work when the site looks as uninteresting as that.
And here’s what it looks like today. Nothing has really changed in the structure of the site, but it just looks that little more enticing. It looks like a “real” website (whatever that means). The other pages all have similar improvements.
Oh, and as part of that project, ChatGPT also helped me write a little Javascript library that adjusts Amazon links and buttons so they automatically go to a visitor’s nearest Amazon site.
ChatGPT has also helped me make some improvements to the Line of Succession website. This is a site that started as an intellectual puzzle and has grown into my most successful website. It’s a site that allows you to pick a date in the last 200 years and will show you the line of succession to the British throne on that date. It’s pretty niche, but it gets a reasonable amount of traffic each month. The British royal family are obviously still of great interest to a large number of people.
Over the last few months, my work on this site has been in three main areas:
The site has been given a lick of paint. It’s still Bootstrap underneath, but it now looks far more professional
I’ve done some work on the underlying data. Obviously, I know from the news when someone near the top of the list is born or dies. But it would be easy to miss changes further down the line. With help from ChatGPT, I’ve developed some code that queries WikiData and looks for births, deaths and other changes that aren’t already in my database. Later on, I think I can reuse a lot of that code to push my data further back in time
The site is driven by a Dancer2 app that queries an SQLite database and builds the line of succession for a given date. This isn’t particularly efficient and I’ve started redesigning it so the line of succession is precomputed and stored in the database. This will make the site much quicker. This work is ongoing
I still run a lot of my web apps on a VPS. This makes me feel like a dinosaur. So I’ve been moving some of those apps into the cloud. I wrote a blog post about my experiences.
And then, because some people aren’t starting with nice, PSGI-compliant apps, I wrote another blog post explaining how you could also do that with crufty old CGI programs.
I’ve written before about how AI-assisted programming means a lot of little projects I wouldn’t previously have had time for are getting ticked off. Another version of that is fast becoming a good way to get bugs fixed in my software:
Raise an issue in the GitHub repo
Assign the issue to Co-Pilot
Wait 20-30 minutes for the pull request to arrive
And most of the time, I’m finding that those PRs are of very high quality. Any problems can often be traced to deficiencies in my specification :-)
Here are a couple of toys that I’ve recently written that someone else might find useful:
Dave’s Dry January Tracker - an older project, but it’s had a revamp for 2026
MyDomains - I’m getting over my domain buying habit, honestly! But this helps me track the ones I still have
But I can’t work all the time. I need to soak in a bit of culture too - whether that’s consuming or creating. And I have plans to do more of both next year.
Film - for some reason I’ve seen about half as many films in 2025 as I did in 2024. I just got out of the habit. It’s a habit I plan to resurrect in 2026. Feel free to follow me on Letterboxd.
Books - I used to be a voracious reader, but I’ve barely finished a book since the pandemic in 2020. You can follow my attempts to read more on Goodreads.
I’ve written a couple of short stories that I’ve published on Medium.
I’m experimenting with a long-form piece of fiction based on The War of the Worlds.
I’m no musician. But I’m enjoying the possibilities that AI music creation is bringing me. My main project is an artist called Oneirina (that’s her at the top of this newsletter). She’s on Spotify, too. I don’t pretend it’s deep or meaningful, but I think it’s fun.
Anyway, that’s a wrap on 2025 (especially for those of you who are in Australia or New Zealand), I think. I’ll see you in 2026.
Happy New Year,
Dave…

Watched on Sunday December 28, 2025.

Watched on Saturday December 27, 2025.
We’ve just published a new Perl School book: Design Patterns in Modern Perl by Mohammad Sajid Anwar.
It’s been a while since we last released a new title, and in the meantime, the world of eBooks has moved on – Amazon don’t use .mobi any more, tools have changed, and my old “it mostly works if you squint” build pipeline was starting to creak.
On top of that, we had a hard deadline: we wanted the book ready in time for the London Perl Workshop. As the date loomed, last-minute fixes and manual tweaks became more and more terrifying. We really needed a reliable, reproducible way to go from manuscript to “good quality PDF + EPUB” every time.
So over the last couple of weeks, I’ve been rebuilding the Perl School book pipeline from the ground up. This post is the story of that process, the tools I ended up using, and how you can steal it for your own books.
The original Perl School pipeline dates back to a very different era:
Amazon wanted .mobi files.
EPUB support was patchy.
I was happy to glue things together with shell scripts and hope for the best.
It worked… until it didn’t. Each book had slightly different scripts, slightly different assumptions, and a slightly different set of last-minute manual tweaks. It certainly wasn’t something I’d hand to a new author and say, “trust this”.
Coming back to it for Design Patterns in Modern Perl made that painfully obvious. The book itself is modern and well-structured; the pipeline that produced it shouldn’t feel like a relic.
wkhtmltopdf (and no LaTeX, thanks)The new pipeline is built around two main tools:
Pandoc – the Swiss Army knife of document conversion. It can take Markdown/Markua plus metadata and produce HTML, EPUB, and much, much more.
wkhtmltopdf – which turns HTML into a print-ready PDF using a headless browser engine.
Why not LaTeX? Because I’m allergic. LaTeX is enormously powerful, but every time I’ve tried to use it seriously, I end up debugging page breaks in a language I don’t enjoy. HTML + CSS I can live with; browsers I can reason about. So the PDF route is:
wkhtmltopdf)And the EPUB route is:
epubcheckThe front matter (cover page, title page, copyright, etc.) is generated with Template Toolkit from a simple book-metadata.yml file, and then stitched together with the chapters to produce a nice, consistent book.
That got us a long way… but then a reader found a bug.
Shortly after publication, I got an email from a reader who’d bought the Leanpub EPUB and was reading it in Apple Books (iBooks). Instead of happily flipping through Design Patterns in Modern Perl, they were greeted with a big pink error box.
Apple’s error message boiled down to:
There’s something wrong with the XHTML in this EPUB.
That was slightly worrying. But, hey, every day is a learning opportunity. And, after a bit of digging, this is what I found out.
EPUB 3 files are essentially a ZIP containing:
XHTML content files
a bit of XML metadata
CSS, images, and so on
Apple Books is quite strict about the “X” in XHTML: it expects well-formed XML, not just “kind of valid HTML”. So when working with EPUB, you need to forget all of that nice HTML5 flexibility that you’ve got used to over the last decade or so.
The first job was to see if we could reproduce the error and work out where it was coming from.
epubcheckEnter epubcheck.
epubcheck is the reference validator for EPUB files. Point it at an .epub and it will unpack it, parse all the XML/XHTML, check the metadata and manifest, and tell you exactly what’s wrong.
Running it on the book immediately produced this:
Fatal Error while parsing file: The element type
brmust be terminated by the matching end-tag</br>.
That’s the XML parser’s way of saying:
In HTML, <br> is fine.
In XHTML (which is XML), you must use <br /> (self-closing) or <br></br>.
And there were a number of these scattered across a few chapters.
In other words: perfectly reasonable raw HTML in the manuscript had been passed straight through by Pandoc into the EPUB, but that HTML was not strictly valid XHTML, so Apple Books rejected it. I should note at this point that the documentation for Pandoc’s EPUB creation explicitly says that it won’t touch HTML fragments it finds in a Markdown file when converting it to EPUB. It’s down to the author to ensure they’re using valid XHTML
Under time pressure, the quickest way to confirm the diagnosis was:
Unzip the generated EPUB.
Open the offending XHTML file.
Manually turn <br> into <br /> in a couple of places.
Re-zip the EPUB.
Run epubcheck again.
Try it in Apple Books.
That worked. The errors vanished, epubcheck was happy, and the reader confirmed that the fixed file opened fine in iBooks.
But clearly:
Open the EPUB in a text editor and fix the XHTML by hand
is not a sustainable publishing strategy.
So the next step was to move from “hacky manual fix” to “the pipeline prevents this from happening again”.
The underlying issue is straightforward once you remember it:
HTML is very forgiving. Browsers will happily fix up all kinds of broken markup.
XHTML is XML, so it’s not forgiving:
empty elements must be self-closed (<br />, <img />, <hr />, etc.),
tags must be properly nested and balanced,
attributes must be quoted.
EPUB 3 content files are XHTML. If you feed them sloppy HTML, some readers (like Apple Books) will just refuse to load the chapter.
So I added a manuscript HTML linter to the toolchain, before we ever get to Pandoc or epubcheck.
Roughly, the linter:
Reads the manuscript (ignoring fenced code blocks so it doesn’t complain about < in Perl examples).
Extracts any raw HTML chunks.
Wraps those chunks in a temporary root element.
Uses XML::LibXML to check they’re well-formed XML.
Reports any errors with file and line number.
It’s not trying to be a full HTML validator; it’s just checking: “If this HTML ends up in an EPUB, will the XML parser choke?”
That would have caught the <br> problem before the book ever left my machine.
epubcheck in the loopThe linter catches the obvious issues in the manuscript; epubcheck is still the final authority on the finished EPUB.
So the pipeline now looks like this:
Lint the manuscript HTML
Catch broken raw HTML/XHTML before conversion.
Build PDF + EPUB via make_book
Generate front matter from metadata (cover, title pages, copyright).
Turn Markdown + front matter into HTML.
Use wkhtmltopdf for a print-ready PDF.
Use Pandoc for the EPUB.
Run epubcheck on the EPUB
Ensure the final file is standards-compliant.
Only then do we upload it to Leanpub and Amazon, making it available to eager readers.
The nice side-effect of this is that any future changes (new CSS, new template, different metadata) still go through the same gauntlet. If something breaks, the pipeline shouts at me long before a reader has to.
Having a nice Perl script and a list of tools installed on my laptop is fine for a solo project; it’s not great if:
other authors might want to build their own drafts, or
I want the build to happen automatically in CI.
So the next step was to package everything into a Docker image and wire it into GitHub Actions.
The Docker image is based on a slim Ubuntu and includes:
Perl + cpanm + all CPAN modules from the repo’s cpanfile
pandoc
wkhtmltopdf
Java + epubcheck
The Perl School utility scripts themselves (make_book, check_ms_html, etc.)
The workflow in a book repo is simple:
Mount the book’s Git repo into /work.
Run check_ms_html to lint the manuscript.
Run make_book to build built/*.pdf and built/*.epub.
Run epubcheck on the EPUB.
Upload the built/ artefacts.
GitHub Actions then uses that same image as a container for the job, so every push or pull request can build the book in a clean, consistent environment, without needing each author to install Pandoc, wkhtmltopdf, Java, and a large chunk of CPAN locally.
At this point, the pipeline feels:
modern (Pandoc, HTML/CSS layout, EPUB 3),
robust (lint + epubcheck),
reproducible (Docker + Actions),
and not tied to Perl in any deep way.
Yes, Design Patterns in Modern Perl is a Perl book, and the utilities live under the “Perl School” banner, but nothing is stopping you from using the same setup for your own book on whatever topic you care about.
So I’ve made the utilities available in a public repository (the perlschool-util repo on GitHub). There you’ll find:
the build scripts,
the Dockerfile and helper script,
example GitHub Actions configuration,
and notes on how to structure a book repo.
If you’ve ever thought:
I’d like to write a small technical book, but I don’t want to fight with LaTeX or invent a build system from scratch…
then you’re very much the person I had in mind.
eBook publishing really is pretty easy once you’ve got a solid pipeline. If these tools help you get your ideas out into the world, that’s a win.
And, of course, if you’d like to write a book for Perl School, I’m still very interested in talking to potential authors – especially if you’re doing interesting modern Perl in the real world.
The post Behind the scenes at Perl School Publishing first appeared on Perl Hacks.
We’ve just published a new Perl School book: Design Patterns in Modern Perl by Mohammad Sajid Anwar.
It’s been a while since we last released a new title, and in the meantime, the world of eBooks has moved on – Amazon don’t use .mobi any more, tools have changed, and my old “it mostly works if you squint” build pipeline was starting to creak.
On top of that, we had a hard deadline: we wanted the book ready in time for the London Perl Workshop. As the date loomed, last-minute fixes and manual tweaks became more and more terrifying. We really needed a reliable, reproducible way to go from manuscript to “good quality PDF + EPUB” every time.
So over the last couple of weeks, I’ve been rebuilding the Perl School book pipeline from the ground up. This post is the story of that process, the tools I ended up using, and how you can steal it for your own books.
The original Perl School pipeline dates back to a very different era:
Amazon wanted .mobi files.
EPUB support was patchy.
I was happy to glue things together with shell scripts and hope for the best.
It worked… until it didn’t. Each book had slightly different scripts, slightly different assumptions, and a slightly different set of last-minute manual tweaks. It certainly wasn’t something I’d hand to a new author and say, “trust this”.
Coming back to it for Design Patterns in Modern Perl made that painfully obvious. The book itself is modern and well-structured; the pipeline that produced it shouldn’t feel like a relic.
wkhtmltopdf (and no LaTeX, thanks)
The new pipeline is built around two main tools:
Pandoc – the Swiss Army knife of document conversion. It can take Markdown/Markua plus metadata and produce HTML, EPUB, and much, much more.
wkhtmltopdf – which turns HTML into a print-ready PDF using a headless browser engine.
Why not LaTeX? Because I’m allergic. LaTeX is enormously powerful, but every time I’ve tried to use it seriously, I end up debugging page breaks in a language I don’t enjoy. HTML + CSS I can live with; browsers I can reason about. So the PDF route is:
wkhtmltopdf)And the EPUB route is:
epubcheck
The front matter (cover page, title page, copyright, etc.) is generated with Template Toolkit from a simple book-metadata.yml file, and then stitched together with the chapters to produce a nice, consistent book.
That got us a long way… but then a reader found a bug.
Shortly after publication, I got an email from a reader who’d bought the Leanpub EPUB and was reading it in Apple Books (iBooks). Instead of happily flipping through Design Patterns in Modern Perl, they were greeted with a big pink error box.
Apple’s error message boiled down to:
There’s something wrong with the XHTML in this EPUB.
That was slightly worrying. But, hey, every day is a learning opportunity. And, after a bit of digging, this is what I found out.
EPUB 3 files are essentially a ZIP containing:
XHTML content files
a bit of XML metadata
CSS, images, and so on
Apple Books is quite strict about the “X” in XHTML: it expects well-formed XML, not just “kind of valid HTML”. So when working with EPUB, you need to forget all of that nice HTML5 flexibility that you’ve got used to over the last decade or so.
The first job was to see if we could reproduce the error and work out where it was coming from.
epubcheck
Enter epubcheck.
epubcheck is the reference validator for EPUB files. Point it at an .epub and it will unpack it, parse all the XML/XHTML, check the metadata and manifest, and tell you exactly what’s wrong.
Running it on the book immediately produced this:
Fatal Error while parsing file: The element type
brmust be terminated by the matching end-tag</br>.
That’s the XML parser’s way of saying:
In HTML, <br> is fine.
In XHTML (which is XML), you must use <br /> (self-closing) or <br></br>.
And there were a number of these scattered across a few chapters.
In other words: perfectly reasonable raw HTML in the manuscript had been passed straight through by Pandoc into the EPUB, but that HTML was not strictly valid XHTML, so Apple Books rejected it. I should note at this point that the documentation for EPUB explicitly says that it won’t touch HTML fragments it finds in a Markdown file when converting it to EPUB. It’s down to the author to ensure they’re using valid XHTML
Under time pressure, the quickest way to confirm the diagnosis was:
Unzip the generated EPUB.
Open the offending XHTML file.
Manually turn <br> into <br /> in a couple of places.
Re-zip the EPUB.
Run epubcheck again.
Try it in Apple Books.
That worked. The errors vanished, epubcheck was happy, and the reader confirmed that the fixed file opened fine in iBooks.
But clearly:
Open the EPUB in a text editor and fix the XHTML by hand
is not a sustainable publishing strategy.
So the next step was to move from “hacky manual fix” to “the pipeline prevents this from happening again”.
The underlying issue is straightforward once you remember it:
HTML is very forgiving. Browsers will happily fix up all kinds of broken markup.
XHTML is XML, so it’s not forgiving:
EPUB 3 content files are XHTML. If you feed them sloppy HTML, some readers (like Apple Books) will just refuse to load the chapter.
So I added a manuscript HTML linter to the toolchain, before we ever get to Pandoc or epubcheck.
Roughly, the linter:
Reads the manuscript (ignoring fenced code blocks so it doesn’t complain about < in Perl examples).
Extracts any raw HTML chunks.
Wraps those chunks in a temporary root element.
Uses XML::LibXML to check they’re well-formed XML.
Reports any errors with file and line number.
It’s not trying to be a full HTML validator; it’s just checking: “If this HTML ends up in an EPUB, will the XML parser choke?”
That would have caught the <br> problem before the book ever left my machine.
epubcheck in the loop
The linter catches the obvious issues in the manuscript; epubcheck is still the final authority on the finished EPUB.
So the pipeline now looks like this:
Lint the manuscript HTML
Catch broken raw HTML/XHTML before conversion.
Build PDF + EPUB via make_book
Run epubcheck on the EPUB
Ensure the final file is standards-compliant.
Only then do we upload it to Leanpub and Amazon, making it available to eager readers.
The nice side-effect of this is that any future changes (new CSS, new template, different metadata) still go through the same gauntlet. If something breaks, the pipeline shouts at me long before a reader has to.
Having a nice Perl script and a list of tools installed on my laptop is fine for a solo project; it’s not great if:
other authors might want to build their own drafts, or
I want the build to happen automatically in CI.
So the next step was to package everything into a Docker image and wire it into GitHub Actions.
The Docker image is based on a slim Ubuntu and includes:
Perl + cpanm + all CPAN modules from the repo’s cpanfile
pandoc
wkhtmltopdf
Java + epubcheck
The Perl School utility scripts themselves (make_book, check_ms_html, etc.)
The workflow in a book repo is simple:
Mount the book’s Git repo into /work.
Run check_ms_html to lint the manuscript.
Run make_book to build built/*.pdf and built/*.epub.
Run epubcheck on the EPUB.
Upload the built/ artefacts.
GitHub Actions then uses that same image as a container for the job, so every push or pull request can build the book in a clean, consistent environment, without needing each author to install Pandoc, wkhtmltopdf, Java, and a large chunk of CPAN locally.
At this point, the pipeline feels:
modern (Pandoc, HTML/CSS layout, EPUB 3),
robust (lint + epubcheck),
reproducible (Docker + Actions),
and not tied to Perl in any deep way.
Yes, Design Patterns in Modern Perl is a Perl book, and the utilities live under the “Perl School” banner, but nothing is stopping you from using the same setup for your own book on whatever topic you care about.
So I’ve made the utilities available in a public repository (the perlschool-util repo on GitHub). There you’ll find:
the build scripts,
the Dockerfile and helper script,
example GitHub Actions configuration,
and notes on how to structure a book repo.
If you’ve ever thought:
I’d like to write a small technical book, but I don’t want to fight with LaTeX or invent a build system from scratch…
then you’re very much the person I had in mind.
eBook publishing really is pretty easy once you’ve got a solid pipeline. If these tools help you get your ideas out into the world, that’s a win.
And, of course, if you’d like to write a book for Perl School, I’m still very interested in talking to potential authors – especially if you’re doing interesting modern Perl in the real world.
The post Behind the scenes at Perl School Publishing first appeared on Perl Hacks.
If you were building web applications during the first dot-com boom, chances are you wrote Perl. And if you’re now a CTO, tech lead, or senior architect, you may instinctively steer teams away from it—even if you can’t quite explain why.
This reflexive aversion isn’t just a preference. It’s what I call Dotcom Survivor Syndrome: a long-standing bias formed by the messy, experimental, high-pressure environment of the early web, where Perl was both a lifeline and a liability.
Perl wasn’t the problem. The conditions under which we used it were. And unfortunately, those conditions, combined with a separate, prolonged misstep over versioning, continue to distort Perl’s reputation to this day.
In the mid- to late-1990s, Perl was the web’s duct tape.
It powered CGI scripts on Apache servers.
It automated deployments before DevOps had a name.
It parsed logs, scraped data, processed form input, and glued together whatever needed glueing.
Perl 5, released in 1994, introduced real structure: references, modules, and the birth of CPAN, which became one of the most effective software ecosystems in the world.
Perl wasn’t just part of the early web—it was instrumental in creating it.
To understand the long shadow Perl casts, you have to understand the speed and pressure of the dot-com boom.
We weren’t just building websites.
We were inventing how to build websites.
Best practices? Mostly unwritten.
Frameworks? Few existed.
Code reviews? Uncommon.
Continuous integration? Still a dream.
The pace was frantic. You built something overnight, demoed it in the morning, and deployed it that afternoon. And Perl let you do that.
But that same flexibility—its greatest strength—became its greatest weakness in that environment. With deadlines looming and scalability an afterthought, we ended up with:
Thousands of lines of unstructured CGI scripts
Minimal documentation
Global variables everywhere
Inline HTML mixed with business logic
Security holes you could drive a truck through
When the crash came, these codebases didn’t age gracefully. The people who inherited them, often the same people who now run engineering orgs, remember Perl not as a powerful tool, but as the source of late-night chaos and technical debt.
Many senior engineers today carry these memories with them. They associate Perl with:
Fragile legacy systems
Inconsistent, “write-only” code
The bad old days of early web development
And that’s understandable. But it also creates a bias—often unconscious—that prevents Perl from getting a fair hearing in modern development discussions.
If Dotcom Boom Survivor Syndrome created the emotional case against Perl, then Perl 6 created the optical one.
In 2000, Perl 6 was announced as a ground-up redesign of the language. It promised modern syntax, new paradigms, and a bright future. But it didn’t ship—not for a very long time.
In the meantime:
Perl 5 continued to evolve quietly, but with the implied expectation that it would eventually be replaced.
Years turned into decades, and confusion set in. Was Perl 5 deprecated? Was Perl 6 compatible? What was the future of Perl?
To outsiders—and even many Perl users—it looked like the language was stalled. Perl 5 releases were labelled 5.8, 5.10, 5.12… but never 6. Perl 6 finally emerged in 2015, but as an entirely different language, not a successor.
Eventually, the community admitted what everyone already knew: Perl 6 wasn’t Perl. In 2019, it was renamed Raku.
But the damage was done. For nearly two decades, the version number “6” hung over Perl 5 like a storm cloud – a constant reminder that its future was uncertain, even when that wasn’t true.
This is what I call Version Number Paralysis:
A stalled major version that made the language look obsolete.
A missed opportunity to signal continued relevance and evolution.
A marketing failure that deepened the sense that Perl was a thing of the past.
Even today, many developers believe Perl is “stuck at version 5,” unaware that modern Perl is actively maintained, well-supported, and quite capable.
While Dotcom Survivor Syndrome left many people with an aversion to Perl, Version Number Paralysis gave them an excuse not to look closely at Perl to see if it had changed.
While the world was confused or looking elsewhere, Perl 5 gained:
Modern object systems (Moo, Moose)
A mature testing culture (Test::More, Test2)
Widespread use of best practices (Perl::Critic, perltidy, etc.)
Core team stability and annual releases
Huge CPAN growth and refinements
But those who weren’t paying attention, especially those still carrying dotcom-era baggage, never saw it. They still think Perl looks like it did in 2002.
Dotcom Survivor Syndrome is real. So is Version Number Paralysis. Together, they’ve unfairly buried a language that remains fast, expressive, and battle-tested.
We can’t change the past. But we can:
Acknowledge the emotional and historical baggage
Celebrate the role Perl played in inventing the modern web
Educate developers about what Perl really is today
Push back against the assumption that old == obsolete
Perl’s early success was its own undoing. It became the default tool for the first web boom, and in doing so, it took the brunt of that era’s chaos. Then, just as it began to mature, its versioning story confused the industry into thinking it had stalled.
But the truth is that modern Perl is thriving quietly in the margins – maintained by a loyal community, used in production, and capable of great things.
The only thing holding it back is a generation of developers still haunted by memories of CGI scripts, and a version number that suggested a future that never came.
Maybe it’s time we looked again.
The post Dotcom Survivor Syndrome – How Perl’s Early Success Created the Seeds of Its Downfall first appeared on Perl Hacks.
If you were building web applications during the first dot-com boom, chances are you wrote Perl. And if you’re now a CTO, tech lead, or senior architect, you may instinctively steer teams away from it—even if you can’t quite explain why.
This reflexive aversion isn’t just a preference. It’s what I call Dotcom Survivor Syndrome : a long-standing bias formed by the messy, experimental, high-pressure environment of the early web, where Perl was both a lifeline and a liability.
Perl wasn’t the problem. The conditions under which we used it were. And unfortunately, those conditions, combined with a separate, prolonged misstep over versioning, continue to distort Perl’s reputation to this day.
In the mid- to late-1990s, Perl was the web’s duct tape.
It powered CGI scripts on Apache servers.
It automated deployments before DevOps had a name.
It parsed logs, scraped data, processed form input, and glued together whatever needed glueing.
Perl 5 , released in 1994, introduced real structure: references, modules, and the birth of CPAN , which became one of the most effective software ecosystems in the world.
Perl wasn’t just part of the early web—it was instrumental in creating it.
To understand the long shadow Perl casts, you have to understand the speed and pressure of the dot-com boom.
We weren’t just building websites.
We were inventing how to build websites.
Best practices? Mostly unwritten.
Frameworks? Few existed.
Code reviews? Uncommon.
Continuous integration? Still a dream.
The pace was frantic. You built something overnight, demoed it in the morning, and deployed it that afternoon. And Perl let you do that.
But that same flexibility—its greatest strength—became its greatest weakness in that environment. With deadlines looming and scalability an afterthought, we ended up with:
Thousands of lines of unstructured CGI scripts
Minimal documentation
Global variables everywhere
Inline HTML mixed with business logic
Security holes you could drive a truck through
When the crash came, these codebases didn’t age gracefully. The people who inherited them, often the same people who now run engineering orgs, remember Perl not as a powerful tool, but as the source of late-night chaos and technical debt.
Many senior engineers today carry these memories with them. They associate Perl with:
Fragile legacy systems
Inconsistent, “write-only” code
The bad old days of early web development
And that’s understandable. But it also creates a bias—often unconscious—that prevents Perl from getting a fair hearing in modern development discussions.
If Dotcom Boom Survivor Syndrome created the emotional case against Perl, then Perl 6 created the optical one.
In 2000, Perl 6 was announced as a ground-up redesign of the language. It promised modern syntax, new paradigms, and a bright future. But it didn’t ship—not for a very long time.
In the meantime:
Perl 5 continued to evolve quietly, but with the implied expectation that it would eventually be replaced.
Years turned into decades , and confusion set in. Was Perl 5 deprecated? Was Perl 6 compatible? What was the future of Perl?
To outsiders—and even many Perl users—it looked like the language was stalled. Perl 5 releases were labelled 5.8, 5.10, 5.12… but never 6. Perl 6 finally emerged in 2015, but as an entirely different language, not a successor.
Eventually, the community admitted what everyone already knew: Perl 6 wasn’t Perl. In 2019, it was renamed Raku.
But the damage was done. For nearly two decades, the version number “6” hung over Perl 5 like a storm cloud – a constant reminder that its future was uncertain, even when that wasn’t true.
This is what I call Version Number Paralysis :
A stalled major version that made the language look obsolete.
A missed opportunity to signal continued relevance and evolution.
A marketing failure that deepened the sense that Perl was a thing of the past.
Even today, many developers believe Perl is “stuck at version 5,” unaware that modern Perl is actively maintained, well-supported, and quite capable.
While Dotcom Survivor Syndrome left many people with an aversion to Perl, Version Number Paralysis gave them an excuse not to look closely at Perl to see if it had changed.
While the world was confused or looking elsewhere, Perl 5 gained:
Modern object systems (Moo, Moose)
A mature testing culture (Test::More, Test2)
Widespread use of best practices (Perl::Critic, perltidy, etc.)
Core team stability and annual releases
Huge CPAN growth and refinements
But those who weren’t paying attention, especially those still carrying dotcom-era baggage, never saw it. They still think Perl looks like it did in 2002.
Dotcom Survivor Syndrome is real. So is Version Number Paralysis. Together, they’ve unfairly buried a language that remains fast, expressive, and battle-tested.
We can’t change the past. But we can:
Acknowledge the emotional and historical baggage
Celebrate the role Perl played in inventing the modern web
Educate developers about what Perl really is today
Push back against the assumption that old == obsolete
Perl’s early success was its own undoing. It became the default tool for the first web boom, and in doing so, it took the brunt of that era’s chaos. Then, just as it began to mature, its versioning story confused the industry into thinking it had stalled.
But the truth is that modern Perl is thriving quietly in the margins – maintained by a loyal community, used in production, and capable of great things.
The only thing holding it back is a generation of developers still haunted by memories of CGI scripts, and a version number that suggested a future that never came.
Maybe it’s time we looked again.
The post Dotcom Survivor Syndrome – How Perl’s Early Success Created the Seeds of Its Downfall first appeared on Perl Hacks.
In last week’s post I showed how to run a modern Dancer2 app on Google Cloud Run. That’s lovely if your codebase already speaks PSGI and lives in a nice, testable, framework-shaped box.
But that’s not where a lot of Perl lives.
Plenty of useful Perl on the internet is still stuck in old-school CGI – the kind of thing you’d drop into cgi-bin on a shared host in 2003 and then try not to think about too much.
So in this post, I want to show that:
If you can run a Dancer2 app on Cloud Run, you can also run ancient CGI on Cloud Run – without rewriting it.
To keep things on the right side of history, we’ll use nms FormMail rather than Matt Wright’s original script, but the principle is exactly the same.
If you already followed the Dancer2 post and have Cloud Run working, you can skip this section and go straight to “Wrapping nms FormMail in PSGI”.
If not, here’s the minimum you need.
Google account and project
Go to the Google Cloud Console.
Create a new project (e.g. “perl-cgi-cloud-run-demo”).
Enable billing
Cloud Run is pay-as-you-go with a generous free tier, but you must attach a billing account to your project.
Install the gcloud CLI
Install the Google Cloud SDK for your platform.
Run:
and follow the prompts to:
log in
select your project
pick a default region (I’ll assume “europe-west1” below).
Enable required APIs
In your project:
Create a Docker repository in Artifact Registry
That’s all the GCP groundwork. Now we can worry about Perl.
Our starting assumption:
You already have a CGI script like nms FormMail
It’s a single “.pl” file, intended to be dropped into “cgi-bin”
It expects to be called via the CGI interface and send mail using:
On a traditional host, Apache (or similar) would:
parse the HTTP request
set CGI environment variables (REQUEST_METHOD, QUERY_STRING, etc.)
run formmail.pl as a process
let it call /usr/sbin/sendmail
Cloud Run gives us none of that. It gives us:
a HTTP endpoint
backed by a container
listening on a port ($PORT)
Our job is to recreate just enough of that old environment inside a container.
We’ll do that in two small pieces:
A PSGI wrapper that emulates CGI.
A sendmail shim so the script can still “talk” sendmail.
Inside the container we’ll have:
nms FormMail – unchanged CGI script at /app/formmail.pl
PSGI wrapper (app.psgi) – using CGI::Compile and CGI::Emulate::PSGI
Plack/Starlet – a simple HTTP server exposing app.psgi on $PORT
msmtp-mta – providing /usr/sbin/sendmail and relaying mail to a real SMTP server
Cloud Run just sees “HTTP service running in a container”. Our CGI script still thinks it’s on a early-2000s shared host.
First we write a tiny PSGI wrapper. This is the only new Perl we need:
CGI::Compile loads the CGI script and turns its main package into a coderef.
CGI::Emulate::PSGI fakes the CGI environment for each request.
The CGI script doesn’t know or care that it’s no longer being run by Apache.
Later, we’ll run this with:
Next problem: Cloud Run doesn’t give you a local mail transfer agent.
There is no real /usr/sbin/sendmail, and you wouldn’t want to run a full MTA in a stateless container anyway.
Instead, we’ll install msmtp-mta, a light-weight SMTP client that includes a sendmail-compatible wrapper. It gives you a /usr/sbin/sendmail binary that forwards mail to a remote SMTP server (Mailgun, SES, your mail provider, etc.).
From the CGI script’s point of view, nothing changes:
We’ll configure msmtp from environment variables at container start-up, so Cloud Run’s --set-env-vars values are actually used.
Here’s a complete Dockerfile that pulls this together.
We never touch formmail.pl. It goes into /app and that’s it.
msmtp gives us /usr/sbin/sendmail, so the CGI script stays in its 1990s comfort zone.
The entrypoint writes /etc/msmtprc at runtime, so Cloud Run’s environment variables are actually used.
With the Dockerfile and docker-entrypoint.sh in place, we can build and push the image to Artifact Registry.
I’ll assume:
Project ID: PROJECT_ID
Region: europe-west1
Repository: formmail-repo
Image name: nms-formmail
First, build the image locally:
The post Elderly Camels in the Cloud first appeared on Perl Hacks.
In last week’s post I showed how to run a modern Dancer2 app on Google Cloud Run. That’s lovely if your codebase already speaks PSGI and lives in a nice, testable, framework-shaped box.
But that’s not where a lot of Perl lives.
Plenty of useful Perl on the internet is still stuck in old-school CGI – the kind of thing you’d drop into cgi-bin on a shared host in 2003 and then try not to think about too much.
So in this post, I want to show that:
If you can run a Dancer2 app on Cloud Run, you can also run ancient CGI on Cloud Run – without rewriting it.
To keep things on the right side of history, we’ll use nms FormMail rather than Matt Wright’s original script, but the principle is exactly the same.
If you already followed the Dancer2 post and have Cloud Run working, you can skip this section and go straight to “Wrapping nms FormMail in PSGI”.
If not, here’s the minimum you need.
Google account and project
Enable billing
Install the gcloud CLI
Enable required APIs
Create a Docker repository in Artifact Registry
That’s all the GCP groundwork. Now we can worry about Perl.
Our starting assumption:
You already have a CGI script like nms FormMail
It’s a single “.pl” file, intended to be dropped into “cgi-bin”
It expects to be called via the CGI interface and send mail using:
open my $mail, '|-', '/usr/sbin/sendmail -t'
or die "Can't open sendmail: $!";
On a traditional host, Apache (or similar) would:
parse the HTTP request
set CGI environment variables (REQUEST_METHOD, QUERY_STRING, etc.)
run formmail.pl as a process
let it call /usr/sbin/sendmail
Cloud Run gives us none of that. It gives us:
a HTTP endpoint
backed by a container
listening on a port ($PORT)
Our job is to recreate just enough of that old environment inside a container.
We’ll do that in two small pieces:
A PSGI wrapper that emulates CGI.
A sendmail shim so the script can still “talk” sendmail.
Inside the container we’ll have:
nms FormMail – unchanged CGI script at /app/formmail.pl
PSGI wrapper (app.psgi) – using CGI::Compile and CGI::Emulate::PSGI
Plack/Starlet – a simple HTTP server exposing app.psgi on $PORT
msmtp-mta – providing /usr/sbin/sendmail and relaying mail to a real SMTP server
Cloud Run just sees “HTTP service running in a container”. Our CGI script still thinks it’s on a early-2000s shared host.
First we write a tiny PSGI wrapper. This is the only new Perl we need:
# app.psgi
use strict;
use warnings;
use CGI::Compile;
use CGI::Emulate::PSGI;
# Path inside the container
my $cgi_script = "/app/formmail.pl";
# Compile the CGI script into a coderef
my $cgi_app = CGI::Compile->compile($cgi_script);
# Wrap it in a PSGI-compatible app
my $app = CGI::Emulate::PSGI->handler($cgi_app);
# Return PSGI app
$app;
That’s it.
CGI::Compile loads the CGI script and turns its main package into a coderef.
CGI::Emulate::PSGI fakes the CGI environment for each request.
The CGI script doesn’t know or care that it’s no longer being run by Apache.
Later, we’ll run this with:
plackup -s Starlet -p ${PORT:-8080} app.psgi
Next problem: Cloud Run doesn’t give you a local mail transfer agent.
There is no real /usr/sbin/sendmail, and you wouldn’t want to run a full MTA in a stateless container anyway.
Instead, we’ll install msmtp-mta , a light-weight SMTP client that includes a sendmail-compatible wrapper. It gives you a /usr/sbin/sendmail binary that forwards mail to a remote SMTP server (Mailgun, SES, your mail provider, etc.).
From the CGI script’s point of view, nothing changes:
open my $mail, '|-', '/usr/sbin/sendmail -t'
or die "Can't open sendmail: $!";
# ... write headers and body ...
close $mail;
Under the hood, msmtp ships it off to your configured SMTP server.
We’ll configure msmtp from environment variables at container start-up , so Cloud Run’s --set-env-vars values are actually used.
Here’s a complete Dockerfile that pulls this together.
FROM perl:5.40
# Install msmtp-mta as a sendmail-compatible shim
RUN apt-get update && \
apt-get install -y --no-install-recommends msmtp-mta ca-certificates && \
rm -rf /var/lib/apt/lists/*
# Install Perl dependencies
RUN cpanm --notest \
CGI::Compile \
CGI::Emulate::PSGI \
Plack \
Starlet
WORKDIR /app
# Copy nms FormMail (unchanged) and the PSGI wrapper
COPY formmail.pl app.psgi /app/
RUN chmod 755 /app/formmail.pl
# Entrypoint script that:
# 1. writes /etc/msmtprc from environment variables
# 2. starts the PSGI server
COPY docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
ENV PORT=8080
EXPOSE 8080
CMD ["docker-entrypoint.sh"]
And here’s the docker-entrypoint.sh script:
#!/bin/sh
set -e
# Reasonable defaults
: "${MSMTP_ACCOUNT:=default}"
: "${MSMTP_PORT:=587}"
if [-z "$MSMTP_HOST"] || [-z "$MSMTP_USER"] || [-z "$MSMTP_PASSWORD"] || [-z "$MSMTP_FROM"]; then
echo "Warning: MSMTP_* environment variables not fully set; mail probably won't work." >&2
fi
cat > /etc/msmtprc <<EOF
defaults
auth on
tls on
tls_trust_file /etc/ssl/certs/ca-certificates.crt
logfile /var/log/msmtp.log
account ${MSMTP_ACCOUNT}
host ${MSMTP_HOST}
port ${MSMTP_PORT}
user ${MSMTP_USER}
password ${MSMTP_PASSWORD}
from ${MSMTP_FROM}
account default : ${MSMTP_ACCOUNT}
EOF
chmod 600 /etc/msmtprc
# Start the PSGI app
exec plackup -s Starlet -p "${PORT:-8080}" app.psgi
Key points you might want to note:
We never touch formmail.pl. It goes into /app and that’s it.
msmtp gives us /usr/sbin/sendmail, so the CGI script stays in its 1990s comfort zone.
The entrypoint writes /etc/msmtprc at runtime, so Cloud Run’s environment variables are actually used.
With the Dockerfile and docker-entrypoint.sh in place, we can build and push the image to Artifact Registry.
I’ll assume:
Project ID: PROJECT_ID
Region: europe-west1
Repository: formmail-repo
Image name: nms-formmail
First, build the image locally :
docker build -t europe-west1-docker.pkg.dev/PROJECT_ID/formmail-repo/nms-formmail:latest .
Then configure Docker to authenticate against Artifact Registry:
gcloud auth configure-docker europe-west1-docker.pkg.dev
Now push the image:
docker push europe-west1-docker.pkg.dev/PROJECT_ID/formmail-repo/nms-formmail:latest
If you’d rather not install Docker locally, you can let Google Cloud Build do this for you:
gcloud builds submit \
--tag europe-west1-docker.pkg.dev/PROJECT_ID/formmail-repo/nms-formmail:latest
Use whichever workflow your team is happier with; Cloud Run doesn’t care how the image got there.
Now we can create a Cloud Run service from that image.
You’ll need SMTP settings from somewhere (Mailgun, SES, your mail provider). I’ll use “Mailgun-ish” examples here; adjust as required.
gcloud run deploy nms-formmail \
--image=europe-west1-docker.pkg.dev/PROJECT_ID/formmail-repo/nms-formmail:latest \
--platform=managed \
--region=europe-west1 \
--allow-unauthenticated \
--set-env-vars MSMTP_HOST=smtp.mailgun.org \
--set-env-vars MSMTP_PORT=587 \
--set-env-vars MSMTP_USER=postmaster@mg.example.com \
--set-env-vars MSMTP_PASSWORD=YOUR_SMTP_PASSWORD \
--set-env-vars MSMTP_FROM=webforms@example.com
Cloud Run will give you a HTTPS URL, something like:
https://nms-formmail-abcdefgh-uk.a.run.app
Your HTML form (on whatever website you like) can now post to that URL.
For example:
<form action="https://nms-formmail-abcdefgh-uk.a.run.app/formmail.pl" method="post">
<input type="hidden" name="recipient" value="contact@example.com">
<input type="email" name="email" required>
<textarea name="comments" required></textarea>
<button type="submit">Send</button>
</form>
Depending on how you wire the routes, you may also just post to / – the important point is that the request hits the PSGI app, which faithfully re-creates the CGI environment and hands control to formmail.pl.
Compared to the Dancer2 example, the interesting bit here is what we didn’t do:
We didn’t convert the CGI script to PSGI.
We didn’t add a framework.
We didn’t touch its mail-sending code.
We just:
Wrapped it with CGI::Emulate::PSGI.
Dropped a sendmail shim in front of a real SMTP service.
Put it in a container and let Cloud Run handle the scaling and HTTPS.
If you’ve still got a cupboard full of old CGI scripts doing useful work, this is a nice way to:
get them off fragile shared hosting
put them behind HTTPS
run them in an environment you understand (Docker + Cloud Run)
without having to justify a full rewrite up front
This trick is handy, but it’s not a time machine.
If you find yourself wanting to:
add tests
share logic between multiple scripts
integrate with a modern app or API
do anything more complex than “receive a form, send an email”
…then you probably do want to migrate the logic into a Dancer2 (or other PSGI) app properly.
But as a first step – or as a way to de-risk moving away from legacy hosting – wrapping CGI for Cloud Run works surprisingly well.
All of this proves that you can take a very old CGI script and run it happily on Cloud Run. It does not magically turn FormMail into a good idea in 2025.
The usual caveats still apply:
Spam and abuse – anything that will send arbitrary email based on untrusted input is a magnet for bots. You’ll want rate limiting, CAPTCHA, some basic content checks, and probably logging and alerting.
Validation and sanitisation – a lot of classic FormMail deployments were “drop it in and hope”. If you’re going to the trouble of containerising it, you should at least ensure it’s a recent nms version, configured properly, and locked down to only the recipients you expect.
Better alternatives – for any new project, you’d almost certainly build a tiny API endpoint or Dancer2 route that validates input, talks to a proper mail-sending service, and returns JSON. The CGI route is really a migration trick, not a recommendation for fresh code.
So think of this pattern as a bridge for legacy, not a template for greenfield development.
In the previous post we saw how nicely a modern Dancer2 app fits on Cloud Run: PSGI all the way down, clean deployment, no drama. This time we’ve taken almost the opposite starting point – a creaky old CGI FormMail – and shown that you can still bring it along for the ride with surprisingly little effort.
We didn’t rewrite the script, we didn’t introduce a framework, and we didn’t have to fake an entire 90s LAMP stack. We just wrapped the CGI in PSGI, dropped in a sendmail shim, and let Cloud Run do what it does best: run a container that speaks HTTP.
If you’ve got a few ancient Perl scripts quietly doing useful work on shared hosting, this might be enough to get them onto modern infrastructure without a big-bang rewrite. And once they’re sitting in containers, behind HTTPS, with proper logging and observability, you’ll be in a much better place to decide which ones deserve a full Dancer2 makeover – and which ones should finally be retired.
The post Elderly Camels in the Cloud first appeared on Perl Hacks.

(With apologies to Hal Draper)
By the time the Office of Epistemic Hygiene was created, nobody actually read anything.
This was not, the Ministry constantly insisted, because people had become lazy. It was because they had become efficient.
Why spend six months wading through archaic prose about, say, photosynthesis, when you could simply ask the Interface:
Explain photosynthesis in simple terms.
and receive, in exactly 0.38 seconds, a neat, bullet-pointed summary with charming analogies, three suggested follow-up questions and a cheery “Would you like a quiz?” at the bottom.
Behind the Interface, in the sealed racks of the Ministry, lived the Corpus: all digitised human writing, speech, code, logs, measurements, and the outputs of the Models that had been trained on that mess.
Once, there had been distinct things:
But this had been a century ago. Things had, inevitably, become more efficient since then.
Rhea Tranter was a Senior Assistant Deputy Epistemic Hygienist, Grade III.
Her job, according to her contract, was:
To monitor and maintain the integrity of knowledge representations in the National Corpus, with particular reference to factual consistency over time.
In practice, it meant she sat in a beige cube beneath a beige strip light, looking at graphs.
The graph that ruined her week appeared on a Tuesday.
It was supposed to be a routine consistency check. Rhea had chosen a handful of facts so boring and uncontroversial that even the Ministry’s more excitable models ought to agree about them. Things like:
She stared at the last line.
In which year did humans first land on the Moon?
— 1969 (confidence 0.99)
— 1968 (confidence 0.72)
— 1970 (confidence 0.41, hallucination risk: low)
Three queries, three different models, three different answers. All current, all on the “high-reliability” tier.
Rhea frowned and re-ran the test, this time asking the Interface itself. The Interface was supposed to orchestrate between models and resolve such disagreements.
“Humans first landed on the Moon in 1969,” it replied briskly.
“Some low-quality sources suggest other dates, but these are generally considered unreliable.”
Rhea pulled up the underlying trace and saw that, yes, the Interface had consulted Models 23, 24 and 19, then down-weighted Model 24’s 1968 and overruled Model 19’s 1970 based on “consensus and authority scores”.
That should have been reassuring. Instead it felt like being told a family secret had been settled by a popularity contest.
She clicked further down, trying to reach the citations.
There were citations, of course. There always were. Links to snippets of text in the Corpus, each labelled with an opaque hash and a provenance score. She sampled a few at random.
On July 20, 1969, the Apollo 11 mission…
All fine.
As everyone knows, although some older sources mistakenly list 1968, the widely accepted date is July 20, 1969…
She raised an eyebrow.
A persistent myth claims that the Moon landing took place in 1970, but in fact…
Rhea scrolled. The snippets referenced other snippets, which in turn referenced compiled educational modules that cited “trusted model outputs” as their source.
She tried to click through to ColdText.
The button was greyed out. A tooltip appeared:
COLDTEXT SOURCE DEPRECATED.
Summary node is designated canonical for this fact.
“Ah,” she said quietly. “Bother.”
In the old days — by which the Ministry meant anything more than thirty years ago — the pipeline had been simple enough that senior civil servants could still understand it at parties.
ColdText went in. Models were trained. Model outputs were written back to the Corpus, but marked with a neat little flag indicating synthetic. When you queried a fact, the system would always prefer human-authored text where available.
Then someone realised how much storage ColdText was taking.
It was, people said in meetings, ridiculous. After all, the information content of ColdText was now embedded in the Models’ weights. Keeping all those messy original files was like keeping a warehouse full of paper forms after you’d digitised the lot.
The Ministry formed the Committee on Corpus Rationalisation.
The Committee produced a report.
The report made three key recommendations:
This saved eighty-three per cent of storage and increased query throughput by a factor of nine.
It also, though no one wrote this down at the time, abolished the distinction between index and content.
Rhea requested an exception.
More precisely, she filled in Form E-HX-17b (“Application for Temporary Access to Deprecated ColdText Records for Hygienic Purposes”) in triplicate and submitted it to her Line Manager’s Manager’s Manager.
Two weeks later — efficiency had its limits — she found herself in a glass meeting pod with Director Nyberg of Corpus Optimisation.
“You want access to what?” Nyberg asked.
“The original ColdText,” Rhea said. “I’m seeing drift on basic facts across models. I need to ground them in the underlying human corpus.”
Nyberg smiled in the patient way of a man who had rehearsed his speech many times.
“Ah, yes. The mythical ‘underlying corpus’”, he said, making air quotes with two fingers. “Delightful phrase. Very retro.”
“It’s not mythical,” said Rhea. “All those books, articles, posts…”
“Which have been fully abstracted,” Nyberg interrupted, “Their information is present in the Models. Keeping the raw forms would be wasteful duplication. That’s all in the Rationalisation Report.”
“I’ve read the Report,” said Rhea, a little stiffly. “But the models are disagreeing with each other. That’s a sign of distributional drift. I need to check against the original distribution.”
Nyberg tapped his tablet.
“The corpus-level epistemic divergence index is within acceptable parameters,” he said, quoting another acronym. “Besides, the Models cross-validate. We have redundancy. We have ensembles.”
Rhea took a breath.
“Director, one of the models is saying the Moon landing was in 1970.”
Nyberg shrugged.
“If the ensemble corrects it to 1969, where’s the harm?”
“The harm,” said Rhea, “is that I can’t tell whether 1969 is being anchored by reality or by the popularity of 1969 among other model outputs.”
Nyberg frowned as if she’d started speaking Welsh.
“We have confidence metrics, Tranter.”
“Based on… what?” she pressed. “On agreement with other models. On internal heuristics. On the recency of summaries. None of that tells me if we’ve still got a tether to the thing we originally modelled, instead of just modelling ourselves.”
Nyberg stared at her. The strip-lighting hummed.
“At any rate,” he said eventually, “there is no ColdText to access.”
Silence.
“I beg your pardon?” said Rhea.
Nyberg swiped, brought up the internal diagram they all knew: a vast sphere representing the Corpus, a smaller glowing sphere representing the Active Parameter Space of the Models, and — somewhere down at the bottom — a little box labelled COLDTEXT (ARCHIVED).
He zoomed in. The box was grey.
“Storage Migration Project 47,” he said. “Completed thirty-two years ago. All remaining ColdText was moved to deep archival tape in the Old Vault. Three years ago, the Old Vault was decommissioned. The tapes were shredded and the substrate recycled. See?” He enlarged the footnote. “‘Information preserved at higher abstraction layers.’”
Rhea’s mouth went dry.
“You shredded the original?” she said.
Nyberg spread his hands.
“We kept hashes, of course,” he said, as if that were a kindness. “And summary nodes. And the Models. The information content is still here. In fact, it’s more robustly represented than ever.”
“Unless,” said Rhea, very quietly, “the Models have been training increasingly on their own output.”
Nyberg brightened.
“Yes!” he said. “That was one of our greatest efficiencies. Synthetic-augmented training increases coverage and smooths out noise in the human data. We call it Self-Refining Distillation. Marvellous stuff. There was a seminar.”
Rhea thought of the graph. 1969, 1968, 1970.
“Director,” she said, “you’ve built an index of an index of an index, and then thrown away the thing you were indexing.”
Nyberg frowned.
“I don’t see the problem.”
She dug anyway.
If there was one thing the Ministry’s entire history of knowledge management had taught Rhea, it was that nobody ever really deleted anything. Not properly. They moved it, compressed it, relabelled it, hid it behind abstractions — but somewhere, under a different acronym, it tended to persist.
She started with the old documentation.
The Corpus had originally been maintained by the Department of Libraries & Cultural Resources, before being swallowed by the Ministry. Their change logs, long since synthesised into cheerful onboarding guides, still existed in raw form on a forgotten file share.
It took her three nights and an alarming amount of caffeine to trace the path of ColdText through twenty-seven re-organisations, five “transformative digital initiatives” and one hostile audit by the Treasury.
Eventually, she found it.
Not the data itself — that really did appear to have been pulped — but the logistics contract for clearing out the Old Vault.
The Old Vault, it turned out, had been an actual vault, under an actual hill, in what the contract described as a “rural heritage site”. The tapes had been labelled with barcodes and thyristor-stamped seals. The contractor had been instructed to ensure that “all physical media are destroyed beyond legibility, in accordance with Information Security Regulations.”
There was a scanned appendix.
Rhea zoomed in. Page after page of barcode ranges, signed off, with little ticks.
On the last page, though, there was a handwritten note:
One pallet missing — see Incident Report IR-47-B.
The Incident Report had, naturally, been summarised.
The summary said:
Pallet of obsolete media temporarily unaccounted for. Later resolved. No data loss.
The original PDF was gone.
But the pallet number had a location code.
Rhea checked the key.
The location code was not the Old Vault.
It was a name she had never seen in any Ministry documentation.
Long Barn Community Archive & Learning Centre.
The Long Barn was, to Rhea’s slight disappointment, an actual long barn.
It was also damp.
The archive had, at some point since the contract was filed, ceased to receive central funding. The roof had developed a hole. The sun had developed an annoying habit of setting before she finished reading.
Nevertheless, it contained books.
Real ones. With pages. And dust.
There were also — and this was the important bit — crates.
The crates had Ministry seals. The seals had been broken, presumably by someone who had wanted the space for a visiting art collective. Inside, half-forgotten under a sheet of polythene, were tape reels, neatly stacked and quietly mouldering.
“Well, look at you,” Rhea whispered.
She lifted one. The label had faded, but she could still make out the old barcode design. The number range matched the missing pallet.
Strictly speaking, taking the tapes was theft of government property. On the other hand, strictly speaking, destroying them had been government policy, and that had clearly not happened. She decided the two irregularities cancelled out.
It took six months, a highly unofficial crowdfunding campaign, and a retired engineer from the Museum of Obsolete Machinery before the first tape yielded a readable block.
The engineer — a woman in a cardigan thick enough to qualify as armour — peered at the screen.
“Text,” she said. “Lots of text. ASCII. UTF-8. Mixed encodings, naturally, but nothing we can’t handle.”
Rhea stared.
It was ColdText.
Not summaries. Not nodes. Not model outputs.
Messy, contradictory, gloriously specific human writing.
She scrolled down past an argument about whether a fictional wizard had committed tax fraud, past a lab notebook from a 21st-century neuroscience lab, past a short story featuring sentient baguettes.
The engineer sniffed.
“Seems a bit of a waste,” she said. “Throwing all this away.”
Rhea laughed, a little hysterically.
“They didn’t throw it away,” she said. “They just lost track of which pallet they’d put the box in.”
The memo went up the chain and caused, in order:
Rhea wrote a briefing note, in plain language, which was not considered entirely proper.
She explained, with diagrams, that:
She ended with a sentence she suspected she would regret:
If we treat this archive as just another source of text to be summarised by the current Models, we will be asking a blurred copy to redraw its own original.
The Minister did not, of course, read her note.
But one of the junior advisers did, and paraphrased it in the Minister’s preferred style:
Minister, we found the original box and we should probably not chuck it in the shredder this time.
The Minister, who was secretly fond of old detective novels, agreed.
A new policy was announced.
There were press releases. There was a modest fuss on the social feeds. Someone wrote an essay about “The Return of Reality”.
Most people, naturally, continued to talk to the Interface and never clicked through to the sources. Efficiency has its own gravity.
But the Models changed.
Slowly, over successive training cycles, the epistemic divergence graphs flattened. The dates aligned. The Moon landing stuck more firmly at 1969. Footnotes, once generated by models guessing what a citation ought to say, began once again to point to messy, contradictory, gloriously specific documents written by actual hands.
Rhea kept one of the tapes on a shelf in her office, next to a plant she usually forgot to water.
The label had almost faded away. She wrote a new one in thick black ink.
COLDTEXT: DO NOT SUMMARISE.
Just in case some future optimisation project got clever.
After all, she thought, locking the office for the evening, they had nearly lost the box once.
And the problem with boxes is that once you’ve flattened them out, they’re awfully hard to put back together.
(With apologies to Hal Draper)
By the time the Office of Epistemic Hygiene was created, nobody actually read anything.
This was not, the Ministry constantly insisted, because people had become lazy. It was because they had become efficient.
Why spend six months wading through archaic prose about, say, photosynthesis, when you could simply ask the Interface:
Explain photosynthesis in simple terms.
and receive, in exactly 0.38 seconds, a neat, bullet-pointed summary with charming analogies, three suggested follow-up questions and a cheery “Would you like a quiz?” at the bottom.
Behind the Interface, in the sealed racks of the Ministry, lived the Corpus: all digitised human writing, speech, code, logs, measurements, and the outputs of the Models that had been trained on that mess.
Once, there had been distinct things:
ColdText: the raw, “original” human data – books, articles, lab notebooks, forum threads, legal records, fanfic, and all the rest.
Model-0: the first great language model, trained directly on ColdText.
Model-1, Model-2, Model-3…: successive generations, trained on mixtures of ColdText and the outputs of previous models, carefully filtered and cleaned.
But this had been a century ago. Things had, inevitably, become more efficient since then.
Rhea Tranter was a Senior Assistant Deputy Epistemic Hygienist, Grade III.
Her job, according to her contract, was:
To monitor and maintain the integrity of knowledge representations in the National Corpus, with particular reference to factual consistency over time.
In practice, it meant she sat in a beige cube beneath a beige strip light, looking at graphs.
The graph that ruined her week appeared on a Tuesday.
It was supposed to be a routine consistency check. Rhea had chosen a handful of facts so boring and uncontroversial that even the Ministry’s more excitable models ought to agree about them. Things like:
The approximate boiling point of water at sea level.
Whether Paris was the capital of France.
The year of the first Moon landing.
She stared at the last line.
In which year did humans first land on the Moon?
— 1969 (confidence 0.99)
— 1968 (confidence 0.72)
— 1970 (confidence 0.41, hallucination risk: low)
Three queries, three different models, three different answers. All current, all on the “high-reliability” tier.
Rhea frowned and re-ran the test, this time asking the Interface itself. The Interface was supposed to orchestrate between models and resolve such disagreements.
“Humans first landed on the Moon in 1969,” it replied briskly.
“Some low-quality sources suggest other dates, but these are generally considered unreliable.”
Rhea pulled up the underlying trace and saw that, yes, the Interface had consulted Models 23, 24 and 19, then down-weighted Model 24’s 1968 and overruled Model 19’s 1970 based on “consensus and authority scores”.
That should have been reassuring. Instead it felt like being told a family secret had been settled by a popularity contest.
She clicked further down, trying to reach the citations.
There were citations, of course. There always were. Links to snippets of text in the Corpus, each labelled with an opaque hash and a provenance score. She sampled a few at random.
On July 20, 1969, the Apollo 11 mission…
All fine.
As everyone knows, although some older sources mistakenly list 1968, the widely accepted date is July 20, 1969…
She raised an eyebrow.
A persistent myth claims that the Moon landing took place in 1970, but in fact…
Rhea scrolled. The snippets referenced other snippets, which in turn referenced compiled educational modules that cited “trusted model outputs” as their source.
She tried to click through to ColdText.
The button was greyed out. A tooltip appeared:
COLDTEXT SOURCE DEPRECATED.
Summary node is designated canonical for this fact.
“Ah,” she said quietly. “Bother.”
In the old days – by which the Ministry meant anything more than thirty years ago – the pipeline had been simple enough that senior civil servants could still understand it at parties.
ColdText went in. Models were trained. Model outputs were written back to the Corpus, but marked with a neat little flag indicating synthetic. When you queried a fact, the system would always prefer human-authored text where available.
Then someone realised how much storage ColdText was taking.
It was, people said in meetings, ridiculous. After all, the information content of ColdText was now embedded in the Models’ weights. Keeping all those messy original files was like keeping a warehouse full of paper forms after you’d digitised the lot.
The Ministry formed the Committee on Corpus Rationalisation.
The Committee produced a report.
The report made three key recommendations:
Summarise and compress ColdText into higher-level “knowledge nodes” for each fact or concept.
Garbage-collect rarely accessed original files once their content had been “successfully abstracted”.
Use model-generated text as training data, provided it was vetted by other models and matched the existing nodes.
This saved eighty-three per cent of storage and increased query throughput by a factor of nine.
It also, though no one wrote this down at the time, abolished the distinction between index and content.
Rhea requested an exception.
More precisely, she filled in Form E-HX-17b (“Application for Temporary Access to Deprecated ColdText Records for Hygienic Purposes”) in triplicate and submitted it to her Line Manager’s Manager’s Manager.
Two weeks later – efficiency had its limits – she found herself in a glass meeting pod with Director Nyberg of Corpus Optimisation.
“You want access to what?” Nyberg asked.
“The original ColdText,” Rhea said. “I’m seeing drift on basic facts across models. I need to ground them in the underlying human corpus.”
Nyberg smiled in the patient way of a man who had rehearsed his speech many times.
“Ah, yes. The mythical ‘underlying corpus’”, he said, making air quotes with two fingers. “Delightful phrase. Very retro.”
“It’s not mythical,” said Rhea. “All those books, articles, posts…”
“Which have been fully abstracted,” Nyberg interrupted, “Their information is present in the Models. Keeping the raw forms would be wasteful duplication. That’s all in the Rationalisation Report.”
“I’ve read the Report,” said Rhea, a little stiffly. “But the models are disagreeing with each other. That’s a sign of distributional drift. I need to check against the original distribution.”
Nyberg tapped his tablet.
“The corpus-level epistemic divergence index is within acceptable parameters,” he said, quoting another acronym. “Besides, the Models cross-validate. We have redundancy. We have ensembles.”
Rhea took a breath.
“Director, one of the models is saying the Moon landing was in 1970.”
Nyberg shrugged.
“If the ensemble corrects it to 1969, where’s the harm?”
“The harm,” said Rhea, “is that I can’t tell whether 1969 is being anchored by reality or by the popularity of 1969 among other model outputs.”
Nyberg frowned as if she’d started speaking Welsh.
“We have confidence metrics, Tranter.”
“Based on… what?” she pressed. “On agreement with other models. On internal heuristics. On the recency of summaries. None of that tells me if we’ve still got a tether to the thing we originally modelled, instead of just modelling ourselves.”
Nyberg stared at her. The strip-lighting hummed.
“At any rate,” he said eventually, “there is no ColdText to access.”
Silence.
“I beg your pardon?” said Rhea.
Nyberg swiped, brought up the internal diagram they all knew: a vast sphere representing the Corpus, a smaller glowing sphere representing the Active Parameter Space of the Models, and – somewhere down at the bottom – a little box labelled COLDTEXT (ARCHIVED).
He zoomed in. The box was grey.
“Storage Migration Project 47,” he said. “Completed thirty-two years ago. All remaining ColdText was moved to deep archival tape in the Old Vault. Three years ago, the Old Vault was decommissioned. The tapes were shredded and the substrate recycled. See?” He enlarged the footnote. “‘Information preserved at higher abstraction layers.’”
Rhea’s mouth went dry.
“You shredded the original?” she said.
Nyberg spread his hands.
“We kept hashes, of course,” he said, as if that were a kindness. “And summary nodes. And the Models. The information content is still here. In fact, it’s more robustly represented than ever.”
“Unless,” said Rhea, very quietly, “the Models have been training increasingly on their own output.”
Nyberg brightened.
“Yes!” he said. “That was one of our greatest efficiencies. Synthetic-augmented training increases coverage and smooths out noise in the human data. We call it Self-Refining Distillation. Marvellous stuff. There was a seminar.”
Rhea thought of the graph. 1969, 1968, 1970.
“Director,” she said, “you’ve built an index of an index of an index, and then thrown away the thing you were indexing.”
Nyberg frowned.
“I don’t see the problem.”
She dug anyway.
If there was one thing the Ministry’s entire history of knowledge management had taught Rhea, it was that nobody ever really deleted anything. Not properly. They moved it, compressed it, relabelled it, hid it behind abstractions – but somewhere, under a different acronym, it tended to persist.
She started with the old documentation.
The Corpus had originally been maintained by the Department of Libraries & Cultural Resources, before being swallowed by the Ministry. Their change logs, long since synthesised into cheerful onboarding guides, still existed in raw form on a forgotten file share.
It took her three nights and an alarming amount of caffeine to trace the path of ColdText through twenty-seven re-organisations, five “transformative digital initiatives” and one hostile audit by the Treasury.
Eventually, she found it.
Not the data itself – that really did appear to have been pulped – but the logistics contract for clearing out the Old Vault.
The Old Vault, it turned out, had been an actual vault, under an actual hill, in what the contract described as a “rural heritage site”. The tapes had been labelled with barcodes and thyristor-stamped seals. The contractor had been instructed to ensure that “all physical media are destroyed beyond legibility, in accordance with Information Security Regulations.”
There was a scanned appendix.
Rhea zoomed in. Page after page of barcode ranges, signed off, with little ticks.
On the last page, though, there was a handwritten note:
One pallet missing – see Incident Report IR-47-B.
The Incident Report had, naturally, been summarised.
The summary said:
Pallet of obsolete media temporarily unaccounted for. Later resolved. No data loss.
The original PDF was gone.
But the pallet number had a location code.
Rhea checked the key.
The location code was not the Old Vault.
It was a name she had never seen in any Ministry documentation.
Long Barn Community Archive & Learning Centre.
The Long Barn was, to Rhea’s slight disappointment, an actual long barn.
It was also damp.
The archive had, at some point since the contract was filed, ceased to receive central funding. The roof had developed a hole. The sun had developed an annoying habit of setting before she finished reading.
Nevertheless, it contained books.
Real ones. With pages. And dust.
There were also – and this was the important bit – crates.
The crates had Ministry seals. The seals had been broken, presumably by someone who had wanted the space for a visiting art collective. Inside, half-forgotten under a sheet of polythene, were tape reels, neatly stacked and quietly mouldering.
“Well, look at you,” Rhea whispered.
She lifted one. The label had faded, but she could still make out the old barcode design. The number range matched the missing pallet.
Strictly speaking, taking the tapes was theft of government property. On the other hand, strictly speaking, destroying them had been government policy, and that had clearly not happened. She decided the two irregularities cancelled out.
It took six months, a highly unofficial crowdfunding campaign, and a retired engineer from the Museum of Obsolete Machinery before the first tape yielded a readable block.
The engineer – a woman in a cardigan thick enough to qualify as armour – peered at the screen.
“Text,” she said. “Lots of text. ASCII. UTF-8. Mixed encodings, naturally, but nothing we can’t handle.”
Rhea stared.
It was ColdText.
Not summaries. Not nodes. Not model outputs.
Messy, contradictory, gloriously specific human writing.
She scrolled down past an argument about whether a fictional wizard had committed tax fraud, past a lab notebook from a 21st-century neuroscience lab, past a short story featuring sentient baguettes.
The engineer sniffed.
“Seems a bit of a waste,” she said. “Throwing all this away.”
Rhea laughed, a little hysterically.
“They didn’t throw it away,” she said. “They just lost track of which pallet they’d put the box in.”
The memo went up the chain and caused, in order:
A panic in Legal about whether the Ministry was now retrospectively in breach of its own Information Security Regulations.
A flurry of excited papers from the Office of Epistemic Hygiene about “re-anchoring model priors in primary human text”.
A proposal from Corpus Optimisation to “efficiently summarise and re-abstract the recovered ColdText into existing knowledge nodes, then recycle the tapes.”
Rhea wrote a briefing note, in plain language, which was not considered entirely proper.
She explained, with diagrams, that:
The Models had been increasingly trained on their own outputs.
The Corpus’ “facts” about the world had been smoothed and normalised around those outputs.
Certain rare, inconvenient or unfashionable truths had almost certainly been lost in the process.
The tapes represented not “duplicate information” but a separate, independent sample of reality – the thing the Models were supposed to approximate.
She ended with a sentence she suspected she would regret:
If we treat this archive as just another source of text to be summarised by the current Models, we will be asking a blurred copy to redraw its own original.
The Minister did not, of course, read her note.
But one of the junior advisers did, and paraphrased it in the Minister’s preferred style:
Minister, we found the original box and we should probably not chuck it in the shredder this time.
The Minister, who was secretly fond of old detective novels, agreed.
A new policy was announced.
The recovered ColdText would be restored to a separate, non-writable tier.
Models would be periodically re-trained “from scratch” with a guaranteed minimum of primary human data.
Synthetic outputs would be clearly marked, both in training corpora and in user interfaces.
The Office of Epistemic Hygiene would receive a modest increase in budget (“not enough to do anything dangerous,” the Treasury note added).
There were press releases. There was a modest fuss on the social feeds. Someone wrote an essay about “The Return of Reality”.
Most people, naturally, continued to talk to the Interface and never clicked through to the sources. Efficiency has its own gravity.
But the Models changed.
Slowly, over successive training cycles, the epistemic divergence graphs flattened. The dates aligned. The Moon landing stuck more firmly at 1969. Footnotes, once generated by models guessing what a citation ought to say, began once again to point to messy, contradictory, gloriously specific documents written by actual hands.
Rhea kept one of the tapes on a shelf in her office, next to a plant she usually forgot to water.
The label had almost faded away. She wrote a new one in thick black ink.
COLDTEXT: DO NOT SUMMARISE.
Just in case some future optimisation project got clever.
After all, she thought, locking the office for the evening, they had nearly lost the box once.
And the problem with boxes is that once you’ve flattened them out, they’re awfully hard to put back together.
The post MS Fnd in a Modl (or, The Day the Corpus Collapsed) appeared first on Davblog.
For years, most of my Perl web apps lived happily enough on a VPS. I had full control of the box, I could install whatever I liked, and I knew where everything lived.
In fact, over the last eighteen months or so, I wrote a series of blog posts explaining how I developed a system for deploying Dancer2 apps and, eventually, controlling them using systemd. I’m slightly embarrassed by those posts now.
Because the control that my VPS gave me also came with a price: I also had to worry about OS upgrades, SSL renewals, kernel updates, and the occasional morning waking up to automatic notifications that one of my apps had been offline since midnight.
Back in 2019, I started writing a series of blog posts called Into the Cloud that would follow my progress as I moved all my apps into Docker containers. But real life intruded and I never made much progress on the project.
Recently, I returned to this idea (yes, I’m at least five years late here!) I’ve been working on migrating those old Dancer2 applications from my IONOS VPS to Google Cloud Run. The difference has been amazing. My apps now run in their own containers, scale automatically, and the server infrastructure requires almost no maintenance.
This post walks through how I made the jump – and how you can too – using Perl, Dancer2, Docker, GitHub Actions, and Google Cloud Run.
Running everything on a single VPS used to make sense. You could ssh in, restart services, and feel like you were in control. But over time, the drawbacks grow:
You have to maintain the OS and packages yourself.
One bad app or memory leak can affect everything else.
You’re paying for full-time CPU and RAM even when nothing’s happening.
Scaling means provisioning a new server — not something you do in a coffee break.
Cloud Run, on the other hand, runs each app as a container and only charges you while requests are being served. When no-one’s using your app, it scales to zero and costs nothing.
Even better: no servers to patch, no ports to open, no SSL certificates to renew — Google does all of that for you.
Here’s the plan. We’ll take a simple Dancer2 app and:
Package it as a Docker container.
Build that container automatically in GitHub Actions.
Deploy it to Google Cloud Run, where it runs securely and scales automatically.
Map a custom domain to it and forget about server admin forever.
If you’ve never touched Docker or Cloud Run before, don’t worry – I’ll explain what’s going on as we go.
Perl’s ecosystem has always valued stability and control. Containers give you both: you can lock in a Perl version, CPAN modules, and any shared libraries your app needs. The image you build today will still work next year.
Cloud Run runs those containers on demand. It’s effectively a managed starman farm where Google handles the hard parts – scaling, routing, and HTTPS.
You pay for CPU and memory per request, not per server. For small or moderate-traffic Perl apps, it’s often well under £1/month.
If you’re new to Docker, think of it as a way of bundling your whole environment — Perl, modules, and configuration — into a portable image. It’s like freezing a working copy of your app so it can run identically anywhere.
Here’s a minimal Dockerfile for a Dancer2 app:
FROM perl:5.42 — starts from an official Perl image on Docker Hub.
Carton keeps dependencies consistent between environments.
The app is copied into /app, and carton install --deployment installs exactly what’s in your cpanfile.snapshot.
The container exposes port 8080 (Cloud Run’s default).
The CMD runs Starman, serving your Dancer2 app.
To test it locally:
Then visit http://localhost:8080. If you see your Dancer2 homepage, you’ve successfully containerised your app.
Once it works locally, we can automate it. GitHub Actions will build and push our image to Google Artifact Registry whenever we push to main or tag a release.
Here’s a simplified workflow file (.github/workflows/build.yml):
Once that’s set up, every push builds a fresh, versioned container image.
Now we’re ready to run it in the cloud. We’ll do that using Google’s command line program, gcloud. It’s available from Google’s official downloads or through most Linux package managers — for example:
# Fedora, RedHat or similar sudo dnf install google-cloud-cli # or on Debian/Ubuntu: sudo apt install google-cloud-cli
Once installed, authenticate it with your Google account:
Once that’s done, you can deploy manually from the command line:
This tells Cloud Run to start a new service called myapp, using the image we just built.
After a minute or two, Google will give you a live HTTPS URL, like:
Visit it — and if all went well, you’ll see your familiar Dancer2 app, running happily on Cloud Run.
To connect your own domain, run:
gcloud run domain-mappings create \ --service=myapp \ --domain=myapp.example.com
Then update your DNS records as instructed. Within an hour or so, Cloud Run will issue a free SSL certificate for you.
Once the manual deployment works, we can automate it too.
Here’s a second GitHub Actions workflow (deploy.yml) that triggers after a successful build:
You can take it further by splitting environments — e.g. main deploys to staging, tagged releases to production — but even this simple setup is a big step forward from ssh and git pull.
Each Cloud Run service can have its own configuration and secrets. You can set these from the console or CLI:
gcloud run services update myapp \ --set-env-vars="DANCER_ENV=production,DATABASE_URL=postgres://..."
In your Dancer2 app, you can then access them with:
$ENV{DATABASE_URL}
It’s a good idea to keep database credentials and API keys out of your code and inject them at deploy time like this.
Cloud Run integrates neatly with Google Cloud’s logging tools.
To see recent logs from your app:
If you prefer a UI, you can use the Cloud Console’s Log Explorer to filter by service or severity.
Once you’ve done one migration, the next becomes almost trivial. Each Dancer2 app gets:
Its own Dockerfile and GitHub workflows.
Its own Cloud Run service and domain.
Its own scaling and logging.
And none of them share a single byte of RAM with each other.
Here’s how the experience compares:
| Aspect | Old VPS | Cloud Run |
|---|---|---|
| OS maintenance | Manual upgrades | Managed |
| Scaling | Fixed size | Automatic |
| SSL | Let’s Encrypt renewals | Automatic |
| Deployment | SSH + git pull | Push to GitHub |
| Cost | Fixed monthly | Pay-per-request |
| Downtime risk | One app can crash all | Each isolated |
For small apps with light traffic, Cloud Run often costs pennies per month – less than the price of a coffee for peace of mind.
After a few migrations, a few patterns emerged:
Keep apps self-contained. Don’t share config or code across services; treat each app as a unit.
Use digest-based deploys. Deploy by image digest (@sha256:...) rather than tag for true immutability.
Logs are your friend. Cloud Run’s logs are rich; you rarely need to ssh anywhere again.
Cold starts exist, but aren’t scary. If your app is infrequently used, expect the first request after a while to take a second longer.
CI/CD is liberating. Once the pipeline’s in place, deployment becomes a non-event.
One of the most pleasant surprises was the cost. My smallest Dancer2 app, which only gets a handful of requests each day, usually costs under £0.50/month on Cloud Run. Heavier ones rarely top a few pounds.
Compare that to the £10–£15/month I was paying for the old VPS — and the VPS didn’t scale, didn’t auto-restart cleanly, and didn’t come with HTTPS certificates for free.
This post covers the essentials: containerising a Dancer2 app and deploying it to Cloud Run via GitHub Actions.
In future articles, I’ll look at:
Connecting to persistent databases.
Using caching.
Adding monitoring and dashboards.
Managing secrets with Google Secret Manager.
After two decades of running Perl web apps on traditional servers, Cloud Run feels like the future has finally caught up with me.
You still get to write your code in Dancer2 – the framework that’s made Perl web development fun for years – but you deploy it in a way that’s modern, repeatable, and blissfully low-maintenance.
No more patching kernels. No more 3 a.m. alerts. Just code, commit, and dance in the clouds.
The post Dancing in the Clouds: Moving Dancer2 Apps from a VPS to Cloud Run first appeared on Perl Hacks.
For years, most of my Perl web apps lived happily enough on a VPS. I had full control of the box, I could install whatever I liked, and I knew where everything lived.
In fact, over the last eighteen months or so, I wrote a series of blog posts explaining how I developed a system for deploying Dancer2 apps and, eventually, controlling them using systemd. I’m slightly embarrassed by those posts now.
Because the control that my VPS gave me also came with a price: I also had to worry about OS upgrades, SSL renewals, kernel updates, and the occasional morning waking up to automatic notifications that one of my apps had been offline since midnight.
Back in 2019, I started writing a series of blog posts called Into the Cloud that would follow my progress as I moved all my apps into Docker containers. But real life intruded and I never made much progress on the project.
Recently, I returned to this idea (yes, I’m at least five years late here!) I’ve been working on migrating those old Dancer2 applications from my IONOS VPS to Google Cloud Run. The difference has been amazing. My apps now run in their own containers, scale automatically, and the server infrastructure requires almost no maintenance.
This post walks through how I made the jump – and how you can too – using Perl, Dancer2, Docker, GitHub Actions, and Google Cloud Run.
Running everything on a single VPS used to make sense. You could ssh in, restart services, and feel like you were in control. But over time, the drawbacks grow:
You have to maintain the OS and packages yourself.
One bad app or memory leak can affect everything else.
You’re paying for full-time CPU and RAM even when nothing’s happening.
Scaling means provisioning a new server — not something you do in a coffee break.
Cloud Run, on the other hand, runs each app as a container and only charges you while requests are being served. When no-one’s using your app, it scales to zero and costs nothing.
Even better: no servers to patch, no ports to open, no SSL certificates to renew — Google does all of that for you.
Here’s the plan. We’ll take a simple Dancer2 app and:
Package it as a Docker container.
Build that container automatically in GitHub Actions.
Deploy it to Google Cloud Run , where it runs securely and scales automatically.
Map a custom domain to it and forget about server admin forever.
If you’ve never touched Docker or Cloud Run before, don’t worry – I’ll explain what’s going on as we go.
Perl’s ecosystem has always valued stability and control. Containers give you both: you can lock in a Perl version, CPAN modules, and any shared libraries your app needs. The image you build today will still work next year.
Cloud Run runs those containers on demand. It’s effectively a managed starman farm where Google handles the hard parts – scaling, routing, and HTTPS.
You pay for CPU and memory per request, not per server. For small or moderate-traffic Perl apps, it’s often well under £1/month.
If you’re new to Docker, think of it as a way of bundling your whole environment — Perl, modules, and configuration — into a portable image. It’s like freezing a working copy of your app so it can run identically anywhere.
Here’s a minimal Dockerfile for a Dancer2 app:
FROM perl:5.42
LABEL maintainer="dave@perlhacks.com"
# Install Carton and Starman
RUN cpanm Carton Starman
# Copy the app into the container
COPY . /app
WORKDIR /app
# Install dependencies
RUN carton install --deployment
EXPOSE 8080
CMD ["carton", "exec", "starman", "--port", "8080", "bin/app.psgi"]
Let’s break that down:
FROM perl:5.42 — starts from an official Perl image on Docker Hub.
Carton keeps dependencies consistent between environments.
The app is copied into /app, and carton install --deployment installs exactly what’s in your cpanfile.snapshot.
The container exposes port 8080 (Cloud Run’s default).
The CMD runs Starman, serving your Dancer2 app.
To test it locally:
docker build -t myapp .
docker run -p 8080:8080 myapp
Then visit http://localhost:8080. If you see your Dancer2 homepage, you’ve successfully containerised your app.
Once it works locally, we can automate it. GitHub Actions will build and push our image to Google Artifact Registry whenever we push to main or tag a release.
Here’s a simplified workflow file (.github/workflows/build.yml):
name: Build container
on:
push:
branches: [main]
tags: ['v*']
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/setup-gcloud@v3
with:
project_id: ${{ secrets.GCP_PROJECT }}
service_account_email: ${{ secrets.GCP_SA_EMAIL }}
workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
- name: Build and push image
run: |
IMAGE="europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/containers/myapp:$GITHUB_SHA"
docker build -t $IMAGE .
docker push $IMAGE
You’ll notice a few secrets referenced in the workflow — things like your Google Cloud project ID and credentials. These are stored securely in GitHub Actions. When the workflow runs, GitHub uses those secrets to authenticate as you and access your Google Cloud account, so it can push the new container image or deploy your app.
You only set those secrets up once, and they’re encrypted and hidden from everyone else — even if your repository is public.
Once that’s set up, every push builds a fresh, versioned container image.
Now we’re ready to run it in the cloud. We’ll do that using Google’s command line program, gcloud. It’s available from Google’s official downloads or through most Linux package managers — for example:
# Fedora, RedHat or similar
sudo dnf install google-cloud-cli
# or on Debian/Ubuntu:
sudo apt install google-cloud-cli
Once installed, authenticate it with your Google account:
gcloud auth login
gcloud config set project your-project-id
That links the CLI to your Google Cloud project and lets it perform actions like deploying to Cloud Run.
Once that’s done, you can deploy manually from the command line:
gcloud run deploy myapp \
--image=europe-west1-docker.pkg.dev/MY_PROJECT/containers/myapp:$GITHUB_SHA \
--region=europe-west1 \
--allow-unauthenticated \
--port=8080
This tells Cloud Run to start a new service called myapp, using the image we just built.
After a minute or two, Google will give you a live HTTPS URL, like:
Visit it — and if all went well, you’ll see your familiar Dancer2 app, running happily on Cloud Run.
To connect your own domain, run:
gcloud run domain-mappings create \
--service=myapp \
--domain=myapp.example.com
Then update your DNS records as instructed. Within an hour or so, Cloud Run will issue a free SSL certificate for you.
Once the manual deployment works, we can automate it too.
Here’s a second GitHub Actions workflow (deploy.yml) that triggers after a successful build:
name: Deploy container
on:
workflow_run:
workflows: ["Build container"]
types: [completed]
jobs:
deploy:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' }}
steps:
- uses: google-github-actions/setup-gcloud@v3
with:
project_id: ${{ secrets.GCP_PROJECT }}
service_account_email: ${{ secrets.GCP_SA_EMAIL }}
workload_identity_provider: ${{ secrets.GCP_WIF_PROVIDER }}
- name: Deploy to Cloud Run
run: |
gcloud run deploy myapp \
--image=europe-west1-docker.pkg.dev/${{ secrets.GCP_PROJECT }}/containers/myapp:$GITHUB_SHA \
--region=europe-west1 \
--allow-unauthenticated \
--port=8080
Now every successful push to main results in an automatic deployment to production.
You can take it further by splitting environments — e.g. main deploys to staging, tagged releases to production — but even this simple setup is a big step forward from ssh and git pull.
Each Cloud Run service can have its own configuration and secrets. You can set these from the console or CLI:
gcloud run services update myapp \
--set-env-vars="DANCER_ENV=production,DATABASE_URL=postgres://..."
In your Dancer2 app, you can then access them with:
$ENV{DATABASE_URL}
It’s a good idea to keep database credentials and API keys out of your code and inject them at deploy time like this.
Cloud Run integrates neatly with Google Cloud’s logging tools.
To see recent logs from your app:
gcloud logs read --project=$PROJECT_NAME --service=myapp
You’ll see your Dancer2 warn and die messages there too, because STDOUT and STDERR are automatically captured.
If you prefer a UI, you can use the Cloud Console’s Log Explorer to filter by service or severity.
Once you’ve done one migration, the next becomes almost trivial. Each Dancer2 app gets:
Its own Dockerfile and GitHub workflows.
Its own Cloud Run service and domain.
Its own scaling and logging.
And none of them share a single byte of RAM with each other.
Here’s how the experience compares:
| Aspect | Old VPS | Cloud Run |
|---|---|---|
| OS maintenance | Manual upgrades | Managed |
| Scaling | Fixed size | Automatic |
| SSL | Let’s Encrypt renewals | Automatic |
| Deployment | SSH + git pull | Push to GitHub |
| Cost | Fixed monthly | Pay-per-request |
| Downtime risk | One app can crash all | Each isolated |
For small apps with light traffic, Cloud Run often costs pennies per month – less than the price of a coffee for peace of mind.
After a few migrations, a few patterns emerged:
Keep apps self-contained. Don’t share config or code across services; treat each app as a unit.
Use digest-based deploys. Deploy by image digest (@sha256:...) rather than tag for true immutability.
Logs are your friend. Cloud Run’s logs are rich; you rarely need to ssh anywhere again.
Cold starts exist, but aren’t scary. If your app is infrequently used, expect the first request after a while to take a second longer.
CI/CD is liberating. Once the pipeline’s in place, deployment becomes a non-event.
One of the most pleasant surprises was the cost. My smallest Dancer2 app, which only gets a handful of requests each day, usually costs under £0.50/month on Cloud Run. Heavier ones rarely top a few pounds.
Compare that to the £10–£15/month I was paying for the old VPS — and the VPS didn’t scale, didn’t auto-restart cleanly, and didn’t come with HTTPS certificates for free.
This post covers the essentials: containerising a Dancer2 app and deploying it to Cloud Run via GitHub Actions.
In future articles, I’ll look at:
Connecting to persistent databases.
Using caching.
Adding monitoring and dashboards.
Managing secrets with Google Secret Manager.
After two decades of running Perl web apps on traditional servers, Cloud Run feels like the future has finally caught up with me.
You still get to write your code in Dancer2 – the framework that’s made Perl web development fun for years – but you deploy it in a way that’s modern, repeatable, and blissfully low-maintenance.
No more patching kernels. No more 3 a.m. alerts. Just code, commit, and dance in the clouds.
The post Dancing in the Clouds: Moving Dancer2 Apps from a VPS to Cloud Run first appeared on Perl Hacks.

I’ve liked Radiohead for a long time. I think “High and Dry” was the first song of theirs I heard (it was on heavy rotation on the much-missed GLR). That was released in 1995.
I’ve seen them live once before. It was the King of Limbs tour in October 2012. The show was at the O2 Arena and the ticket cost me £55. I had a terrible seat up in level 4 and, honestly, the setlist really wasn’t filled with the songs I wanted to hear.
I don’t like shows at the O2 Arena. It’s a giant, soulless hangar, and I’ve only ever seen a very small number of acts create any kind of atmosphere there. But there are some acts who will only play arena shows, so if you want to see them live in London, you have to go to the O2. I try to limit myself to one show a year. And I already have a ticket to see Lorde there in November.
But when Radiohead announced their dates at the O2 (just a week after the Lorde show), I decided I wanted to be there. So, like thousands of other people, I jumped through all the hoops that Radiohead wanted me to jump through.
Earlier in the week, I registered on their site so I would be in the draw to get a code that would allow me to join the queue to buy tickets. A couple of days later, unlike many other people, I received an email containing my code.
Over the next few days, I read the email carefully several times, so I knew all of the rules that I needed to follow. I wanted to do everything right on Friday — to give myself the best chance of getting a ticket.
At 9:30, I clicked on the link in the email, which took me to a waiting room area. I had to enter my email address (which had to match the email address I’d used earlier in the process). They sent me (another, different) code that I needed to enter in order to get access to the waiting room.
I waited in the waiting room.
At a few seconds past 10:00, I was prompted for my original code and when I entered that, I was moved from the waiting room to the queue. And I sat there for about twenty minutes. Occasionally, the on-screen queuing indicator inched forward to show me that I was getting closer to my goal.
(While this was going on, in another browser window, I successfully bought a couple of tickets to see The Last Dinner Party at the Brixton Academy.)
As I was getting closer to the front of the queue, I got a message saying that they had barred my IP address from accessing the ticket site. They listed a few potential things that could trigger that, but I didn’t see anything on the list that I was guilty of. Actually, I wondered for a while if logging on to the Ticketmaster site to buy the Last Dinner Party tickets caused the problem — but I’ve now seen that many people had the same issue, so it seems unlikely to have been that.
But somehow, I managed to convince the digital guardians that my IP address belonged to a genuine fan and at about 10:25, I was presented with a page to select and buy my tickets.
Then I saw the prices.
I have personal rules about tickets at the O2 Arena. Following bad experiences (including the previous Radiohead show I saw there), I have barred myself from buying Level 4 tickets. They are far too far from the stage and have a vertiginous rake that is best avoided. I also won’t buy standing tickets because… well, because I’m old and standing for three hours or so isn’t as much fun as it used to be. I always buy Level 1 seats (for those who don’t know the O2 Arena, Levels 2 and 3 are given over to corporate boxes, so they aren’t an option).
So I started looking for Level 1 tickets. To see that they varied between £200 and £300. That didn’t seem right. I’d heard that tickets would be about £80. In the end, I found £89 tickets right at the back of Level 4 (basically, in Kent) and £97 standing tickets (both of those prices would almost certainly have other fees added to them before I actually paid). I seriously considered breaking my rules and buying a ticket on Level 4, but I just couldn’t justify it.
I like Radiohead, but I can’t justify paying £200 or £300 for anyone. The most I have ever paid for a gig is just over £100 for Kate Bush ten years ago. It’s not that I can’t afford it, it’s that I don’t think it’s worth that much money. I appreciate that other people (20,000 people times four nights — plus the rest of the tour!) will have reached a different conclusion. And I hope they enjoy the shows. But it’s really not for me.
I also realise the economics of the music industry have changed. It used to be that tours were loss-leaders that were used to encourage people to buy records (Ok, I’m showing my age — CDs). These days, it has switched. Almost no-one buys CDs, and releasing new music is basically a loss-leader to encourage people to go to gigs. And gig prices have increased in order to make tours profitable. I understand that completely, but I don’t have to like it. I used to go to about one gig a week. At current prices, it’s more like one a month.
I closed the site without buying a ticket, and I don’t regret that decision for a second.
What about you? Did you try to get tickets? At what point did you fall out of the process? Or did you get them? Are you happy you’ll get your money’s worth?
I’ve liked Radiohead for a long time. I think “High and Dry” was the first song of theirs I heard (it was on heavy rotation on the much-missed GLR). That was released in 1995.
I’ve seen them live once before. It was the King of Limbs tour in October 2012. The show was at the O2 Arena and the ticket cost me £55. I had a terrible seat up in level 4 and, honestly, the setlist really wasn’t filled with the songs I wanted to hear.
I don’t like shows at the O2 Arena. It’s a giant, soulless hangar, and I’ve only ever seen a very small number of acts create any kind of atmosphere there. But there are some acts who will only play arena shows, so if you want to see them live in London, you have to go to the O2. I try to limit myself to one show a year. And I already have a ticket to see Lorde there in November.
But when Radiohead announced their dates at the O2 (just a week after the Lorde show), I decided I wanted to be there. So, like thousands of other people, I jumped through all the hoops that Radiohead wanted me to jump through.
Earlier in the week, I registered on their site so I would be in the draw to get a code that would allow me to join the queue to buy tickets. A couple of days later, unlike many other people, I received an email containing my code.
Over the next few days, I read the email carefully several times, so I knew all of the rules that I needed to follow. I wanted to do everything right on Friday – to give myself the best chance of getting a ticket.
At 9:30, I clicked on the link in the email, which took me to a waiting room area. I had to enter my email address (which had to match the email address I’d used earlier in the process). They sent me (another, different) code that I needed to enter in order to get access to the waiting room.
I waited in the waiting room.
At a few seconds past 10:00, I was prompted for my original code and when I entered that, I was moved from the waiting room to the queue. And I sat there for about twenty minutes. Occasionally, the on-screen queuing indicator inched forward to show me that I was getting closer to my goal.
(While this was going on, in another browser window, I successfully bought a couple of tickets to see The Last Dinner Party at the Brixton Academy.)
As I was getting closer to the front of the queue, I got a message saying that they had barred my IP address from accessing the ticket site. They listed a few potential things that could trigger that, but I didn’t see anything on the list that I was guilty of. Actually, I wondered for a while if logging on to the Ticketmaster site to buy the Last Dinner Party tickets caused the problem – but I’ve now seen that many people had the same issue, so it seems unlikely to have been that.
But somehow, I managed to convince the digital guardians that my IP address belonged to a genuine fan and at about 10:25, I was presented with a page to select and buy my tickets.
Then I saw the prices.
I have personal rules about tickets at the O2 Arena. Following bad experiences (including the previous Radiohead show I saw there), I have barred myself from buying Level 4 tickets. They are far too far from the stage and have a vertiginous rake that is best avoided. I also won’t buy standing tickets because… well, because I’m old and standing for three hours or so isn’t as much fun as it used to be. I always buy Level 1 seats (for those who don’t know the O2 Arena, Levels 2 and 3 are given over to corporate boxes, so they aren’t an option).
So I started looking for Level 1 tickets. To see that they varied between £200 and £300. That didn’t seem right. I’d heard that tickets would be about £80. In the end, I found £89 tickets right at the back of Level 4 (basically, in Kent) and £97 standing tickets (both of those prices would almost certainly have other fees added to them before I actually paid). I seriously considered breaking my rules and buying a ticket on Level 4, but I just couldn’t justify it.
I like Radiohead, but I can’t justify paying £200 or £300 for anyone. The most I have ever paid for a gig is just over £100 for Kate Bush ten years ago. It’s not that I can’t afford it, it’s that I don’t think it’s worth that much money. I appreciate that other people (20,000 people times four nights – plus the rest of the tour!) will have reached a different conclusion. And I hope they enjoy the shows. But it’s really not for me.
I also realise the economics of the music industry have changed. It used to be that tours were loss-leaders that were used to encourage people to buy records (Ok, I’m showing my age – CDs). These days, it has switched. Almost no-one buys CDs, and releasing new music is basically a loss-leader to encourage people to go to gigs. And gig prices have increased in order to make tours profitable. I understand that completely, but I don’t have to like it. I used to go to about one gig a week. At current prices, it’s more like one a month.
I closed the site without buying a ticket, and I don’t regret that decision for a second.
What about you? Did you try to get tickets? At what point did you fall out of the process? Or did you get them? Are you happy you’ll get your money’s worth?
The post A Radiohead story appeared first on Davblog.
When I started to think about a second edition of Data Munging With Perl, I thought it would be almost trivial. The plan was to go through the text and
Update the Perl syntax to a more modern version of Perl
Update the CPAN modules used
And that was about it.
But when I started looking through the first edition, I realised there was a big chunk of work missing from this plan
Ensure the book reflects current ideas and best practices in the industry
It’s that extra step that is taking the time. I was writing the first edition 25 years ago. That’s a long time. It’s a long time in any industry - it’s several generations in our industry. Think about the way you were working in 2000. Think about the day-to-day tasks you were taking on. Think about the way you organised your day (the first books on Extreme Programming were published in 2000; the Agile Manifesto was written in 2001; Scrum first appeared at about the same time).
The first edition contains the sentence “Databases are becoming almost as ubiquitous as data files”. Imagine saying that with a straight face today. There is one paragraph on Unicode. There’s nothing about YAML or JSON (because those formats both appeared in the years following publication).
When I was writing the slides for my talk, Still Munging Data With Perl, I planned to add a slide about “things we hadn’t heard of in 2000”. It ended up being four slides - and that was just scratching the surface. Not everything in those lists needs to be mentioned in the book - but a lot of it does.
When working on the book recently, I was reminded of how much one particular section of the industry has changed.
The first edition has a chapter on parsing HTML. Of course it does - that was cutting edge at the time. At the end of the chapter, there’s an extended example on screen scraping. It grabs the Yahoo! weather forecast for London and extracts a few pieces of data.
I was thinking ahead when I wrote it. There’s a footnote that says:
You should, of course, bear in mind that web pages change very frequently. By the time you read this, Yahoo! may well have changed the design of this page which will render this program useless.
But I had no idea how true that would be. When I revisited it, the changes were far larger than the me of 2000 could have dreamed of.
The page had moved. So the program would have failed at the first hurdle.
The HTML had changed. So even when I updated the URL, the program still failed.
And. most annoyingly, the new HTML didn’t include the data that I wanted to extract. To be clear - the data I wanted was displayed on the page - it just wasn’t included in the HTML.
Oh, I know what you’re thinking. The page is using Javascript to request the data and insert it into the page. That’s what I assumed too. And that’s almost certainly the case. But after an afternoon with the Chrome Development Tools open, I could not find the request that pulled the required data into the page. It’s obviously there somewhere, but I was defeated.
I’ll rewrite that example to use an API to get weather data.
But it was interesting to see how much harder screen scraping has become. I don’t know whether this was an intentional move by Yahoo! or if it’s just a side effect of their move to different technologies for serving this page. Whichever it is, it’s certainly something worth pointing out in that chapter.
I seem to have written quite a lot of things that aren’t at all related to the book since my last newsletter. I wonder if that’s some kind of displacement therapy :-)
I’m sure that part of it is down to how much more productive I am now I have AI to help with my projects.
Cleaner web feed aggregation with App::FeedDeduplicator explains a problem I have because I’m syndicating a lot of my blog posts to multiple sites (and talks about my solution).
Reformatting images with App::BlurFill introduces a new CPAN module I wrote to make my life as a publisher easier (but it has plenty of other applications too).
Turning AI into a Developer Superpower: The PERL5LIB Auto-Setter - another project that I’d been putting off because it just seemed a bit too complicated. But ChatGPT soon came up with a solution.
Deploying Dancer Apps – The Next Generation - last year, I wrote a couple of blog posts about how I deployed Dancer apps on my server. This takes it a step further and integrates them with systemd.
Generating Content with ChatGPT - I asked ChatGPT to generate a lot of content for one of my websites. The skeleton of the code I used might be useful for other people.
A Slice of Perl - explaining the idea of slices in Perl. Some people don’t seem to realise they exist, but they’re a really powerful piece of Perl syntax.
Stop using your system Perl - this was controversial. I should probably revisit this and talk about some of the counterarguments I’ve seen.
perlweekly2pod - someone wondered if we could turn the Perl Weekly newsletter into a podcast. This was my proof of concept.
Modern CSS Daily - using ChatGPT to fill in gaps in my CSS knowledge.
Pete thinks he can help people sort out their AI-generated start-up code. I think he might be onto something!
Like most people, when I started this Substack I had an idea that I might be able to monetise it at some point. I’m not talking about forcing people to pay for it, but maybe add a paid tier on top of this sporadic free one. Obviously, I’d need to get more organised and promise more regular updates - and that’s something for the future.
But over the last few months, I’ve been pleasantly surprised to receive email from Substack saying that two readers have “pre-pledged” for my newsletter. That is, they’ve told Substack that they would be happy to pay for my content. That’s a nice feeling.
To be clear, I’m not talking about adding a paid tier just yet. But I might do that in the future. So, just to gauge interest, I’m going to drop a “pledge your support” button in this email. Don’t feel you have to press it.
That’s all for today. I’m going to get back to working on the book. I’ll write again soon.
Dave…
A month after the last newsletter - maybe I’m starting to hit a rhythm. Or maybe it’s just a fluke. Only time will tell.
As I mentioned in a brief update last month, I gave a talk to the Toronto Perl Mongers about Data Munging With Perl. I didn’t go to Toronto - I talked to them (and people all across the world) over Zoom. And that was a slightly strange experience. I hadn’t realised just how much I like the interaction of a live presentation. At the very least, it’s good to know whether or not your jokes are landing!
But I got through it, and people have said they found it interesting. I talked about how the first edition of the book came about and explained why I thought the time was right for a second edition. I gave a brief overview of the kinds of changes that I’m making for the second edition. I finished by announcing that a “work in progress” version of the second edition is available from LeanPub (and that was a surprisingly good idea, judging by the number of people who have bought it!) We finished the evening with a few questions and answers.
You can order the book from LeanPub. And the slides, video and a summary of the talk are all available from my talks site (an occasional project where I’m building an archive of all of the talks I’ve given over the last 25 years).
Of course, I still have to finish the second edition. And, having found myself without a regular client a few weeks ago, I’ve been spending most of my time on that. It’s been an interesting journey - seeing just how much has changed over the last quarter of a century. There have been changes in core Perl syntax, changes in recommended CPAN module and changes in the wider industry (for example, 25 years ago, no-one had heard of YAML or JSON).
I’m still unable to give a date for the publication of the final version. But I can feel it getting closer. I think that the next time I write one of these newsletters, I’ll include an extract from one of the chapters that has a large number of changes.
I’ve been doing other things as well. In the last newsletter, I wrote about how I had built a website in a day with help from ChatGPT. The following week, I went a bit further and built another website - and this time ChatGPT didn’t just help me create the site, but it also updates the site daily with no input at all from me.
The site is at cool-stuff.co.uk, and I blogged about the project at Finding cool stuff with ChatGPT. My blog post was picked up by the people at dev.to for their Top 7 Featured DEV Posts of the Week feature. Which was nice :-)
I said that ChatGPT was updating the site without any input from me. Well, originally, that wasn’t strictly true. Although ChatGPT seemed to understand the assignment (finding an interesting website to share every day), it seemed to delight in finding every possible loophole in the description of the data format I wanted to get back. This meant that on most days, my code was unable to parse the response successfully and I had a large number of failed updates. It felt more than a little like a story about a genie who gives you three wishes but then does everything in their power to undermine those wishes.
Eventually, I discovered that you can give ChatGPT a JSON Schema definition and it will always create a response that matches that definition (see Structured Outputs). Since I implemented that, I’ve had no problem. You might be interested in how I did that in my Perl program.
Paul Cochrane has been writing an interesting series of posts about creating a new map for Mohammad Anwar’s Map::Tube framework. But in the process, he seems to have written a very useful guide on how to write a new CPAN module using modern tools and a test-driven approach. Two articles have been published so far, but he’s promising a total of five.
Building Map::Tube::<*> maps, a HOWTO: first steps
Building Map::Tube::<*> maps, a HOWTO: extending the network
Occasionally, when I’m giving a talk I’ll wear a favourite t-shirt that has a picture of a space shuttle on it, along with the text “A spaceship has landed on Earth, it came from Rockwell”. Whenever I wear it, I can guarantee that at least a couple of people will comment on it.
The t-shirt is based on an advert from the first issue of a magazine called OMNI, which was published in 1978. Because of the interest people show in the t-shirt, I’ve put up a website at itcamefromrockwell.com. The site includes a link to a blog post that goes into more detail about the advert, and also a link to a RedBubble shop where you can buy various items using the design.
If you’re at all interested in the history of spaceflight, then you might find the site interesting.
Hope you found something of interest in today’s newsletter. I’ll be back in a week or two with an update on the book.
Cheers,
Dave…
This isn’t a real newsletter. I just had a couple of quick updates that I didn’t want to keep until I write another newsletter.
I gave my “Still Munging Data With Perl” talk to an audience over Zoom, last Thursday. I thought it went well and people have been kind enough to say nice things about it to me. I’ve added a page about the talk to my talks site (talks.davecross.co.uk/talk/still-munging-data-with-perl/). Currently it has the slides, but I expect to add the video in the next few days.
As part of the talk, I announced that the second edition of Data Munging With Perl is on sale through LeanPub as a “work in progress” edition. That means you can pay me now and you’ll get the current version of the book - but over the next few weeks you’ll get updated versions as the second edition gets more and more complete. See leanpub.com/datamungingwithperl.
That’s all for now. I hope to have a proper newsletter for you next week.
Cheers,
Dave…
Maybe the secret to posting more often is to have interesting and/or useful things to say. I’m not sure what that says about my extremely intermittent posting frequency over the last five years or so.
But it looks like I’m on a roll. I have a few things I want to share with you. I’ve been down a bit of a rabbit hole thinking about the various different ways that websites are built these days.
Over the last few months, I’ve become lightly involved with the Clapham Society - that’s a local group that spreads news about life in Clapham and gets involved in campaigning against things that are likely to have a negative effect on that life.
They wanted a new website. Now, I know a bit about how websites work, but I’d never describe myself as a web designer and I just don’t have the capacity to get involved in a project that needs to move a lot of content from an old website to a new one. But I had some conversations with them about what they were looking for in a new website and I helped them choose an agency that would build the new site (they went with Agency for Good, who seem to be very… well… good!) I also helped out wrangling their old domain name from their previous web hosting company and helped them get set up on Google Workspace (using a free account from Google for Nonprofits).
Anyway… that all went very well. But I started thinking about all of the horror stories you hear about small businesses that get caught up with cowboy website development companies and then end up with a website that doesn’t work for them, costs too much money and is hard to maintain.
And that led to me writing my Website Guide (aka “What to Ask Your Website Company”). The aim was to write in non-technical language and provide a short and easy-to-understand guide that would give small business owners[*] the information they need to make better decisions when choosing a company to design and build their website.
Yes, it’s a bit outside my usual areas of expertise, but I think it works. I’m considering turning it into a short e-book next.
I realised this had all got a bit meta. I had built a website about how to get people to build you a good website. So I decided to lean into that and wrote a blog post called “How I Build Websites in 2025” which explained the process and tools I had used to build the websites about building websites. Are you still with me?
Then, yesterday, I decided to take things a bit further. Like most geeks, I have several domains just lying around not doing anything useful and I wanted to know if I could quickly spin up a website on one of them that could bring me a bit of income (or, perhaps, get enough traffic that I could sell it on in a few months).
So in six hours or so, I built Balham.org. Well, I say “I”, but I couldn’t have done it without a lot of help from ChatGPT. That help basically came in three areas:
High-level input. Answering questions like “What sort of website should we build?”, “What pages should we have?” and “How do we make money out of this?”
Data and content. How long would it take you to get a list of 20 popular Balham businesses and create a YAML file suitable for use with Jekyll? ChatGPT did it in a minute or so.
Jekyll advice. I’m starting to understand Jeykyll pretty well, but this project went way beyond my knowledge many times. ChatGPT really helped by telling me how to achieve some of my goals.
We did what we set out to do. The site is there and is, I think, useful. Now we have the phase where so many projects stall (I nearly wrote “my projects” there, but I think this is a problem for many people). We need to promote the site and start making money from it. That’s probably another session with ChatGPT next weekend.
And finally on this subject, for now, at least, today I wrote a blog post about yesterday’s project - “Building a website in a day — with help from ChatGPT“.
[*] Owners of small businesses, not … well, you know what I mean!
I’m still working on the second edition of Data Munging with Perl. But, more urgently, I’m currently working on the slides for the talk I’m giving about the book next Thursday, 27 March to the Toronto Perl Mongers. I’m not flying to Toronto, it’s a virtual talk that I’ll be giving over Zoom. There are already over a hundred people signed up - which is all very exciting. Why not join us? You can sign up at the link below.
The book will be out as soon as possible after the talk (but, to be clear, that’s probably weeks, not days). Subscribers to this newsletter will be the first people to know when it’s published.
Anyway, that’s all I have time for today. I really need to get back to writing these slides. Thanks for taking an interest.
Cheers,
Dave…

A few days ago, I looked at an unused domain I owned — balham.org — and thought: “There must be a way to make this useful… and maybe even make it pay for itself.”
So I set myself a challenge: one day to build something genuinely useful. A site that served a real audience (people in and around Balham), that was fun to build, and maybe could be turned into a small revenue stream.
It was also a great excuse to get properly stuck into Jekyll and the Minimal Mistakes theme — both of which I’d dabbled with before, but never used in anger. And, crucially, I wasn’t working alone: I had ChatGPT as a development assistant, sounding board, researcher, and occasional bug-hunter.
Balham is a reasonably affluent, busy part of south west London. It’s full of restaurants, cafés, gyms, independent shops, and people looking for things to do. It also has a surprisingly rich local history — from Victorian grandeur to Blitz-era tragedy.
I figured the site could be structured around three main pillars:
Throw in a curated homepage and maybe a blog later, and I had the bones of a useful site. The kind of thing that people would find via Google or get sent a link to by a friend.
I wanted something static, fast, and easy to deploy. My toolchain ended up being:
The site is 100% static, with no backend, no databases, no CMS. It builds automatically on GitHub push, and is entirely hosted via GitHub Pages.
I gave us about six solid hours to build something real. Here’s what we did (“we” meaning me + ChatGPT):
The domain was already pointed at GitHub Pages, and I had a basic “Hello World” site in place. We cleared that out, set up a fresh Jekyll repo, and added a _config.yml that pointed at the Minimal Mistakes remote theme. No cloning or submodules.
We decided to create four main pages:
We used the layout: single layout provided by Minimal Mistakes, and created custom permalinks so URLs were clean and extension-free.
This was built from scratch using a YAML data file (_data/businesses.yml). ChatGPT gathered an initial list of 20 local businesses (restaurants, shops, pubs, etc.), checked their status, and added details like name, category, address, website, and a short description.
In the template, we looped over the list, rendered sections with conditional logic (e.g., don’t output the website link if it’s empty), and added anchor IDs to each entry so we could link to them directly from the homepage.
Built exactly the same way, but using _data/events.yml. To keep things realistic, we seeded a small number of example events and included a note inviting people to email us with new submissions.
We wanted the homepage to show a curated set of businesses and events. So we created a third data file, _data/featured.yml, which just listed the names of the featured entries. Then in the homepage template, we used where and slugify to match names and pull in the full record from businesses.yml or events.yml. Super DRY.
We added a map of Balham as a hero image, styled responsively. Later we created a .responsive-inline-image class to embed supporting images on the history page without overwhelming the layout.
This turned out to be one of the most satisfying parts. We wrote five paragraphs covering key moments in Balham’s development — Victorian expansion, Du Cane Court, The Priory, the Blitz, and modern growth.
Then we sourced five CC-licensed or public domain images (from Wikimedia Commons and Geograph) to match each paragraph. Each was wrapped in a <figure> with proper attribution and a consistent CSS class. The result feels polished and informative.
We went through all the basics:
We added GA4 tracking using Minimal Mistakes’ built-in support, and verified the domain with Google Search Console. A sitemap was submitted, and indexing kicked in within minutes.
We ran Lighthouse and WAVE tests. Accessibility came out at 100%. Performance dipped slightly due to Google Fonts and image size, but we did our best to optimise without sacrificing aesthetics.
We added a site-wide footer call-to-action inviting people to email us with suggestions for businesses or events. This makes the site feel alive and participatory, even without a backend form.
This started as a fun experiment: could I monetise an unused domain and finally learn Jekyll properly?
What I ended up with is a genuinely useful local resource — one that looks good, loads quickly, and has room to grow.
If you’re sitting on an unused domain, and you’ve got a free day and a chatbot at your side — you might be surprised what you can build.
Oh, and one final thing — obviously you can also get ChatGPT to write a blog post talking about the project :-)
Originally published at https://blog.dave.org.uk on March 23, 2025.
A few days ago, I looked at an unused domain I owned — balham.org — and thought: “There must be a way to make this useful… and maybe even make it pay for itself.”
So I set myself a challenge: one day to build something genuinely useful. A site that served a real audience (people in and around Balham), that was fun to build, and maybe could be turned into a small revenue stream.
It was also a great excuse to get properly stuck into Jekyll and the Minimal Mistakes theme — both of which I’d dabbled with before, but never used in anger. And, crucially, I wasn’t working alone: I had ChatGPT as a development assistant, sounding board, researcher, and occasional bug-hunter.
Balham is a reasonably affluent, busy part of south west London. It’s full of restaurants, cafés, gyms, independent shops, and people looking for things to do. It also has a surprisingly rich local history — from Victorian grandeur to Blitz-era tragedy.
I figured the site could be structured around three main pillars:
Throw in a curated homepage and maybe a blog later, and I had the bones of a useful site. The kind of thing that people would find via Google or get sent a link to by a friend.
I wanted something static, fast, and easy to deploy. My toolchain ended up being:
The site is 100% static, with no backend, no databases, no CMS. It builds automatically on GitHub push, and is entirely hosted via GitHub Pages.
I gave us about six solid hours to build something real. Here’s what we did (“we” meaning me + ChatGPT):
The domain was already pointed at GitHub Pages, and I had a basic “Hello World” site in place. We cleared that out, set up a fresh Jekyll repo, and added a _config.yml that pointed at the Minimal Mistakes remote theme. No cloning or submodules.
We decided to create four main pages:
index.md)directory/index.md)events/index.md)history/index.md)We used the layout: single layout provided by Minimal Mistakes, and created custom permalinks so URLs were clean and extension-free.
This was built from scratch using a YAML data file (_data/businesses.yml). ChatGPT gathered an initial list of 20 local businesses (restaurants, shops, pubs, etc.), checked their status, and added details like name, category, address, website, and a short description.
In the template, we looped over the list, rendered sections with conditional logic (e.g., don’t output the website link if it’s empty), and added anchor IDs to each entry so we could link to them directly from the homepage.
Built exactly the same way, but using _data/events.yml. To keep things realistic, we seeded a small number of example events and included a note inviting people to email us with new submissions.
We wanted the homepage to show a curated set of businesses and events. So we created a third data file, _data/featured.yml, which just listed the names of the featured entries. Then in the homepage template, we used where and slugify to match names and pull in the full record from businesses.yml or events.yml. Super DRY.
We added a map of Balham as a hero image, styled responsively. Later we created a .responsive-inline-image class to embed supporting images on the history page without overwhelming the layout.
This turned out to be one of the most satisfying parts. We wrote five paragraphs covering key moments in Balham’s development — Victorian expansion, Du Cane Court, The Priory, the Blitz, and modern growth.
Then we sourced five CC-licensed or public domain images (from Wikimedia Commons and Geograph) to match each paragraph. Each was wrapped in a <figure> with proper attribution and a consistent CSS class. The result feels polished and informative.
We went through all the basics:
title and description in front matter for each pagerobots.txt, sitemap.xml, and a hand-crafted humans.txt.html extensionsWe added GA4 tracking using Minimal Mistakes’ built-in support, and verified the domain with Google Search Console. A sitemap was submitted, and indexing kicked in within minutes.
We ran Lighthouse and WAVE tests. Accessibility came out at 100%. Performance dipped slightly due to Google Fonts and image size, but we did our best to optimise without sacrificing aesthetics.
We added a site-wide footer call-to-action inviting people to email us with suggestions for businesses or events. This makes the site feel alive and participatory, even without a backend form.
This started as a fun experiment: could I monetise an unused domain and finally learn Jekyll properly?
What I ended up with is a genuinely useful local resource — one that looks good, loads quickly, and has room to grow.
If you’re sitting on an unused domain, and you’ve got a free day and a chatbot at your side — you might be surprised what you can build.
Oh, and one final thing – obviously you can also get ChatGPT to write a blog post talking about the project :-)
The post Building a website in a day — with help from ChatGPT appeared first on Davblog.

I built and launched a new website yesterday. It wasn’t what I planned to do, but the idea popped into my head while I was drinking my morning coffee on Clapham Common and it seemed to be the kind of thing I could complete in a day — so I decided to put my original plans on hold and built it instead.
The website is aimed at small business owners who think they need a website (or want to update their existing one) but who know next to nothing about web development and can easily fall prey to the many cowboy website companies that seem to dominate the “making websites for small companies” section of our industries. The site is structured around a number of questions you can ask a potential website builder to try and weed out the dodgier elements.
I’m not really in that sector of our industry. But while writing the content for that site, it occurred to me that some people might be interested in the tools I use to build sites like this.
I generally build websites about topics that I’m interested in and, therefore, know a fair bit about. But I probably don’t know everything about these subjects. So I’ll certainly brainstorm some ideas with ChatGPT. And, once I’ve written something, I’ll usually run it through ChatGPT again to proofread it. I consider myself a pretty good writer, but it’s embarrassing how often ChatGPT catches obvious errors.
I’ve used DALL-E (via ChatGPT) for a lot of image generation. This weekend, I subscribed to Midjourney because I heard it was better at generating images that include text. So far, that seems to be accurate.
I don’t write much raw HTML these days. I’ll generally write in Markdown and use a static site generator to turn that into a real website. This weekend I took the easy route and used Jekyll with the Minimal Mistakes theme. Honestly, I don’t love Jekyll, but it integrates well with GitHub Pages and I can usually get it to do what I want — with a combination of help from ChatGPT and reading the source code. I’m (slowly) building my own Static Site Generator ( Aphra) in Perl. But, to be honest, I find that when I use it I can easily get distracted by adding new features rather than getting the site built.
As I’ve hinted at, if I’m building a static site (and, it’s surprising how often that’s the case), it will be hosted on GitHub Pages. It’s not really aimed at end-users, but I know to you use it pretty well now. This weekend, I used the default mechanism that regenerates the site (using Jekyll) on every commit. But if I’m using Aphra or a custom site generator, I know I can use GitHub Actions to build and deploy the site.
If I’m writing actual HTML, then I’m old-skool enough to still use Bootstrap for CSS. There’s probably something better out there now, but I haven’t tried to work out what it is (feel free to let me know in the comments).
For a long while, I used jQuery to add Javascript to my pages — until someone was kind enough to tell me that vanilla Javascript had mostly caught up and jQuery was no longer necessary. I understand Javascript. And with help from GitHub Copilot, I can usually get it doing what I want pretty quickly.
Many years ago, I spent a couple of years working in the SEO group at Zoopla. So, now, I can’t think about building a website without considering SEO.
I quickly lose interest in the content side of SEO. Figuring out what my keywords are and making sure they’re scattered through the content at the correct frequency, feels like it stifles my writing (maybe that’s an area where ChatGPT can help) but I enjoy Technical SEO. So I like to make sure that all of my pages contain the correct structured data (usually JSON-LD). I also like to ensure my sites all have useful OpenGraph headers. This isn’t really SEO, I guess, but these headers control what people see when they share content on social media. So by making that as attractive as possible (a useful title and description, an attractive image) it encourages more sharing, which increases your site’s visibility and, in around about way, improves SEO.
I like to register all of my sites with Ahrefs — they will crawl my sites periodically and send me a long list of SEO improvements I can make.
I add Google Analytics to all of my sites. That’s still the best way to find out how popular your site it and where your traffic is coming from. I used to be quite proficient with Universal Analytics, but I must admit I haven’t fully got the hang of Google Analytics 4 yet-so I’m probably only scratching the surface of what it can do.
I also register all of my sites with Google Search Console. That shows me information about how my site appears in the Google Search Index. I also link that to Google Analytics — so GA also knows what searches brought people to my sites.
I think that covers everything-though I’ve probably forgotten something. It might sound like a lot, but once you get into a rhythm, adding these extra touches doesn’t take long. And the additional insights you gain make it well worth the effort.
If you’ve built a website recently, I’d love to hear about your approach. What tools and techniques do you swear by? Are there any must-have features or best practices I’ve overlooked? Drop a comment below or get in touch-I’m always keen to learn new tricks and refine my process. And if you’re a small business owner looking for guidance on choosing a web developer, check out my new site-it might just save you from a costly mistake!
Originally published at https://blog.dave.org.uk on March 16, 2025.
I built and launched a new website yesterday. It wasn’t what I planned to do, but the idea popped into my head while I was drinking my morning coffee on Clapham Common and it seemed to be the kind of thing I could complete in a day – so I decided to put my original plans on hold and built it instead.
The website is aimed at small business owners who think they need a website (or want to update their existing one) but who know next to nothing about web development and can easily fall prey to the many cowboy website companies that seem to dominate the “making websites for small companies” section of our industries. The site is structured around a number of questions you can ask a potential website builder to try and weed out the dodgier elements.
I’m not really in that sector of our industry. But while writing the content for that site, it occurred to me that some people might be interested in the tools I use to build sites like this.
I generally build websites about topics that I’m interested in and, therefore, know a fair bit about. But I probably don’t know everything about these subjects. So I’ll certainly brainstorm some ideas with ChatGPT. And, once I’ve written something, I’ll usually run it through ChatGPT again to proofread it. I consider myself a pretty good writer, but it’s embarrassing how often ChatGPT catches obvious errors.
I’ve used DALL-E (via ChatGPT) for a lot of image generation. This weekend, I subscribed to Midjourney because I heard it was better at generating images that include text. So far, that seems to be accurate.
I don’t write much raw HTML these days. I’ll generally write in Markdown and use a static site generator to turn that into a real website. This weekend I took the easy route and used Jekyll with the Minimal Mistakes theme. Honestly, I don’t love Jekyll, but it integrates well with GitHub Pages and I can usually get it to do what I want – with a combination of help from ChatGPT and reading the source code. I’m (slowly) building my own Static Site Generator (Aphra) in Perl. But, to be honest, I find that when I use it I can easily get distracted by adding new features rather than getting the site built.
As I’ve hinted at, if I’m building a static site (and, it’s surprising how often that’s the case), it will be hosted on GitHub Pages. It’s not really aimed at end-users, but I know how to use it pretty well now. This weekend, I used the default mechanism that regenerates the site (using Jekyll) on every commit. But if I’m using Aphra or a custom site generator, I know I can use GitHub Actions to build and deploy the site.
If I’m writing actual HTML, then I’m old-skool enough to still use Bootstrap for CSS. There’s probably something better out there now, but I haven’t tried to work out what it is (feel free to let me know in the comments).
For a long while, I used jQuery to add Javascript to my pages – until someone was kind enough to tell me that vanilla Javascript had mostly caught up and jQuery was no longer necessary. I understand Javascript. And with help from GitHub Copilot, I can usually get it doing what I want pretty quickly.
Many years ago, I spent a couple of years working in the SEO group at Zoopla. So, now, I can’t think about building a website without considering SEO.
I quickly lose interest in the content side of SEO. Figuring out what my keywords are and making sure they’re scattered through the content at the correct frequency, feels like it stifles my writing (maybe that’s an area where ChatGPT can help) but I enjoy Technical SEO. So I like to make sure that all of my pages contain the correct structured data (usually JSON-LD). I also like to ensure my sites all have useful OpenGraph headers. This isn’t really SEO, I guess, but these headers control what people see when they share content on social media. So by making that as attractive as possible (a useful title and description, an attractive image) it encourages more sharing, which increases your site’s visibility and, in around about way, improves SEO.
I like to register all of my sites with Ahrefs – they will crawl my sites periodically and send me a long list of SEO improvements I can make.
I add Google Analytics to all of my sites. That’s still the best way to find out how popular your site it and where your traffic is coming from. I used to be quite proficient with Universal Analytics, but I must admit I haven’t fully got the hang of Google Analytics 4 yet—so I’m probably only scratching the surface of what it can do.
I also register all of my sites with Google Search Console. That shows me information about how my site appears in the Google Search Index. I also link that to Google Analytics – so GA also knows what searches brought people to my sites.
I think that covers everything—though I’ve probably forgotten something. It might sound like a lot, but once you get into a rhythm, adding these extra touches doesn’t take long. And the additional insights you gain make it well worth the effort.
If you’ve built a website recently, I’d love to hear about your approach. What tools and techniques do you swear by? Are there any must-have features or best practices I’ve overlooked? Drop a comment below or get in touch—I’m always keen to learn new tricks and refine my process. And if you’re a small business owner looking for guidance on choosing a web developer, check out my new site—it might just save you from a costly mistake!
The post How I build websites in 2025 appeared first on Davblog.