I have been using a nginx container image for my blog and my ACM workshop for a while now.

Actually, I use a secure variation of it: nginxinc/nginx-unprivileged:alpine-slim.

I can run this container securely like this for example:

docker run -d \
    -p 8080:8080 \
    -u 1000 \
    --cap-drop=ALL \
    --read-only \
    --tmpfs /tmp \

Same approach on Kubernetes:

apiVersion: apps/v1
kind: Deployment
  name: nginx
    app: nginx
      app: nginx
        app: nginx
        fsGroup: 1000
        runAsGroup: 1000
        runAsNonRoot: true
        runAsUser: 1000
          type: RuntimeDefault
        - name: nginx
            allowPrivilegeEscalation: false
                - ALL
            privileged: false
            readOnlyRootFilesystem: true
          image: nginxinc/nginx-unprivileged:alpine-slim
            - containerPort: 8080
          - mountPath: /tmp
            name: tmp
      - emptyDir: {}
        name: tmp

I thought about using distroless for this scenario, but it is more about build your own base image if you need it. And I didn’t want to go that path.

I found out that Chainguard has been building and maintaining a bunch of distroless-like container images. Sounds exciting!

Reduce attack surface and minimize dependencies with our suite of images.

Chainguard Images contain only what is required to build or run your application. This results in fewer CVEs over time compared to other base images and on average an 80% reduction in overall size.

Chainguard images, among many other security features provided, are backed by:

  • wolfi, a Linux (un)distribution built with default security measures for the software supply chain.
  • melange, a declarative pipelines tool to build apk packages.
  • apko, a declarative APK-based OCI image builder.

Wow! I mean, I’m in! Let’s give the cgr.dev/chainguard/nginx container image a try!

Chainguard and Nginx logos

Here we are:

docker run -d \

Now, I need to run it as an unprivileged container, here is how I can accomplish it:

cat <<EOF > nginx.conf
events {}
http {
    server {
        listen                  8080;
docker run -d \
    -v $PWD/nginx.conf:/etc/nginx/nginx.conf \
    -p 8080:8080 \
    -u 65532 \
    --cap-drop=ALL \

Great! But now the missing piece is running it in a read-only mode:

docker run -d \
    -v $PWD/nginx.conf:/etc/nginx/nginx.conf \
    -p 8080:8080 \
    -u 65532 \
    --cap-drop=ALL \
    --read-only \
    --tmpfs /tmp \
    --tmpfs /sv \

But this is not working at the time this blog post is written, there is a known issue. Almost there, stay tuned! So for now, I will still continue using the nginxinc/nginx-unprivileged:alpine-slim one.

Why using cgr.dev/chainguard/nginx container image?

Smaller size!

REPOSITORY                    TAG           IMAGE ID       CREATED       SIZE
cgr.dev/chainguard/nginx      latest        7d2ef33a602f   6 hours ago   20.4MB
nginx                         latest        904b8cb13b93   2 weeks ago   142MB
nginx                         alpine        2bc7edbc3cf2   4 weeks ago   40.7MB
nginx                         alpine-slim   c59097225492   4 weeks ago   11.5MB

With the cgr.dev/chainguard/nginx (20.4MB) in comparison to nginx:alpine (40.7MB), we are saving 50.5% of space on disk!

But on the other hand, we could see that the nginx:alpine-slim (11.5MB) is way much smaller than the cgr.dev/chainguard/nginx (20.4MB).

Does it make cgr.dev/chainguard/nginx less secure than nginx:alpine-slim because the image size is bigger? The following section will tackle this question.

Less packages and dependencies!

Actually no, it doesn’t make nginx:alpine-slim more secure than cgr.dev/chainguard/nginx.

This blog post Image sizes miss the point explains the why:

To reduce debt, reduce image complexity not size.

By using a tool like Syft, we could see that cgr.dev/chainguard/nginx is less complex, with less dependencies, reducing the debt and surface of risks.

For cgr.dev/chainguard/nginx:

ca-certificates-bundle  20220614-r4  apk   
execline         apk   
glibc                   2.37-r1      apk   
glibc-locale-posix      2.37-r1      apk   
libcrypto3              3.0.8-r0     apk   
libgcc                  12.2.0-r9    apk   
libssl3                 3.0.8-r0     apk   
libstdc++               12.2.0-r9    apk   
nginx                   1.23.3-r1    apk   
pcre                    8.45-r0      apk   
s6              apk   
wolfi-baselayout        20230201-r0  apk   
zlib                    1.2.13-r3    apk

For nginx:alpine-slim:

alpine-baselayout       3.4.0-r0     apk     
alpine-baselayout-data  3.4.0-r0     apk     
alpine-keys             2.4-r1       apk     
apk-tools               2.12.10-r1   apk     
busybox                 1.35.0       binary  
busybox                 1.35.0-r29   apk     
busybox-binsh           1.35.0-r29   apk     
ca-certificates-bundle  20220614-r4  apk     
libc-utils              0.7.2-r3     apk     
libcrypto3              3.0.8-r0     apk     
libintl                 0.21.1-r1    apk     
libssl3                 3.0.8-r0     apk     
musl                    1.2.3-r4     apk     
musl-utils              1.2.3-r4     apk     
nginx                   1.23.3-r1    apk     
pcre2                   10.42-r0     apk     
scanelf                 1.3.5-r1     apk     
ssl_client              1.35.0-r29   apk     
tzdata                  2022f-r1     apk     
zlib                    1.2.13-r0    apk

Note: we could also expect that the cgr.dev/chainguard/nginx image will be simplified a little bit more soon like discussed here. Stay tuned!

Another important point is the fact that alpine is based on musl, on the other hand, cgr.dev/chainguard/nginx is based on glibc. This blog post: Why I Will Never Use Alpine Linux Ever Again highlights some known issues with alpine/musl.

More secure!

Less packages and dependencies could land to a more secure container image, by using Trivy for example, we can confirm it.

For both nginx:alpine-slim and cgr.dev/chainguard/nginx we could see that we don’t have any CVEs:

Total: 0 (UNKNOWN: 0, LOW: 0, MEDIUM: 0, HIGH: 0, CRITICAL: 0)

As a comparison, here below is what the scanning results will give for the two other variations.

For nginx:alpine:

Total: 6 (UNKNOWN: 0, LOW: 0, MEDIUM: 2, HIGH: 2, CRITICAL: 2)

For nginx:

Total: 116 (UNKNOWN: 0, LOW: 84, MEDIUM: 11, HIGH: 18, CRITICAL: 3)

That’s a wrap! Hope you liked it!

You could find many more Chainguard images such as Redis, Postgres, Go, Node, Python, Ruby, Rust, etc.

Happy sailing, stay safe out there!