___________________________________________
title: Containerizing a Perl Script
tags: perl docker containers
date: 2015-08-29
___________________________________________

Intro

Almost every IT department has one; a Perl script written decades ago
by a long-gone employee that’s critical to production. It does it’s
job well, but no one wants to touch or take responsibility for it. You
just want to upgrade your infrastructure and bring it along, but there
are so many CPAN modules and inconsitencies you want never want to
look at it again. What to do?

 [CPAN]: http://www.cpan.org/

Executable Containers

Most articles on containers focus on running a service, exposing ports
and data volumes, but one feature frequently overlooked is the ability
to run a container as an executable.

In Docker, the ENTRYPOINT instruction makes this possible by running a
command and accpeting command line arguments from docker run.
Combining this with -it and --rm will run an interactive emphermal
container, disappearing when the command is complete.

 [Docker]: https://www.docker.com
 [ENTRYPOINT]: https://docs.docker.com/reference/builder/#entrypoint

Dockerfile

Let’s walk through an example that runs a Perl script called ps.pl,
which prints a simple process table using the CPAN module
Proc::ProcessTable and includes -h and -v arguments.

 [ps.pl]: https://gist.github.com/ecliptik/9a868cbe348d87a5141a#file-ps-pl
 [Proc::ProcessTable]: http://search.cpan.org/~durist/Proc-ProcessTable-0.39/ProcessTable.pm

The following Multi-Stage Dockerfile will build a container to run a
simple perl script,

 [Multi-Stage Dockerfile]: https://docs.docker.com/develop/develop-images/multistage-build/

-   Debian Stable Slim base image
-   Install OS packages
-   Install CPAN modules
-   Copy ps.pl script
-   Set ps.pl script as ENTRYPOINT
-   Set -h as default argument

 [ps.pl]: https://gist.github.com/ecliptik/9a868cbe348d87a5141a#file-ps-pl
 [1]: https://docs.docker.com/engine/reference/builder/#entrypoint

Here is the Dockerfile in full,

 [Dockerfile]: https://gist.github.com/ecliptik/9a868cbe348d87a5141a#file-dockerfile

   #This Dockerfile uses a Multi-Stage Build: https://docs.docker.com/develop/develop-images/multistage-build/
   FROM debian:stable-slim AS base
   LABEL maintainer="Micheal Waltz <[email protected]>"

   # Environment
   ENV DEBIAN_FRONTEND=noninteractive \
       LANG=en_US.UTF-8 \
       LC_ALL=C.UTF-8 \
       LANGUAGE=en_US.UTF-8

   # Install runtime packages
   RUN apt-get update \
       && apt-get install -y \
         perl

   # Set app dir
   WORKDIR /app

   # Intermediate build layer
   FROM base AS build
   #Update system and install packages
   RUN apt-get update \
       && apt-get install -yq \
           build-essential \
           cpanminus

   # Install cpan modules
   RUN cpanm Proc::ProcessTable Data::Dumper

   # Runtime layer
   FROM base AS run

   # Copy build artifacts from build layer
   COPY --from=build /usr/local /usr/local

   # Copy perl script
   COPY ./ps.pl .

   # Set Entrypoint
   ENTRYPOINT [ "/app/ps.pl" ]

Dockerfile Breakdown

Standard start of a Dockerfile, using the Official Debian image tagged
stable-slim as the base layer and a label for the image maintainer,

 [Official Debian image]: https://hub.docker.com/_/debian/
 [label]: https://docs.docker.com/engine/reference/builder/#label

   FROM debian:stable-slim AS base
   LABEL maintainer="Micheal Waltz <[email protected]>"

Basic environment variables for Debian packages through apt
non-interactive and set locale to UTF-8,

   ENV DEBIAN_FRONTEND=noninteractive \
       LANG=en_US.UTF-8 \
       LC_ALL=C.UTF-8 \
       LANGUAGE=en_US.UTF-8

Install “runtime” packages for the base layer that other layers will
inheriet. Sets /app as WORKDIR,

 [WORKDIR]: https://docs.docker.com/engine/reference/builder/#workdir

   # Install runtime packages
   RUN apt-get update \
       && apt-get install -y \
         perl

   # Set app dir
   WORKDIR /app

Intermediate build layer which is used temporarily to install packages
and modules that won’t be used directly at runtime. Using an
intermeiate layer will save space in the resulting image and no
include packages that aren’t used explicitly for runtime operation,
improving security posture.

   # Intermediate build layer
   FROM base AS build
   #Update system and install packages
   RUN apt-get update \
       && apt-get install -yq \
           build-essential \
           cpanminus

