Getting Started with the C/C++ Chainguard Images

Tutorial on how to get started with the C/C++ Chainguard Images

C and its derivative, C++, are two widely adopted compiled languages. Chainguard offers a variety of minimal, low-CVE container images built on the Wolfi un-distro which are suitable for deploying C-based compiled programs. In this guide, you will explore three ways you can use Chainguard Images to compile and run a C-based binary.

The image with which you choose to run your compiled program depends on the nature of your binaries. Static binaries can be executed in the minimal static Chainguard Image, while dynamically linked binaries can be run in the glibc-dynamic Image. For this demonstration, you will first compile a C binary using the gcc-glibc Chainguard Image, and then learn how to use a multi-stage build to run the resulting binary in the glibc-dynamic image. You’ll also cover an example showing the multi-stage build process for the C++ programming language. To learn more about the differences between these images, read our article on Choosing an Image for your Compiled Programs.

What is distroless? Distroless images are minimalist container images containing only essential software required to build or execute an application. That means no package manager, no shell, and no bloat from software that only makes sense on bare metal servers.
What is Wolfi? Wolfi is a community Linux undistro created specifically for containers. This brings distroless to a new level, including additional features targeted at securing the software supply chain of your application environment: comprehensive SBOMs, signatures, daily updates, and timely CVE fixes.
Chainguard Images Chainguard Images are a mix of distroless and development images based on Wolfi. Nightly builds make sure images are up-to-date with the latest package versions and patches from upstream Wolfi.

Video

The content in this article is also available as a video.

Prerequisites

To follow along with this guide, you will need to have Docker Engine and gcc, the GNU Compiler Collection, installed on your machine. You can find the code and Dockerfiles used in our Images demos GitHub repository.

Example 1 — Minimal C Chainguard Image

Step 1: Setting up a Demo Application

To start, let’s create a demo C application to run in your container. First you will create a folder to contain your demo files. The following command will create a new directory cguide and navigate to it.

mkdir -p ~/cguide && cd ~/cguide

Within this directory, you will create a file to hold the code for your first program. Use the text editor of your choice to begin editing a new file named hello.c. We will use nano as an example:

nano hello.c

Inside of your hello.c file, add in the following C code which will execute a “Hello, world!” application.

/* Chainguard Academy (edu.chainguard.dev)
*  Getting Started with the C/C++ Chainguard Images
*  Examples 1 & 2 - C
*/

#include <stdio.h>

// Main Function
int main(){
	printf("Hello, world!\n");
	printf("I am a demo from the Chainguard Academy.\n");
	printf("My code was written in C.\n");

	return 0;
}

When you are done editing the file, save and close it. If you used nano, you can do so by pressing CTRL + X, Y, and then ENTER.

Now, let’s compile this file with gcc. This command uses the -Wall flag to display compiler errors and warnings, if any occur, and includes the -o flag to rename your executable to hello.

gcc -Wall -o hello hello.c

Once your program has compiled, you can run it with the following command:

./hello

This will return the “Hello, world!” program output in your terminal if the program executed successfully.

Hello, world!
I am a demo from the Chainguard Academy.
My code was written in C.

Now that you have successfully tested your example program locally, next, you will compile and run it from inside of an image.

Step 2: Creating the Dockerfile

An advantage of choosing to run your code inside of containerized environments is portability. In the previous step, gcc compiled the binary to run on your machine. However, if you were to run this binary on a different operating system, it likely will fail to execute properly. Using a container ensures that your program will run on any machine as the containerized environment will be consistent across platforms.

Let us begin by creating a Dockerfile called Dockerfile1 for your image.

nano Dockerfile1

This Dockerfile will do the following:

  1. Use the gcc-glibc:latest Chainguard Image as the base image;
  2. Create and set the current working directory to /home/build;
  3. Copy the hello.c program code to the current directory;
  4. Compile the program and name it hello;
  5. Copy the compiled binary to /usr/bin;
  6. Set the image to run as a non-root user; and,
  7. Execute the compiled binary when the container is started.
# Example 1 - Single Stage Build for C

FROM cgr.dev/chainguard/gcc-glibc:latest

RUN ["mkdir", "/home/build"]
WORKDIR /home/build

COPY hello.c ./

RUN ["gcc", "-Wall", "-o", "hello", "hello.c"]
RUN ["cp", "hello", "/usr/bin/hello"]

USER 65532

ENTRYPOINT ["/usr/bin/hello"]

Add this text to your Dockerfile, save, and close it.

Next, use the Dockerfile you just created to build an image named example1 by running the following command. The -f flag specifies the Dockerfile which you are using to build from, and the -t flag will tag your image with a meaningful name.

docker build -f Dockerfile1 -t example1:latest .

With your image built, you can now run it with the following command.

docker run --name example1 example1:latest

You will see output in your terminal identical to that of the binary you compiled locally.

Hello, world!
I am a demo from the Chainguard Academy.
My code was written in C.

In the next example, we will look at an alternative way to run your binary using a multi-stage build.

Example 2 — Multi-Stage Build for C Applications

In our first example, you successfully compiled and executed your C binary in the gcc-glibc image. To go a step further, you can use a multi-stage build, allowing you to compile your program in one image and execute it in another image.

A multi-stage build gives you more control over your final image, as you can transfer your program to an image with a smaller footprint after build time to reduce your program’s attack surface. The glibc-dynamic image, which you will use as your second stage in the build, does not contain gcc. Because of this, a malicious binary could not be compiled by an attacker tampering with the image.

Creating the Dockerfile

Create a new Dockerfile called Dockerfile2.

nano Dockerfile2

