🚀 Executive Summary

TL;DR: Perl’s `cpm` tool often leads to ‘module not found’ errors in non-interactive environments like cron or CI/CD due to inconsistent `PERL5LIB` settings. The article details how environment differences cause this issue and provides three solutions: temporary environment injection, persistent user environment configuration with `local::lib`, and containerization for immutable infrastructure.

🎯 Key Takeaways

  • Perl’s `cpm` installs modules based on the executing user’s environment, leading to ‘Can’t locate module’ errors when `PERL5LIB` is not correctly set for non-interactive shells (e.g., cron, systemd, CI/CD pipelines).
  • Installing modules as `root` or via `sudo` typically places them in system-wide `@INC` paths, making them universally accessible, whereas non-root installs go to user-specific paths requiring `PERL5LIB` configuration.
  • Effective solutions range from direct environment variable injection for specific commands (e.g., in crontab or systemd unit files) to establishing a consistent user environment using `perl -Mlocal::lib` or adopting immutable infrastructure via containers (Docker) to package dependencies with the application.

q: is a CPM a CPM no matter the venue?

Perl’s CPM tool seems simple, but environment differences between users, sudo, and cron can cause mysterious ‘module not found’ errors. Here’s why it happens and three solid ways to fix it for good.

The ‘It Works On My Machine’ Perl Problem: Demystifying CPM and Environment Hell

I remember a Friday deployment that went sideways. One of our sharp junior engineers, Alex, had just finished a new reporting service. He’d tested it, I’d reviewed the code, and he ran the deploy script on the staging box, `stg-app-01`, as his own user with sudo. Everything lit up green. “Ship it,” I said. He ran the same script through our Jenkins pipeline, which executes as the `svc_deploy` user. Seconds later, red. Alarms blare. The log file screams: Can't locate Acme/Stats.pm in @INC...

Alex was frantic. “But I just installed it! I ran `cpm install Acme::Stats` on the box myself! It’s there!” He was right, it was there. But it wasn’t everywhere. And that, right there, is the heart of the problem. It’s a lesson we all learn the hard way: a CPM is not always a CPM, because the “environment” is the silent partner in every command you run.

The Real Culprit: It’s Not CPM, It’s Your Environment

So, what’s actually happening? It’s not a bug in `cpm`. The tool does exactly what it’s told. The problem is who’s doing the telling and from where. When you install a Perl module with `cpm` (or `cpanm`), it has to decide where to put the files. That decision is based on user permissions and environment variables.

Here’s a simplified look at the difference:

User Running `cpm install` Typical Install Location Who Can Find It?
root (or via `sudo`) A system path, like /usr/local/lib64/perl5/ Everyone. It’s in Perl’s default @INC search path.
A non-root user (e.g., svc_deploy) A local user path, like /home/svc_deploy/perl5/lib/perl5/ Only that user, and only if their shell is configured to look there via the PERL5LIB environment variable.

Your CI/CD pipeline, a cron job, or even a systemd service running as `svc_deploy` executes in a minimal, non-interactive shell. This kind of shell often doesn’t read `.bashrc` or `.profile`, so the magical `PERL5LIB` variable that points to the user’s local modules is never set. Perl runs, checks its standard `@INC` paths, doesn’t find the module, and dies. Game over.

The Fixes: From Duct Tape to a New Engine

You’re in the hot seat and you need to fix it. You’ve got options, ranging from a quick fix to get the system back online to a permanent solution that prevents this class of problem entirely.

Solution 1: The Quick and Dirty Environment Injection

This is the “It’s 3 AM and I just want to go to bed” fix. You’re not fixing the environment, you’re forcing the environment variable into the specific command that’s failing. It’s a band-aid, but sometimes you need a band-aid.

For a cron job: Prefix the command directly in your crontab.

# Old, failing cron job
# */5 * * * * /usr/bin/perl /opt/reporting-app/run_report.pl

# New, working cron job
*/5 * * * * PERL5LIB=/home/svc_deploy/perl5/lib/perl5 /usr/bin/perl /opt/reporting-app/run_report.pl

For a systemd service: Add an `Environment` directive to the unit file.

# in /etc/systemd/system/reporting.service
[Unit]
Description=My Reporting Service

[Service]
User=svc_deploy
ExecStart=/usr/bin/perl /opt/reporting-app/run_report.pl
Environment="PERL5LIB=/home/svc_deploy/perl5/lib/perl5"

[Install]
WantedBy=multi-user.target

Warning: This is a hack. It’s brittle. The moment you add another script or another service, you have to remember to add this variable again. You’re treating the symptom, not the cause. Use it to restore service, but plan to implement a real fix.

