In the previous post we saw how to obfuscate Python code using Nuitka.

In this post, we’ll look at how you can build a Docker image containing either plain Python code or obfuscated Python code out of a single Dockerfile.

This utilizes something called Docker multi-stage builds.

Using Docker multi-stage builds, you can avoid having to maintain multiple Dockerfiles. For example, in this case, you needn’t maintain one Dockerfile to build a Docker image with plain Python code and another to build a Docker image with obfuscated Python code.

A similar multi-stage Dockerfile can be used to generate slightly different images for multiple environments like dev, qa, staging, production, etc.

# Set --build-arg SRC_FORMAT=clear|obfuscated during docker build.
ARG SRC_FORMAT=obfuscated

FROM python:3.6.8-stretch AS base
WORKDIR /usr/src/app

FROM base AS clear-code
COPY . .

FROM base AS obfuscated-code
COPY . .
RUN pip install cython && pip install nuitka==0.6.3
RUN ["./code_obfuscation.py"]
RUN ["rm", "code_obfuscation.py"]

# Cannot reference build arg in COPY --from. Hence creating an intermediate image.
FROM ${SRC_FORMAT}-code AS copy-src

FROM base AS final
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY --from=copy-src /usr/src/app/ .
ENTRYPOINT ["./entrypoint.sh"]


Let us walk through the Dockerfile since there seems to be a lot happening here.


The first line is a build argument to docker build to control whether the final Docker image should have plain Python code or obfuscated Python code.

  • To build a Docker image with plain Python code

      $ docker build --build-arg SRC_FORMAT=clear ...
    
  • To build a Docker image with obfuscated Python code

      $ docker build --build-arg SRC_FORMAT=obfuscated ...
    


Then next step is to create a base image with no Python code files whatsoever.

FROM python:3.6.8-stretch AS base
WORKDIR /usr/src/app

This will form the base for all the subsequent build stages.


Next, we have two build stages - one for the plain Python code and one for the obfuscated Python code.

The build stage for the plain Python code (clear-code) does nothing other than copy the Python files from the build context directly into the Docker image.

FROM base AS clear-code
COPY . .

The build stage for the obfuscated Python code (obfuscated-code) runs the code obfuscation script we saw in the previous post

FROM base AS obfuscated-code
COPY . .
RUN pip install cython && pip install nuitka==0.6.3
RUN ["./code_obfuscation.py"]
RUN ["rm", "code_obfuscation.py"]

One very important thing to notice here is that, since the code obfuscation was done in separate RUN statements, the intermediate layers will still contain plain Python code. However, that is not a problem, since obfuscated-code is not the final image we are going to ship to the customer.


Now that we have two intermediate docker images - clear-code and obfuscated-code - containing plain Python code and obfuscated Python code respectively, we can not build the final stage - final.

FROM base AS final
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY --from=copy-src /usr/src/app/ .
ENTRYPOINT ["./entrypoint.sh"]

This final stage will simply copy files from the previous build stage depending on whether we want plain Python code in our final image in which case we copy files from clear-code or whether we want obfuscated Python code in which case we copy files from obfuscated-code.

However, there is one small problem. The COPY --from= statement does not accept build arguments. So, we have to create another intermediate image before building the final image.

# Cannot reference build arg in COPY --from. Hence creating an intermediate image.
FROM ${SRC_FORMAT}-code AS copy-src


Once all the build stages are complete, you will have a final image which contains either plain Python code or obfuscated Python code depending on the --build-arg you specified during docker build.