Skip to content

Repository files navigation

Perl Docker Image System

Docker Pulls Docker Build Status

This set of images provides a full and extensible setup to run your Perl applications with Docker.

There are three main versions of the image:

  • a -runtime version that should be used to run the final applications - the final target of your Dockerfile should use this one;
  • a -build version that can be used to build your applications;
  • a -devel version that can be used to debug and develop applications: this is mostly the -build version with extra modules.

Two extra variants build on those:

  • a -reply version — the -devel image with a Reply read-eval-print loop as its default command, handy for interactive experimentation;
  • a -runtime-lambda version — the -runtime image plus the AWS Lambda Runtime Interface Emulator for local Lambda testing (see the note below the table).

Each of these is available on three base images: Alpine, the official Perl image, and the wolfi-base from the Chainguard project.

Base Image Development Build Runtime
alpine:3.24 alpine-latest-devel / alpine-3.24-devel alpine-latest-build / alpine-3.24-build alpine-latest-runtime / alpine-3.24-runtime
alpine:edge alpine-next-devel / alpine-edge-devel alpine-next-build / alpine-edge-build alpine-next-runtime / alpine-edge-runtime
perl:5.44-slim perl-latest-devel / perl-5.44-slim-devel perl-latest-build / perl-5.44-slim-build perl-latest-runtime / perl-5.44-slim-runtime
perl:5.44 perl-full-devel / perl-5.44-devel perl-full-build / perl-5.44-build perl-full-runtime / perl-5.44-runtime
cgr.dev/chainguard/wolfi-base chainguard-latest-devel chainguard-latest-build chainguard-latest-runtime

Note — the Lambda variant carries many CVEs. Every base also publishes a -runtime-lambda variant (e.g. perl-latest-runtime-lambda, alpine-latest-runtime-lambda, chainguard-latest-runtime-lambda): the plain -runtime image plus the AWS Lambda Runtime Interface Emulator for local testing (see Lambda support). That emulator is a Go binary that carries a large number of Go-stdlib vulnerabilities, so it is bundled only in the -runtime-lambda and -devel images; the plain -runtime and -build images deliberately omit it. Deploy -runtime; reach for -runtime-lambda (or -devel) only when you need the local emulator.

The rest of this document walks you through the design, then shows how to create a Dockerfile for your project that makes full use of this setup while ending up with the smallest possible final image.

Design and directory layout

The system was designed to have a big, fully featured, build-time image, and another slim runtime image. A third version, which you can use during development, can also be created with a small addition to your app Dockerfile.

With a Docker multi-stage build, you can use a single Dockerfile to build and generate all the images, including the final runtime image.

The system assumes a specific directory layout for the app, the app dependencies, and the "stack":

  • the application lives in /app;
  • application dependencies are installed at /deps;
  • stack code and dependencies are installed at /stack.

Keeping the stack code and dependencies outside the app locations lets you create Docker images with just the stack components, which you can reuse between multiple projects. See Reusable stacks below for two examples: one for a Dancer2 + Text::Xslate + Starman combo, and another with everything needed to run a Minion job system.

Splitting your app dependencies from your code also lets you mount your laptop work directory under /app. With the dependencies in /deps and only your code under /app, you can start a container built from your app Dockerfile, mount the laptop work directory with the docker run -v option under /app, and develop in an environment that matches your deployment environment.

How to use

Below you'll find the recommended Dockerfile. The goal is to get a fast build, making use as much as possible of the Docker build cache, and provide the smallest possible image in the end.

This is an ordinary application. Dependencies are tracked with Carton in a cpanfile with the associated cpanfile.snapshot.

You should be able to just copy&paste this sample Dockerfile to your app work directory, and tweak the apk add lines to make sure that you add any packaged dependencies you might need. If you don't need any package dependencies, you can just remove those lines altogether.

Where the images are published

The images are published to two registries with identical tags on both, so you can use whichever you prefer:

  • Docker Hub — melopt/perl-alt:

    docker pull melopt/perl-alt:perl-latest-runtime
    
  • GitHub Container Registry — ghcr.io/melo/perl-alt:

    docker pull ghcr.io/melo/perl-alt:perl-latest-runtime
    

The Dockerfile examples below use the Docker Hub names. To pull from GHCR instead, just swap the melopt/perl-alt prefix for ghcr.io/melo/perl-alt — the tag suffixes (-build, -runtime, -devel, …) are the same on both.

