Skip to main content

Building a Secure Docker Image

Published 3 min readMarkdown

๐Ÿ›ก๏ธ Building a Secure Docker Image โ€“ Step-by-Step Lab

When it comes to container security, your Docker image is the foundation. The smaller and more focused the image, the smaller the attack surface. In this hands-on lab, weโ€™ll build a secure Docker image from a Django application using multi-stage builds and Distroless base images.


๐Ÿ”ง Step 1: Clone the Source Code

Letโ€™s start by cloning the sample Django project that we'll use for this exercise.

git clone https://gitlab.practical-devsecops.training/pdso/django.nv.git
cd django.nv

You should now be inside the django.nv project folder containing all the source files.


๐Ÿณ Step 2: Build a Basic Docker Image (Alpine-based)

Weโ€™ll first build a simple image using python:3-alpine3.15, a lightweight base image.

docker build -t django.nv:1.0 .

After the build completes, check your images:

docker images

๐Ÿ“ฆ Expected Output

REPOSITORY   TAG   IMAGE ID       CREATED         SIZE
django.nv    1.0   071343727408   Few seconds ago 106MB

โœ… Small, neat image! But can we do better? Letโ€™s take a look at the Dockerfile.

cat Dockerfile

๐Ÿ“‹ Dockerfile (Alpine version)

FROM python:3-alpine3.15
WORKDIR /app
COPY . .
RUN apk add --no-cache gawk sed bash grep bc coreutils \
    && pip install -r requirements.txt \
    && chmod +x run.sh
EXPOSE 8000
CMD ["/app/run.sh"]

Good for starters. But not secure enough for production. Letโ€™s level up.


๐Ÿ” Step 3: Enhance Security with Distroless and Multi-Stage Build

Distroless images by Google contain only your application and its runtime dependenciesโ€”no package manager, shell, or even ls. This dramatically reduces attack surface.

๐Ÿ—๏ธ Multi-Stage Dockerfile (Distroless)

Letโ€™s rewrite our Dockerfile using a build image and a final distroless image.

cat >Dockerfile<<EOF
FROM debian:11-slim AS build

COPY requirements.txt .
RUN apt-get update \
    && apt-get install --no-install-suggests --no-install-recommends --yes python3-venv gcc libpython3-dev \
    && python3 -m venv /venv \
    && /venv/bin/pip install --upgrade pip

FROM build AS build-venv
COPY requirements.txt /requirements.txt
RUN /venv/bin/pip install -r /requirements.txt

FROM gcr.io/distroless/python3-debian11

WORKDIR /app
COPY --from=build-venv /venv /venv
COPY . .
RUN /venv/bin/python3 manage.py migrate \
    && /venv/bin/python3 manage.py loaddata fixtures/*

EXPOSE 8000
ENTRYPOINT ["/venv/bin/python3", "manage.py", "runserver", "0.0.0.0:8000"]
EOF

Now build the new image:

docker build -t django.nv:1.1 .

Check the image list again:

docker images

๐Ÿ“ฆ Expected Output

REPOSITORY                           TAG     IMAGE ID       CREATED         SIZE
django.nv                            1.1     dedad500d9de   Just now        104MB
gcr.io/distroless/python3-debian11   latest  aa70b0553cf1   N/A             54.5MB

๐Ÿ‘ Almost same size, but significantly more secure!


โ–ถ๏ธ Step 4: Run the Secure Container

Now weโ€™ll run the new container:

docker run -d --name django -p 8000:8000 django.nv:1.1
docker ps

Visit: http://localhost:8000 to check if the app is running.


๐Ÿ” Step 5: Test for Restricted Access

Letโ€™s see what happens when we try to get shell access into the container.

docker exec -it django bash

๐Ÿšซ Youโ€™ll get an error like:

exec: "bash": executable file not found in $PATH

Try with sh:

docker exec -it django sh

โœ… If youโ€™re lucky, you might get shell access. Try running a few commands:

hostname
ls -l

๐Ÿšซ Most commands will fail โ€” no shell utilities available!

This is by design. It prevents attackers from using familiar tools to exploit the container.


๐Ÿ’ก Final Thoughts

Distroless images:

  • โœ… Minimize vulnerabilities

  • โœ… Reduce attack surface

  • โœ… Are ideal for production

But:

  • โŒ They are hard to debug

  • โš ๏ธ You might need additional tooling like sidecars or enhanced logging for production observability


๐Ÿ“š Additional Resources


More from this blog