DEV Community

Cover image for Elderly Camels in the Cloud
Dave Cross
Dave Cross

Posted on • Originally published at perlhacks.com on

Elderly Camels in the Cloud

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

  2. Enable billing

  3. Install the gcloud CLI

  4. Enable required APIs

  5. Create a Docker repository in Artifact Registry

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:

# 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;
Enter fullscreen mode Exit fullscreen mode

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:

open my $mail, '|-', '/usr/sbin/sendmail -t'
  or die "Can't open sendmail: $!";
# ... write headers and body ...
close $mail;
Enter fullscreen mode Exit fullscreen mode

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.

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"]
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 :

docker build -t europe-west1-docker.pkg.dev/PROJECT_ID/formmail-repo/nms-formmail:latest .
Enter fullscreen mode Exit fullscreen mode

Then configure Docker to authenticate against Artifact Registry:

gcloud auth configure-docker europe-west1-docker.pkg.dev
Enter fullscreen mode Exit fullscreen mode

Now push the image:

docker push europe-west1-docker.pkg.dev/PROJECT_ID/formmail-repo/nms-formmail:latest
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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.

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
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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.


The post Elderly Camels in the Cloud first appeared on Perl Hacks.

Top comments (0)