Skip to main content

Image Building

In order to internalize images, users must follow some guidelines to make the images functional and compatible with the CAIP platform. This rules are defined to ensure the upmost security and compliance of the platform to garantee the safety of the users and the platform itself on today's standards. Also, since we want to provide the users with the flexibility to use their own custom images, we need to enforce some rules to make sure that the images are compliant with the platform standards.

This document will cover the main rules that users need to follow to make their images compliant with the platform and therefore be able to internalize them and use them in their applications and deployments.

The current rules are the following:

  • One Port, All Applications: All main application images must be exposed on port 8501. This does not apply to init or sidecar containers (check note below).
  • Non-Root User: All images must be configured to run with a non-root user to enhance security and reduce the attack surface of the applications.
  • Zero Vulnerability Tolerance: All images must be scanned for vulnerabilities and have zero Critical vulnerabilities before they can be internalized into the platform. High, Medium and Low wiz findings are accepted.

One Port, All Applications

The first rule is enforcing your image to expose in a specific port. Since the platform is designed to run multiple applications in the same infrastructure, we need to make sure that all applications are exposing in the same port to avoid conflicts and ensure the proper functioning of the platform.

This document establishes 8501 as the single, unified external port for all Dockerised main application services, regardless of the framework running inside the container. This means the main application image must have a service configured to be reachable at http://localhost:8501 inside the container, and this port will then be exposed to the outside world through the platform.

Note

This rule applies only to the main application. Init containers do not need to expose any port. If your sidecar container exposes a service, it must use port 8502 instead. See Init and Sidecar Containers to learn more.

Dockerfile pattern

Every Dockerfile needs to have the following line to expose the port 8501:

EXPOSE 8501

This alone does not make the application run in that port. The command or entry point of the application needs to be configured to run in that port as well, but this differs for different frameworks.

Note

Users can check localy if the image is running at the right port by running the following command: docker run -p 8501:8501 <my-image>

FastAPI

FastAPI is served by Uvicorn. Pass --port 8501 directly to Uvicorn:

EXPOSE 8501

CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8501"]

Streamlit

Streamlit already defaults to 8501, so the configuration is minimal, but still it can be enforced:

EXPOSE 8501

CMD ["streamlit", "run", "app.py",
"--server.port=8501",
"--server.address=0.0.0.0"]

Node.js

This frameworks requires the PORT environment variable to be set before starting the server:

ENV PORT=8501
EXPOSE 8501

CMD ["node", "server.js"]

Flask

Flask also can be configured to run in the port 8501 by setting the port parameter in the env variables:

EXPOSE 8501

ENV FLASK_APP=app/main.py
ENV FLASK_ENV=production
ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_RUN_PORT=8501

CMD ["python", "-m", "flask", "run"]

Non-Root User

As nowadays security is a must, we need to make sure that the images are running with non-root users. This is a common practice in the industry to reduce the attack surface of the applications and to ensure that the applications are running with the least privileges possible. By running with custom non-root users and groups we limit some permissions and capabilities of the process running inside the docker container, reducing the risks associated with privileged user access.

Using a dedicated non-root user:

  • Reduces the impact of container escapes
  • Aligns with Kubernetes Pod Security Standards
  • Improves compatibility with restricted environments
  • Follows container security best practices

In order for the images to be compliant with this rule, the images must be able to run with:

  • User ID: 1000
  • Group ID: 1000

Dockerfile pattern

To create a non-root user and group in the Dockerfile, you can use the following commands:

RUN groupadd -g 1000 customgroup && \
useradd -m -u 1000 -g customgroup customuser

This will create a group named customgroup with GID 1000 and a user named customuser with UID 1000, which belongs to the customgroup.

After the user and group are created, ensure application files are owned by the non-root user:

RUN chown -R customuser:customgroup /app

Finally, switch to the non-root user in the Dockerfile:

USER 1000:1000

Examples

As a platform we want to provide users with some reference Dockerfiles that they can use as a template to create their own compliant images. This examples can be found in the following repository:

Get it on GITHUB

This repository contains some examples of simple applications in different frameworks that are compliant with the guidelines provided by this document. For each example it is also provided an Bruno collection with the same example to make it easier for users to test and run the examples locally.

fastAPI with UV as package manager

