Distroless Images
Distroless container images, unlike the traditional ones, does not include software that are common in distro-based images, such as package managers and shells. This approach aims to minimize the image size and reduce vulnerabilities by removing unnecessary components to run an application.
These types of images are suitable for production environments rather than running interactive containers, since they are often smaller and have less attack vectors than traditional images.
Because distroless images include only essential components to run an application, it might seem that you’re limited to the components that are provided by the distroless base image, since there is no shells or package managers. This is when Docker multi-stage build feature shines.
Multi-Stage builds
One can tell that the multi-stage build feature is being used when a Dockerfile has multiple FROM
statements in it. With this feature, each stage has a well-defined purpose other than running the application, which is the responsibility of the final stage. The earlier stages are typically used to build an application and run tests.
This is particular useful when we are using distroless images. Since we don’t have usual resources provided by a regular image, we can build the application at the first stage of a docker image and then share only the artifacts to the final stage, which can be build on top of a distroless image.
Developing a custom distroless image
The snippet bellow is from CGRFlask.Dockerfile
:
FROM cgr.dev/chainguard/python:latest-dev AS build
WORKDIR /app
RUN git clone https://github.com/lkellermann/LINUXtips-Giropops-Senhas.git /app && rm -rf .git
RUN python -m venv venv
ENV PATH="/app/venv/bin:$PATH"
RUN pip install --no-cache-dir -r requirements.txt
FROM cgr.dev/chainguard/python:latest AS app
WORKDIR /app
COPY --from=build /app /app
ENV PATH="/app/venv/bin:$PATH"
EXPOSE 5000
ENTRYPOINT [ "flask"]
CMD [ "run", "--host=0.0.0.0" ]
The syntax to build the image is the following:
docker image build -f docker/CGRFlask.Dockerfile -t <your dockerhub user>/<image name>:<version> .
In our example:
docker image build -f docker/CGRFlask.Dockerfile -t kellermann92/linuxtips-giropops-senhas:1.0 .
The image above has two stages, build
and app
.
The build
stage is build on top of a regular image provided by Chainguard
. After defining the working directory, the GitHub repository containing the application is cloned into it. Then, we need to create a virtual environment that is going to contain all dependencies of the application and then, share this environment with the final stage. After creating the virtual environment, the path to the binary that enables executing the code using the virtual environment is added to the PATH
environment variable. Then, the dependencies from requirements.txt
files are installed in the virtual environment.
The app
stage is build on top of a distroless image. In order to install the dependencies of our application, we copy the whole app
folder from the build
stage, which contains our application code and the virtual environment with the necessary packages installed. After that, we update the PATH
environment variable by adding the binary path to run the Python virtual environment. And finally, we start our flask application.
Looking for vulnerabilities
Despite having less components and less layers than a regular container image, it’s still important to scan them in order to detect any know vulnerabilities, like misconfiguration of the base image or outdated packages. In our example, we are going to use Trivy and Docker Scout.
Using Trivy
After building your image, run the following command:
trivy image <your dockerhub user>/<your image>:1.0
In our example:
trivy image kellermann92/linuxtips-giropops-senhas:1.0
You should get a similar result as the image below:
As we can see in the the image above, Trivy didn’t catch any vulnerabilities in the developed image. However, it’s not a guarantee that the image is not vulnerable. It’s possible that nothing was detected because the database of vulnerabilities that Trivy uses has no knowledge about the issues we have. To double-check this, we can use another vulnerability scan tool.
Using Docker Scout
You must have a DockerHub account and upload the image by running:
docker image push kellermann92/linuxtips-giropops-senhas:1.0
then
docker scout cves kellermann92/linuxtips-giropops-senhas:1.0
The output will look like the image below:
The output will suggest you to run the following command in order to view the base image update recommendations:
docker scout recommendations kellermann92/linuxtips-giropops-senhas:1.0
Depending on the time you execute the command, you may have recommentations to follow or not. In the image bellow, there are no recommendations:
Enabling Docker Scout Image analysis on DockerHub
Go to your repo, settings and select Docker Scout image analysis
and save.
Running the application
To run the password generator application, you can start by running the Redis container or the Flask container.
To start the Redis container, run the following command:
docker container run -d --name redis -p 6379:6379 redis:alpine3.20
To start the Flask container with the distroless image created in the previous sections, run the following command:
docker container run -d -p 5000:5000 -e REDIS_HOST=<your machine local IP> --name <container name> <your dockerhub username>/<image name>:<version>
In my machine, I ran the following:
docker container run -d -p 5000:5000 -e REDIS_HOST=192.168.0.76 --name linuxtips-giropops-senhas kellermann92/linuxtips-giropops-senhas:1.0
Now, you can access the application by typing in your browser 127.0.0.1:5000
:
Using docker compose
There is a better approach at achieving the task above, which is by using docker compose. This is a plugin that orchestrates multiple docker containers. It’s meant to be used in development environment, not in production. With it, you can define volumes that are shared between containers, or connect them using the same network (instead of using the bridge connection as we did before).
The docker compose file to run the same application is presented below:
services:
giropops-senhas:
build:
context: ./docker
dockerfile: CGRFlask.Dockerfile
#container_name: giropops-senhas
ports:
- 5000:5000
networks:
- giropops
environment:
REDIS_HOST: redis
volumes:
- type: volume
source: strigus
target: /strigus
depends_on:
- redis
labels:
com.leandroasaservice.description: my app on docker compose
com.leandroasaservice.version: 1.0
deploy:
replicas: 1
update_config:
parallelism: 1
delay: 10s
resources:
reservations:
cpus: "0.10"
memory: 128M
limits:
cpus: "0.5"
memory: 256M
restart_policy:
condition: on-failure
delay: 5s
max_attempts: 3
devices:
- /dev/bus/usb:/dev/bus/usb
dns:
- 8.8.8.8
- 8.8.4.4
healthcheck:
test: ["CMD", "flask", "--version"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
redis:
image: redis:alpine3.20
command: redis-server --appendonly yes
networks:
- giropops
volumes:
- strigus:/strigus
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 5s
retries: 3
start_period: 10s
labels:
com.leandroasaservice.description: redis on docker compose
com.leandroasaservice.version: 1.0
networks:
giropops:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16
labels:
com.leandroasaservice.network: giropops_network
volumes:
strigus:
driver: local
driver_opts:
type: none
device: ~/pick2024/strigus
o: bind
labels:
com.leandroasaservice.volume: strigus_volume