Elderly Camels in the Cloud

Elderly cloud camel

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.


Prerequisites: Google Cloud and Cloud Run

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.

  1. Google account and project

    • Go to the Google Cloud Console.

    • Create a new project (e.g. “perl-cgi-cloud-run-demo”).

  2. Enable billing

    • Cloud Run is pay-as-you-go with a generous free tier, but you must attach a billing account to your project.

  3. Install the gcloud CLI

    • Install the Google Cloud SDK for your platform.

    • Run:

      gcloud init

      and follow the prompts to:

      • log in

      • select your project

      • pick a default region (I’ll assume “europe-west1” below).

  4. Enable required APIs

    In your project:

    gcloud services enable \
    run.googleapis.com \
    artifactregistry.googleapis.com \
    c  loudbuild.googleapis.com
  5. Create a Docker repository in Artifact Registry

    gcloud artifacts repositories create formmail-repo \
    --repository-format=docker \
    --location=europe-west1 \
    --description="Docker repo for CGI demos"

That’s all the GCP groundwork. Now we can worry about Perl.


The starting point: an old CGI FormMail

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:

  1. A PSGI wrapper that emulates CGI.

  2. A sendmail shim so the script can still “talk” sendmail.


Architecture in one paragraph

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.


Step 1 – Wrapping nms FormMail in PSGI

First we write a tiny PSGI wrapper. This is the only new Perl we need:


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

Step 2 – Adding a sendmail shim

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:


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.

Step 3 – Dockerfile (+ entrypoint) for Perl, PSGI and sendmail shim

Here’s a complete Dockerfile that pulls this together.


And here’s the docker-entrypoint.sh script:


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.


Step 4 – Building and pushing the image

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:


Then configure Docker to authenticate against Artifact Registry:


Now push the image:


If you’d rather not install Docker locally, you can let Google Cloud Build do this for you:


Use whichever workflow your team is happier with; Cloud Run doesn’t care how the image got there.


Step 5 – Deploying to Cloud Run

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.


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:


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.


How much work did we actually do?

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:

  1. Wrapped it with CGI::Emulate::PSGI.

  2. Dropped a sendmail shim in front of a real SMTP service.

  3. 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


When should you rewrite instead?

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.


FormMail is still probably a bad idea

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.


Conclusion

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.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.