Both registries carry the same multi-arch (linux/amd64 + linux/arm64) images, built natively per-architecture and pushed by the GitHub Actions workflow at .github/workflows/publish.yml.

### First stage, just to add our package dependencies. We put this on a
### separate stage to be able to reuse them across the "build" and
### "devel" phases lower down
FROM melopt/perl-alt:alpine-latest-build AS package_deps

### Add any packaged dependencies that your application might need. Make
### sure you use the -devel or -libs package, as this is to be used to
### build your dependencies and app. The postgres-libs shown below is
### just an example
RUN apk --no-cache add postgres-libs


### Second stage, build our app. We start from the previous stage, package_deps
FROM package_deps AS builder

### We copy all cpanfiles (this includes the optional cpanfile.snapshot)
### to the application directory, and we install the dependencies. Note
### that by default pdi-build-deps will install our apps dependencies
### under /deps. This is important later on.
COPY cpanfile* /app/
RUN cd /app && pdi-build-deps

### Copy the rest of the application to the app folder
COPY . /app/


### The third stage is used to create a developers image, based on the
### package_deps and build phases, and with
### possible some extra tools that you might want during local
### development. This layer has no impact on the runtime final version,
### but can be generated with a `docker build --target devel`
FROM package_deps AS devel

### Add any packaged dependencies that your application might need
### during development time. Given that we start from package_deps
### phase, all package dependencies from the build phase are already
### included.
RUN apk --no-cache add jq

### Assuming you have a cpanfile.devel file with all your devel-time
### dependencies, you can install it with this
RUN cd /app && pdi-build-deps cpanfile.devel

### Copy the App dependencies and the app code
COPY --from=builder /deps/ /deps/
COPY --from=builder /app/ /app/

### And we are done: this "development" image can be generated with:
###
###      docker build -t my-app-devel --target devel .
###
### You can then run it as:
###
###      cd your-app-workdir; docker run -it --rm -v `pwd`:/app my-app-devel
###


### Now for the fourth and final stage, the runtime edition. We start from the
### runtime version and add all the files from the build phase
FROM melopt/perl-alt:alpine-latest-runtime

### Add any packaged dependencies that your application might need
RUN apk --no-cache add postgres-libs

### Copy the App dependencies and the app code
COPY --from=builder /deps/ /deps/
COPY --from=builder /app/ /app/

### Add the command to start the application
CMD [ "your_app_start_command.pl" ]

What's inside?

All images include:

  • perl:
    • on Alpine images, we use the system perl:
      • 3.24: perl 5.42.2;
      • edge: perl 5.42.2.
    • on official Perl images, currently 5.44.0;
    • on Chainguard images, currently 5.44.0.
  • cpanm;
  • Carton;
  • App::cpm.

Some common libs and tools are also included:

  • openssl: this is not the default for Alpine, but a lot of software fails to build without it;
  • zlib;
  • expat;
  • libxml2 and libxml-utils;
  • jq.

The -build and -devel versions include the development versions of these libraries.

Entrypoint

The system includes a standard ENTRYPOINT script that sets a decent PERL5LIB based on the assumption that your app libs are under /app/lib.

