Containerizing applications is an important step in modern software development. It helps with consistency, portability, and reliability across different environments. The Paicku project helps you streamline the process of containerization and testing so you can be sure that your application works as expected during development and once it's been released.
Paicku is a command-line tool, as well as a Node.js library, that lets you containerize applications with Cloud Native Buildpacks. You can run Paicku from the command-line or from Node.js.
Paicku is a wrapper around the pack command (the official interface for building container images with buildpacks).
Why use Paicku?
The name Paicku is a portmanteau of "pack" and "haiku", and it was chosen to reflect the simplicity of its results. Paicku exists to reduce setup, and to fit buildpacks into your Node.js workflow. Specifically, Paicku:
- Downloads the Pack CLI for your operating system (OS) and configures it
- Allows you to call Pack from Node.js, so you can write integration tests and run them in continuous integration and continuous delivery (CI/CD)
- Offers convenience with some additional features during build (for example, switching container-runtime)
Using Paicku
Paicku supports 2 interfaces:
- A command-line application
- A Node.js library, for JavaScript code that calls Paicku
To install and use Paicku, you must have:
- Node.js 20 or greater
- Podman or Docker
- A Node.js application (you can use the Paketo Buildpacks samples repository for testing)
Using the paicku command
You can install paicku globally using npm:
npm install -g paickuIf you're just testing it out, then you don't need to install Paicku globally because npx downloads it and runs it for you.
Whether you've installed paicku or you've decided to use npx paicku, clone the samples repository to follow along with this article's example:
git clone https://github.com/paketo-buildpacks/samplesBuild one of the available Node.js apps (replace npx paicku with just paicku if you've installed Paicku globally):
npx paicku build my-npm-app --path ./samples/nodejs/npmYou can omit the image name during paicku build to let Paicku assign a random one. After a successful build, you get a message:
Successfully built image my-npm-appPaicku doesn't require you to choose a builder. When no builder is defined, you get a warning in the build log telling you that paketobuildpacks/builder-ubi8-base is being used by default:
Warning: You haven't specified a builder, using the default one: docker.io/paketobuildpacks/builder-ubi8-baseThat image is based on the Red Hat Universal Base Image (UBI). For more information about UBI builders with Paketo, see Build applications with Paketo Buildpacks and Red Hat UBI container images
You can list available builders with the npx paicku builder suggest command.
Now run the container image:
podman run -d --rm --name containerized-app -p 8080:8080 my-npm-appOpen a browser and navigate to localhost:8080 to see the Paketo logo, or send an HTTP request with curl:
curl -i http://localhost:8080
HTTP/1.1 200 OK
[...]
<!DOCTYPE html>
<html>
<head>
<title>Powered By Paketo Buildpacks</title>
</head>
<body>
<img style="display: block; margin-left: auto; margin-right: auto; width: 50%;" src="https://paketo.io/images/paketo-logo-full-color.png"></img>
</body>
</html>Stop and remove the container
podman stop containerized-appAs you might have noticed, Paicku detects whether Podman or Docker is available on your host, and configures Pack before each build, meaning you can skip the manual configuration instructions.
To explicitly choose a container runtime, use the --container-runtime argument:
npx paicku build --container-runtime docker \
--path ./samples/nodejs/npmUsing paicku as a Node.js library
First, clone the sample repository:
git clone https://github.com/paketo-buildpacks/samplesNext, install the Paicku package as a development Node.js dependency:
cd samples/nodejs/npm
npm install -D paickuCreate a JavaScript file:
touch paicku-simple-example.mjsOpen the paicku-simple-example.mjs and add this code:
import { createPaicku } from "paicku";
const paicku = createPaicku();
console.log("Containerizing the app...");
const image = await paicku.build({
path: ".",
builder: "docker.io/paketobuildpacks/builder-ubi8-base",
});
console.log("Starting container...");
const container = await image.run({ exposedPorts: 8080 });
const response = await fetch(container.getUrl());
console.log("Response status:", response.status);
console.log("Stopping and removing container...");
await container.stop();
In case you have 2 container runtimes available on your machine, use the containerRuntime attribute on paicku.build to specify which one you want to use. For example:
const image = await paicku.build({
path: ".",
builder: "docker.io/paketobuildpacks/builder-ubi8-base",
containerRuntime: "podman"
});Run the file:
node ./paicku-simple-example.mjsYou receive a 200 response status, but if the build fails, a PaickuError object prints the build failure. The script above containerizes the application with buildpacks and runs the image from Node.js.
The next section shows how to turn this into an integration test.
The power of Paicku as a Node.js library
Normally, writing integration tests for apps using buildpacks can require some extra effort. You must configure the machine, install dependencies, and provide code to handle the build process. Paicku wraps that work into an API for you.
To start writing integration tests with the Paicku API, first install the paicku package as a project dependency and create a test file:
cd samples/nodejs/npm
npm install -D paicku
touch buildpacks-integration.test.mjsAdd the following code to the test file:
import test from "node:test";
import assert from "node:assert";
import { createPaicku } from "paicku";
const paicku = createPaicku();
test("Paicku should build and run an app", async () => {
const appPath = ".";
const appPort = 8080;
let container;
try {
const containerImage = await paicku.build({
builder: "docker.io/paketobuildpacks/builder-ubi8-base",
path: appPath,
});
container = await containerImage.run({ exposedPorts: appPort });
const url = new URL("/", container.getUrl()).toString();
const response = await fetch(url);
assert.strictEqual(
response.status,
200,
`Expected HTTP status 200, but got ${response.status}`,
);
} finally {
if (container) {
await container.stop();
}
}
});Run the test:
node buildpacks-integration.test.mjsIn the event of a successful run, you see the following output:
✔ Paicku should build and run an app (108940.401392ms)
ℹ tests 1
ℹ suites 0
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 108943.317838Buildpacks aren't limited to containerizing only Node.js applications. They support several languages, and so does Paicku. You can containerize a Java application, or any other app the builder supports.
To write an integration test for the samples/java/maven application, change appPath in buildpacks-integration.test.mjs to point to the java/maven project. Here's the relevant section, with the change:
test("Paicku should build and run an app", async () => {
const appPath = "../../java/maven";
const appPort = 8080;
let container;Run the tests:
node buildpacks-integration.test.mjsYou've just containerized a Java application, and the only difference between the 2 tests is the directory they point to.
For more examples, see the Paicku examples repository.
Conclusion
In this article, I demonstrated a few of the conveniences that Paicku has to offer when it comes to configuring Pack CLI. You've also seen how the Paicku Node.js library allows you to call Paicku programmatically, and write integration tests not just for Node.js applications but for any language that a builder supports.
I hope you found this article useful, and in case you have any ideas or questions, feel free to open an issue or a PR on the Paicku project.
Until next time, happy building!