Solution 2: The Right Way – A Consistent User Environment

The real fix is to ensure the `svc_deploy` user’s environment is set up correctly, so any shell it runs has the right paths. The best way to manage local Perl module installations is with the `local::lib` module.

First, as the service user, run the `local::lib` bootstrap. This will figure out the right paths and tell you what to add to your shell’s profile.

# Log in as the service user
$ su - svc_deploy

# Run the bootstrap for the default location (~/perl5)
$ perl -Mlocal::lib

# This will output something like this, which you MUST add to the user's profile:
export PERL_LOCAL_LIB_ROOT="/home/svc_deploy/perl5";
export PERL_MB_OPT="--install_base \"/home/svc_deploy/perl5\"";
export PERL_MM_OPT="INSTALL_BASE=/home/svc_deploy/perl5";
export PERL5LIB="/home/svc_deploy/perl5/lib/perl5:$PERL5LIB";
export PATH="/home/svc_deploy/perl5/bin:$PATH";

Take that output and paste it into the user’s ~/.bash_profile or ~/.profile. Now, any login shell for that user will have the correct `PATH` and `PERL5LIB`. When `cpm` is run, it will install to the right place, and when your scripts are executed, Perl will know where to find them.

Pro Tip: Cron jobs don’t always source .bash_profile. They usually run a very minimal shell. To be safe, many systems will source ~/.profile for any shell, or you might need to force the cron job to run in a login shell using bash -l -c 'command'. Check your OS documentation for the specifics of its cron daemon.

Solution 3: The ‘Nuclear’ Option – Immutable Infrastructure with Containers

If you’re tired of fighting environment drift between dev, staging, and prod, then it’s time to stop managing the environment on the host and start packaging it with your application. I’m talking about containers (Docker, Podman, etc.).

With a container, you define the entire environment—OS, system libraries, Perl version, and all Perl modules—in a single file called a `Dockerfile`. The build process is automated and the result is a static, portable image.

A simple `Dockerfile` for our reporting app might look like this:

# Use an official Perl image as our base
FROM perl:5.34

# Set a working directory inside the container
WORKDIR /usr/src/app

# Copy the file that lists our dependencies
COPY cpanfile .

# Use cpm to install dependencies. They get installed into the container's
# system-wide Perl path, so we don't need PERL5LIB.
RUN cpm install --global --show-progress --cpanfile cpanfile

# Copy our application code into the container
COPY . .

# The command to run when the container starts
CMD ["perl", "./run_report.pl"]

This approach completely eliminates the “it worked on my machine” problem. The environment is explicitly defined, version-controlled, and immutable. It’s more upfront work, but for complex systems, the long-term stability and predictability are worth their weight in gold.

Darian Vance - Lead Cloud Architect

Darian Vance

Lead Cloud Architect & DevOps Strategist

With over 12 years in system architecture and automation, Darian specializes in simplifying complex cloud infrastructures. An advocate for open-source solutions, he founded TechResolve to provide engineers with actionable, battle-tested troubleshooting guides and robust software alternatives.


🤖 Frequently Asked Questions

âť“ Why does a Perl module installed with `cpm` work for one user but fail in a cron job or CI/CD pipeline?

This occurs because cron jobs and CI/CD pipelines typically run in minimal, non-interactive shells that do not source user profile files (like `.bashrc` or `.profile`). Consequently, the `PERL5LIB` environment variable, which points to user-installed modules, is not set, causing Perl to fail to locate the module in its default `@INC` paths.

âť“ How do the different solutions for Perl module environment issues compare in terms of effort and reliability?

Direct environment injection (e.g., in crontab or systemd) is a quick, low-effort fix but is brittle and prone to human error. Configuring a consistent user environment with `local::lib` is a more robust, medium-effort solution that ensures `PERL5LIB` is set for the user. The ‘nuclear’ option of using containers (e.g., Docker) offers the highest reliability and predictability by packaging the entire environment, but requires more upfront effort and a shift to immutable infrastructure practices.

âť“ What is a common pitfall when trying to resolve ‘module not found’ errors for Perl scripts run by cron?

A common pitfall is assuming that cron jobs will automatically source `~/.bash_profile` or `~/.profile` to set `PERL5LIB`. Cron often runs in a very minimal shell that does not execute these files. To fix this, you must either explicitly set `PERL5LIB` directly in the crontab entry or force the cron job to run in a login shell (e.g., `bash -l -c ‘command’`) that will source the necessary profile files.

Leave a Reply

Discover more from TechResolve - SaaS Troubleshooting & Software Alternatives

Subscribe now to keep reading and get access to the full archive.

Continue reading