This time, the Dockerfile will do the following:

  1. Use the gcc-glibc Chainguard Image as the builder stage;
  2. Create and set the current working directory to /home/build;
  3. Copy your example hello.c program code to the current directory;
  4. Compile the program using gcc and name it hello;
  5. Begin a new stage using the glibc-dynamic Chainguard Image;
  6. Copy the compiled binary to /usr/bin from the builder stage;
  7. Set the image to run as a non-root user; and,
  8. Execute your binary from the glibc-dynamic image when the container is started.
# Example 2 - Multi-Stage Build for C

FROM cgr.dev/chainguard/gcc-glibc:latest AS builder

RUN ["mkdir", "/home/build"]
WORKDIR /home/build

COPY hello.c ./

RUN ["gcc", "-Wall", "-o", "hello", "hello.c"]

FROM cgr.dev/chainguard/glibc-dynamic:latest

COPY --from=builder /home/build/hello /usr/bin/

USER 65532

ENTRYPOINT ["/usr/bin/hello"]

When you are finished editing your Dockerfile, save and close it.

With the new Dockerfile created, you can build the image. Execute the following command in your terminal to build your multi-stage image.

docker build -f Dockerfile2 -t example2:latest .

With your image built, you can now run it with the following command.

docker run --name example2 example2:latest

You will see output in your terminal identical to that of the previous example.

Hello, world!
I am a demo from the Chainguard Academy.
My code was written in C.

Having your program execute from a smaller image with less packages reduces your potential attack surface, making it a more secure approach for production-facing builds.

Example 3 — Multi-Stage Build for C++ Applications

So far, our demonstrations have featured a program coded in C. A similar image building process applies to binaries compiled for the C++ programming language.

Step 1: Setting up a Demo Application

In your terminal, create a new file called hello.cpp.

nano hello.cpp

Add the following C++ code to the file you just created. This code will display a greeting specifying that it was written in C++.

/* Chainguard Academy (edu.chainguard.dev)
*  Getting Started with the C/C++ Chainguard Images
*  Example 3 - C++
*/

#include <iostream>
using namespace std;

// Main Function
int main(){
    cout << "Hello, world!\n";
    cout << "I am a demo from the Chainguard Academy.\n";
    cout << "My code was written in C++.\n";

    return 0;
}

When you are done editing your file, save and close it.

You can now compile your C++ program using g++. Execute the following command in your terminal to compile the program. The command will display any compiler warnings or errors and will name the resultant binary hello.

g++ -Wall -o hello hello.cpp

Now you can test your compiled binary.

./hello

You will see the following output in your terminal.

Hello, world!
I am a demo from the Chainguard Academy.
My code was written in C++.

Now that you have confirmed that your C++ program executes, you are ready to build it inside of an image.

Step 2: Creating the Dockerfile

With a working C++ example, you can compile and run our program using a multi-stage build. With the text editor of your choice, create a new file named Dockerfile3.

nano Dockerfile3

This Dockerfile will do the following:

  1. Use the gcc-glibc Chainguard Image as the builder stage;
  2. Create and set the current working directory to /home/build;
  3. Copy your example hello.cpp program code to the current directory;
  4. Compile the program using g++ and name it hello;
  5. Begin a new stage using the glibc-dynamic Chainguard Image;
  6. Copy the compiled binary to /usr/bin from the builder stage;
  7. Set the image to run as a non-root user; and,
  8. Execute your binary from the glibc-dynamic image when the container is started.
# Example 3 - Multi-Stage Build for C++

FROM cgr.dev/chainguard/gcc-glibc:latest AS builder

RUN ["mkdir", "/home/build"]
WORKDIR /home/build

COPY hello.cpp ./

RUN ["g++", "-Wall", "-o", "hello", "hello.cpp"]

FROM cgr.dev/chainguard/glibc-dynamic:latest

COPY --from=builder /home/build/hello /usr/bin/

USER 65532

ENTRYPOINT ["/usr/bin/hello"]

When you are finished editing your Dockerfile, save and close it.

With your new Dockerfile created, you can build the image. Execute the following command in your terminal to build your multi-stage C++ image.

docker build -f Dockerfile3 -t example3:latest .

With your image built, you can now run it with the following command.

docker run --name example3 example3:latest

You will see output in your terminal identical to that of the C++ binary you compiled locally.

Hello, world!
I am a demo from the Chainguard Academy.
My code was written in C++.

With that, you have successfully performed a multi-stage image build for both C and C++ programs.

Clean Up

After completing the previous examples, you will have containers, images, and files remaining on your local machine. This section will show you how to remove these artifacts.

You can remove the containers you built by executing the following command.

docker container rm example1 example2 example3

Then, you can remove their associated image builds as well:

docker image rm example1:latest example2:latest example3:latest

To remove the directory containing your Dockerfiles, binaries, and program code, run the following command:

rm -r ~/cguide

Following these commands, all artifacts introduced in this guide will now be removed from your machine.

Advanced Usage

If your project requires a more specific set of packages that aren't included within the general-purpose C/C++ Chainguard Image, you'll first need to check if the package you want is already available on the wolfi-os repository.

Note: If you're building on top of an image other than the wolfi-base image, the image will run as a non-root user. Because of this, if you need to install packages with apk install you need to use the USER root directive.

If the package is available, you can use the wolfi-base image in a Dockerfile and install what you need with apk, then use the resulting image as base for your app. Check the "Using the wolfi-base Image" section of our images quickstart guide for more information.

If the packages you need are not available, you can build your own apks using melange. Please refer to this guide for more information.

Last updated: 2024-08-15 19:37