Install Perl CPAN packages and dependencies with cpanminus which works
well within a container build environment,

 [2]: https://www.cpan.org/
 [cpanminus]: http://search.cpan.org/~miyagawa/App-cpanminus-0.05/cpanm

   # Install cpan modules
   RUN cpanm Proc::ProcessTable Data::Dumper

Create final run layer for the resulting image and copy /usr/local
from the intermediate build layer to include required CPAN modules,

   # Runtime layer
   FROM base AS run

   # Copy build artifacts from build layer
   COPY --from=build /usr/local /usr/local

Copy ps.pl script into WORKDIR (aka /app) of the container. This is
put near the end of the Dockerfile since it is the file most likely to
change and will allow re-using of cache for previous layers, speeding
up subsequent builds.

 [ps.pl]: https://gist.github.com/ecliptik/9a868cbe348d87a5141a#file-ps-pl
 [speeding up subsequent builds]: https://thenewstack.io/understanding-the-docker-cache-for-faster-builds/

   # Copy perl script
   COPY ./ps.pl .

Set ENTRYPOINT to the ps.pl script,

 [ps.pl]: https://gist.github.com/ecliptik/9a868cbe348d87a5141a#file-ps-pl

   # Set entrypoint
   ENTRYPOINT [ "/app/ps.pl" ]

Running the Containerized Perl Script

Now that the Dockerfile is complete, build a container image with the
name ps-perl,

   🚀➜ docker build -t ps-perl .

With a container image ready, run it with docker run -it --rm ps-perl,
using the -it and --rm options so the container is removed when it
exits,,

   🚀➜ docker run -it --rm ps-perl
   PID    TTY        STAT     START                    COMMAND
   1      /dev/pts/0 run      Fri Sep 11 17:45:16 2020 /usr/bin/perl -w /app/ps.pl
   --------------------------------
   uid:  0
   gid:  0
   pid:  1
   fname:  ps.pl
   ppid:  0
   pgrp:  1
   sess:  1
   ttynum:  34816
   flags:  4210944
   minflt:  1684
   cminflt:  0
   majflt:  0
   cmajflt:  0
   utime:  40000
   stime:  10000
   cutime:  0
   cstime:  0
   priority:  20
   start:  1599846316
   size:  12779520
   rss:  8192000
   wchan:  0
   time:  50000
   ctime:  0
   state:  run
   euid:  0
   suid:  0
   fuid:  0
   egid:  0
   sgid:  0
   fgid:  0
   pctcpu:    5.00
   pctmem:  0.07
   cmndline:  /usr/bin/perl -w /app/ps.pl
   exec:  /usr/bin/perl
   cwd:  /app
   cmdline:  ARRAY(0x55fb475bb9f0)
   environ:  ARRAY(0x55fb472dd500)
   tracer:  0

Command Line Arguments

Command line arguments will also pass into the container when run
using the CMD to pass to ENTRYPOINT,

 [CMD]: https://docs.docker.com/engine/reference/builder/#cmd

-h

   🚀➜ docker run -it --rm ps-perl -h
   A simple perl version of ps

-v

   🚀➜ docker run -it --rm ps-perl -v
   Version: 1.0

Conclusions

By using the -slim variant of Debian for the base image and
multi-stage builds, the resulting container size is just over 130MB.
Using a smaller image like Alpine Linux could further reduce the image
size, but could introduce known caveats because of musl libc and glibc
differences.

 [Alpine Linux]: https://hub.docker.com/_/alpine/
 [known caveats]: http://gliderlabs.viewdocs.io/docker-alpine/caveats/
 [musl libc and glibc differences]: https://wiki.musl-libc.org/functional-differences-from-glibc.html

   🚀➜ docker images
   ps-perl             latest              938c3dde86d7        29 minutes ago      134MB

Additional References

-   Dockerising a Perl application
-   Downsizing Docker Containers
-   Proc::ProcessTable Example

 [Dockerising a Perl application]: http://robn.io/docker-perl/
 [Downsizing Docker Containers]: https://intercityup.com/blog/downsizing-docker-containers.html
 [Proc::ProcessTable Example]: http://search.cpan.org/~durist/Proc-ProcessTable-0.39/ProcessTable.pm

(Updated 9/11/2020)