FROM python:3.14-slim AS libexpat-builder
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
wget \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Build libexpat 2.8.0 from source
RUN wget https://github.com/libexpat/libexpat/releases/download/R_2_8_0/expat-2.8.0.tar.gz \
&& tar -xzf expat-2.8.0.tar.gz \
&& cd expat-2.8.0 \
&& ./configure --prefix=/usr \
&& make -j$(nproc) \
&& make install


FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1
ENV ROOT_PATH=
WORKDIR /app

COPY --from=libexpat-builder /usr/lib/libexpat* /usr/lib/
COPY --from=libexpat-builder /usr/include/expat* /usr/include/

# Install system packages
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Create non-root user/group
RUN groupadd -g 1000 app && \
useradd -u 1000 -g 1000 -m -s /bin/bash app

# Copy dependency files first for better layer caching
COPY pyproject.toml uv.lock ./


# Copy application
COPY app ./app


# Set ownership
RUN chown -R 1000:1000 /app

# Switch to non-root user
USER 1000:1000

# Install dependencies
RUN uv sync --frozen --no-dev

EXPOSE 8501

CMD ["/bin/sh", "-c", "uv run uvicorn app.main:app --host 0.0.0.0 --port 8501 --root-path \"${ROOT_PATH}\""]
Flask with uv as package manager
FROM python:3.14-slim

ENV PYTHONDONTWRITEBYTECODE=1
ENV PYTHONUNBUFFERED=1

WORKDIR /app

# Install system packages
RUN apt-get update && apt-get install -y --no-install-recommends \
curl \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/

# Create non-root user/group
RUN groupadd -g 1000 app && \
useradd -u 1000 -g 1000 -m -s /bin/bash app

# Copy dependency files first for better layer caching
COPY pyproject.toml uv.lock ./

# Copy application
COPY app ./app

# Set ownership
RUN chown -R 1000:1000 /app

# Switch to non-root user
USER 1000:1000

# Install dependencies
RUN uv sync --frozen --no-dev

EXPOSE 8501

ENV FLASK_APP=app/main.py
ENV FLASK_ENV=production
ENV FLASK_RUN_HOST=0.0.0.0
ENV FLASK_RUN_PORT=8501

CMD ["uv", "run", "python", "-m", "flask", "run"]
Nodejs app
FROM node:22-slim

WORKDIR /app

# Install dependencies
COPY app/package*.json ./
RUN npm ci --omit=dev

# Copy app code
COPY app/ ./

EXPOSE 8501

ENV HOST=0.0.0.0
ENV PORT=8501

# Run as non-root user (built into the node image)
USER node

CMD ["node", "main.js"]
Streamlit app using requirements.txt
FROM python:3.14-slim

ENV DEBIAN_FRONTEND=noninteractive

RUN groupadd -g 1000 app && \
useradd -u 1000 -g 1000 -m -s /bin/bash app

WORKDIR /app

RUN apt-get update && apt-get install -y \
build-essential \
curl \
git \
&& rm -rf /var/lib/apt/lists/*

COPY app/ .

RUN pip3 install -r requirements.txt

RUN chown -R 1000:1000 /app

USER 1000:1000

EXPOSE 8501

HEALTHCHECK CMD curl --fail http://localhost:8501/_stcore/health || exit 1

ENTRYPOINT ["streamlit", "run", "app.py", "--server.port=8501", "--server.address=0.0.0.0"]

Zero Vulnerability Tolerance

The platform has a zero vulnerability tolerance policy, which means that all images must be scanned for vulnerabilities and have zero critical vulnerabilities before they can be internalized into the platform. This is done to ensure that the applications running on the platform are secure and do not pose any risk to the users or the platform itself. The platform uses the vulnerability scanning tool WIZ to scan all images before they are internalized, and if any critical vulnerabilities are found, the image will be rejected and the user will be notified of the issues that need to be fixed before the image can be internalized.

BMW has already in place a platform to help developers to scan their images for vulnerabilities before pushing them to the CAIP platform:

WIZ platform also provides a CLI tool that can be used to scan images locally before pushing them to the CAIP platform, which is recommended to avoid delays in the internalization process.

You can use the following command to scan your image locally with WIZ CLI:

wizcli docker scan -i <image-name>

Note

The process of image internalization will fail if any critical vulnerability is found on the image. Users must fix the vulnerabilities and push a new version of the image to be able to internalize it into the platform. It is recommended to scan the images locally before pushing them to the platform to avoid delays in the internalization process.

Conclusion

By following these guidelines, users can ensure that their images are compliant with the CAIP platform and can be internalized and used in their applications and deployments without any issues.