It will also check for submodules under /app/elib/ and include all /app/elib/*/lib folders in PERL5LIB.

Finally, if you need your own ENTRYPOINT script, place an executable at /entrypoint and it will be executed before the COMMAND.

Build-time tests

The pdi-run-tests script runs during docker build to syntax-check your Perl scripts and optionally run your test suite.

Script checks

All executable Perl scripts under /app/bin, /app/sbin, and /app/lambda-handlers are syntax-checked with perl -wc.

Test discovery

Tests are opt-in. The script looks for a .pdi-run-tests-ok marker file to decide which test folders to run:

  • If /app/t/.pdi-run-tests-ok exists, all tests under /app/t are run;
  • If it doesn't exist, each immediate subdirectory of /app/t is checked — only those containing .pdi-run-tests-ok are run;
  • If no marker file is found, no tests are run.

The same logic applies to submodules under /app/elib/. For each /app/elib/<module>/t directory, the .pdi-run-tests-ok gating works identically. Additionally, any /app/elib/<module>/lib directories are added to the include path so that tests can find their modules.

Usage

Add pdi-run-tests as a RUN step in your Dockerfile, after copying your application code:

COPY . /app/
RUN pdi-run-tests

For example, to enable tests for your app and one elib submodule:

touch t/.pdi-run-tests-ok
touch elib/my-module/t/.pdi-run-tests-ok

Or to enable only a specific test subdirectory:

mkdir -p t/unit
touch t/unit/.pdi-run-tests-ok
# t/integration/ tests won't run (no marker file)

Which user runs pdi-build-deps

pdi-build-deps is a build-time step. The recommendation is to run it as the user that owns the install target - root, or any dedicated build user - and to treat that as distinct from the user your container runs as at runtime. There is no requirement that the two be the same.

The only hard rule: whichever user runs pdi-build-deps must be able to write the install directory (/deps by default, /stack in stack mode, or whatever you pass to --root=). In the stock images those directories are created as root, so the default RUN cd /app && pdi-build-deps works because build steps run as root.

If you deliberately build (or rebuild deps at container start via PDI_UPDATE_DEPS) as a non-root user, make the target writable by that user first, for example:

RUN chown -R 1000:0 /deps /stack   # or: chmod -R g+w /deps /stack
USER 1000
RUN cd /app && pdi-build-deps

pdi-build-deps checks this up front: if it cannot write the target it stops with a clear message naming the current user, the directory's owner, and how to fix it - rather than failing halfway through with a confusing error. (It also makes sure cpm/cpanm have a writable HOME for their caches, falling back to a temp dir when the current HOME is not writable.)

cpm build timeouts

On slow build hosts, some CPAN dists can take longer to configure or build than cpm's default per-phase timeouts (60s configure, 3600s build), causing pdi-build-deps to fail. You can raise these:

  • at build time, with the --configure-timeout=N / --build-timeout=N options of pdi-build-deps:
RUN cd /app && pdi-build-deps --build-timeout 900
  • at runtime, when deps are (re)built at container start via PDI_UPDATE_DEPS, with the PDI_CPM_CONFIGURE_TIMEOUT and PDI_CPM_BUILD_TIMEOUT environment variables.

N is a number of seconds. The command-line options take precedence over the environment variables; if neither is set, cpm's own defaults are used.

Lenient installs for NO_MYMETA distributions

App::cpm (the skaji/cpm tool we use to install dependencies) prefers not to install distributions that disable MYMETA generation (NO_MYMETA). This is intentional on the author's part - see skaji/cpm#311.

Note: this fork is not supported by the cpm author. Use it only for the distributions that actually need it.

If you depend on such a distribution, run pdi-build-deps in lenient mode. This installs your dependencies with a small fork of App::cpm, melo/cpm@no-mymeta-fallback, that falls back to the static META.json/META.yml when no MYMETA is produced, instead of aborting:

RUN cd /app && pdi-build-deps --lenient

You can also enable it at runtime (when deps are rebuilt at container start via PDI_UPDATE_DEPS) by setting PDI_BUILD_DEPS_LENIENT=1.

The fork ships as an optional layer at /deps/layers/app-cpm-lenient that nothing loads by default; lenient mode simply prepends its bin/ to PATH and its lib/perl5/ to PERL5LIB for the duration of the install, so the stock cpm (which keeps tracking upstream App::cpm) is untouched everywhere else.

Reusable stacks

You can also make stacks with commonly used combinations of packages. The setup is almost the same, the only difference is that when installing the dependencies and any other software you might need, the destination directory is /stack. The -runtime image will automatically include all of /stack dependencies and libs into PERL5LIB, and it will also make sure that any commands that are placed on bin/ directories are included on our PATH.

Dancer2 + Text::Xslate + Starman

Below you'll find an example of a Dockerfile for a stack that provides you:

  • Dancer2;
  • Text::Xslate for templating;
  • Starman for a web server.

This is actually available at melopt/dancer2-xslate-starman (repository is at Github melo/docker-dancer2-xslate-starman). You can check the cpanfile used for the stack.

FROM melopt/perl-alt:alpine-latest-build AS builder

COPY cpanfile* /stack/
RUN cd /stack && pdi-build-deps --stack


FROM melopt/perl-alt:alpine-latest-runtime

COPY --from=builder /stack /stack/

Some notes about this Dockerfile:

  • notice that the pdi-build-deps is run with the --stack option;
  • for the runtime version, we copy the /stack folders.

With this setup, you'll end up with a Docker image for your stack that you can reuse with multiple projects. For example, a simple Dancer2+Xslate-based web app could have a Dockerfile like this:

### Package deps, for build and devel phases
FROM melopt/perl-alt:latest-build AS package_deps

RUN apk --no-cache add mariadb-dev

### Build phase, build our app and our app deps
FROM package_deps AS builder

COPY cpanfile* /app/
RUN cd /app && pdi-build-deps

COPY . /app/


### Create the "development" image
FROM package_deps AS devel

RUN apk --no-cache add jq
RUN cd /app && pdi-build-deps cpanfile.devel

COPY --from=builder /deps/ /deps/
COPY --from=builder /app/ /app/


### Final phase: the runtime version - notice that we start from the stack image
FROM melopt/dancer2-xslate-starman

ENV PLACK_ENV=production
RUN apk --no-cache add mariadb-client

COPY --from=builder /deps/ /deps/
COPY --from=builder /app/ /app/

CMD [ "plackup", "--port", "80", "--server", "Starman" ]

Minion

Another stack, this time to allow users to run Minion workers and the admin interface. You can find the image at melopt/minion (repository at Github melo/docker-minion).

### Prepare the dependencies
FROM melopt/perl-alt:alpine-latest-build AS builder

RUN apk --no-cache add mariadb-dev postgresql-dev

COPY cpanfile* /stack/
RUN  cd /stack && pdi-build-deps --stack

### This stack includes some helper scripts
COPY bin /stack/bin/
### small "test phase", just to catch stupid mistakes...
RUN set -e && cd /stack && for script in bin/* ; do perl -wc $script ; done


### Runtime image
FROM melopt/perl-alt:alpine-latest-runtime

RUN apk --no-cache add mariadb-client postgresql-libs

COPY --from=builder /stack /stack

ENTRYPOINT [ "/stack/bin/minion-entrypoint" ]

Lambda support (experimental)

The Lambda support is still experimental. It seems to work fine but we are not using it in production at this moment.

The support includes testing your functions locally using the AWS Lambda Runtime Interface Emulator.

Most of the Lambda logic is provided by the excellent AWS::Lambda Perl module. Kudos to Shogo Ichinose for this.

Your handlers should be placed in the lambda-handlers/ of your project. Make sure your .pl handlers are executable.

A sample handler (named functions.pl) looks like this:

#!perl

use strict;
use warnings;
use JSON::MaybeXS;

sub echo {
  my ($payload, $context) = @_;

  return encode_json({ payload => $payload, context => { %$context } });
}

1;

The name of this function is functions.echo. The first part, functions, is the name of the handler file, functions.pl. The second part, echo, is the name of the sub called in that file. See AWS::Lambda for details on writing Lambda handlers.

Local testing uses the AWS Lambda Runtime Interface Emulator (RIE). Because that emulator carries a large number of CVEs, it is not in the plain -runtime images — build your test image FROM a -runtime-lambda (or -devel) base, which bundle it. For a deployed Lambda you can use the leaner plain -runtime image: on real AWS Lambda the RIE is not used at all.

To test the function locally, build your image (from a -runtime-lambda base) then run it like this:

$ docker run --rm -it -p 9000:8080 your_image your_handler.your_function
10 Dec 2022 16:31:26,839 [INFO] (rapid) exec '/var/runtime/bootstrap' (cwd=/app, handler=your_handler.your_function)
10 Dec 2022 16:31:36,015 [INFO] (rapid) extensionsDisabledByLayer(/opt/disable-extensions-jwigqn8j) -> stat /opt/disable-extensions-jwigqn8j: no such file or directory
10 Dec 2022 16:31:36,015 [WARNING] (rapid) Cannot list external agents error=open /opt/extensions: no such file or directory

You can then test with:

$ curl -XPOST 'http://localhost:9000/2015-03-31/functions/function/invocations' -d '{}'

The logs will show something like this:

START RequestId: 3503ccbd-0dfc-4eba-99f7-5aa72b58692b Version: $LATEST
END RequestId: 3503ccbd-0dfc-4eba-99f7-5aa72b58692b
REPORT RequestId: 3503ccbd-0dfc-4eba-99f7-5aa72b58692b	Init Duration: 0.36 ms	Duration: 63.70 ms	Billed Duration: 64 ms	Memory Size: 3008 MB	Max Memory Used: 3008 MB

For a fully working example see test/lambda inside this repository.

Contributing

Maintainer documentation — how the images are built and published, the required secrets, and how to prune stale tags — lives in CONTRIBUTING.md.

Repository

This image source repository is at https://github.com/melo/docker-perl-alt.

Author

Pedro Melo melo@simplicidade.org

About

A Docker image to manage Perl-based projects

Topics

Resources

Contributing

Stars

13 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages