Single entrypoint for pipeline steps in a docker image - docker

I have a docker image which encapsulates some processing steps: A, B, C with a linear dependency: A -> B -> C. Each step produces some artifacts (files) that will be required for subsequent steps.
What is a robust way of running this pipeline given these constraints?
A simple idea is to write a shell script, running each step like:
# run.sh
python step_a.py [args]
python step_b.py [args]
./step_c [args]
and define run.sh as the ENTRYPOINT of the docker image.
Would this be good-enough? What are some potential caveats? Is there a better approach?
I would have preferred something like docker-compose, but even with depends_on, it's not guaranteed that subsequent steps will run only after former steps are finished.

I think the most robust way to do this with a dockerfile would be to use multi-stage builds.
At its core, multi-stage builds just break up the docker file into multiple smaller images that you can control more granularly; so for your use case, you would have a stage for each part. Then you can copy the artifacts you need between stages. Finally, since you want an output and not a container, you would make the entry point the Rust binary and then have that spit out whatever you need. This would look a little something like this
FROM python-3.8:latest AS stage-1 // this can be whatever image you want
RUN pip install requirements_1.txt // install the reqs for the first python file
RUN python_file_1.py
FROM python-3.8:latest AS stage-2 // again, whatever image you want
RUN pip install requirements_2.txt // same idea
COPY --from=stage-1 ./artifact_1 ./destination // this copies the artifact from running python_file_1.py to somewhere you want it to be. The paths here are obviously placeholders
RUN python_file_2.py
FROM rust:1.31
COPY --from=stage-2 ./artifact_2 ./destination
ENTRYPOINT ["./rust_binary"]
Basic gist -
Make some python image, install prereqs for first python file
Run first python file
Makes some python image, install prereqs for second python file
Copy needed artifact from first stage to current (second) stage
Run second python file
Makes some rust image, install anything needed
Copy needed artifact from second stage to current (third) stage
Entrypoint into the rust binary, which should produce your output

Related

Super newbie multi-stage docker image build question

I need to build custom image which contains both terraform and the gcloud CLI. Very new to docker so I'm struggling with this even though it seems very straight forward. I need to make a multi stage image from the following two images:
google/cloud-sdk:slim
hashicorp/terraform:light
How can I copy the terraform binary from the hashicorp/terraform:light image to the google/cloud-sdk:slim image? Any fumbling I've done so far has given me countless errors. Just hoping somebody could give me an example of what this should look like because this is clearly not it:
FROM hashicorp/terraform:light AS builder
FROM google/cloud-sdk:slim
COPY --from=builder /usr/bin/env/terraform ./
Thanks!
That's not really the purpose of multistaging. For your case, you would want to pick either image and install the other tool, instead of copying from one to another.
Multistage is meant when you want to build an app but you don't want to add building dependencies to the final image in order to reduce the image size and reduce the attack surface.
So, for example, you could have a Go app and you would have two stages:
The first stage would build the binary, downloading all the required dependencies.
The second stage would copy the binary from the first stage, and that's it.

Copy artifacts from a docker image build stage

I have a Dockerfile that produces an image as a result of a multi-stage build. One of the steps produces a file (an sql migration script) that I would like to export and store somewhere outside of the build process, while I still want the build to produce the final image.
I was looking at the approach explained here How to copy files from host to Docker container?. It works well, but there is a couple of problems with it:
It either produces the image or output the files.
It only exports the files from the last stage. To limit the number of exported files, I can to use the scratch image, but that is not the final image I want to produce.
FROM scratch AS export
COPY --from=build /script.sql /
If I just copy the sql script into the finally produced (production) image, it will output all the build-produced files. And I also don't really want the script to be in the final image as it has no purpose there.
Is there any way how to do it? Feels silly to run two separated Dockerfiles to do the same build, one to generate the script and another to produce the image.

Re-use Dockerfile with different base image

I have a Dockerfile that currently uses the node:10.21.0-buster-slim as its base. That works well for running in production since I get a nice small image (I'd rather not use Alpine since I've had issues with this code on Alpine previously and I'm already stuck running a MySQL image based on buster-slim in production). However, for development, it would obviously be nice to have an image with more tools for diagnosing issues that crop up (presumably based on either debian:buster or buildpack-deps:buster).
Is there some way I can run the same steps with two different base images without having to duplicate the Dockerfile contents? I assume the answer is yes with some multi-stage build magic, but I haven't figured out how that's supposed to work. In my dream world there are also a few minor differences between the dev and prod build steps (e.g. the --only=production argument to npm install, but I'm willing to sacrifice that if I have to to avoid maintaining two nearly identical Dockerfiles.
Multi-stage build magic is one way to do it:
ARG TARGET="prod"
FROM node:10.21.0-buster-slim as prod
# do stuff
FROM debian:buster as dev
# do other stuff, like apt-get install nodejs
FROM ${TARGET}
# anything in common here
Build the image with DOCKER_BUILDKIT=1 docker build --build-arg 'TARGET=dev' [...] to get the development-specific stuff. Build image with DOCKER_BUILDKIT=1 docker build [...] to get the existing "prod" stuff. Switch out the value in the first ARG line to change the default behavior if the --build-arg flag is omitted.
Using the DOCKER_BUILDKIT=1 environment flag is important; if you leave it out, builds will always do all three stages. This becomes a much bigger problem the more phases you have and the more conditional stuff you do. When you include it, the build executes the last stage in the file, and only the previous stages that are necessary to complete the multi-stage build. Meaning, for TARGET=prod, the dev stage never executes, and vice versa.

Docker multistage build without copying from previous image?

does it have any advantages to use a multistage build in Docker, if you don't copy any files from the previously built image?
eg.
FROM some_base_image as base
#Some random commands
RUN mkdir /app
RUN mkdir /app2
RUN mkdir /app3
#ETC
#Second stage starts from first stage
FROM base
#Add some files to image
COPY foo.txt /app
Does this result in a smaller image or offer any other advantages compared to a non multi-stage version? Or are multi stage builds only useful for preparing some files and then copying those into another base image?
Or are multi stage builds only useful for preparing some files and then copying those into another base image?
This is the main use-case discussed in "Use multi-stage builds"
The main goal is to reduce the number of layers by copying files from one image to another, without including the build environment needed to produce said files.
But, another goal could be not rebuild the entire Dockerfile including every stage.
Then your suggestion (not copying) could still apply.
You can specify a target build stage. The following command assumes you are using the previous Dockerfile but stops at the stage named builder:
$ docker build --target builder -t alexellis2/href-counter:latest .
A few scenarios where this might be very powerful are:
Debugging a specific build stage
Using a debug stage with all debugging symbols or tools enabled, and a lean production stage
Using a testing stage in which your app gets populated with test data, but building for production using a different stage which uses real data

What is a Docker build stage?

As far as I understand build stages in Docker are fundamental things, and I have a practical understanding of them but I have trouble coming up with a proper definition, and I also can't seem to find one.
So: what is the definition of a Docker build stage?
Edit: I'm not asking "how do I use a build stage?" or "how can I use multi-build stages?" which people seem very eager to answer :-)
The reason I have this question is because I saw the following sentences in the docs:
"The FROM instruction initializes a new build stage"
"a name can be given to a new build stage"
Which left me wondering: what exactly is a build stage?
I don't think there will ever be a strict definition for Docker build stage because a build stage is in general something theoretical which:
can be defined by you
depends on your case (language / libraries)
In this question: Difference between build and deploy? one of the answers says...
Build means to Compile the project.
I think you can see it this way too. A build stage is any procedure that generates something which can later be taken and used.
The idea with docker multi-stage builds is to:
generate what you are going to need
leave behind what you don't need and use the product of step 1 in a more lightweight way
If you have read the docs, Alex Ellis has a nice example where the same logic takes place:
he starts with a golang image, adds libraries, builds his app (Go generates a binary executable file)
after that, he doesn't need golang and the libraries to ship/run it so, he picks an alpine image, adds the executable file from step 1 and ships his app with an image that has much smaller size.
Since version 17, docker now supports multiple stages during a docker build executions.
This means, that you no longer need to define only one source image in your docker file and do the whole build in a single run, but you can define multiple stages with different images in your Dockerfile for each stage with multiple FROM definitions:
# Build stage
FROM microsoft/aspnetcore
# ..do a build with a dev image for creating ./app artifact
# Publish - use a hardened, production image
FROM alpine:latest
CMD ["./app"]
This gives you the benefit to break your image building process to be optimized for a task that you are doing in a stage - for example the stages could be:
use an image with extra linting dependencies to check your source
use a dev-image with all development dependencies already installed to build your source
use another image including test frameworks to run various tests on the artifacts
and once everything passed ok, use a minimal-sized, optimized, hardened image to capture the final artifacts for production
Read more in details about multistage-build:
https://docs.docker.com/develop/develop-images/multistage-build/
A stage is the creation an image. In a multi-stage build, you go through the process of creating more than one image, however you typically only tag a single one (exceptions being multiple builds, building a multi-architecture image manifest with a tool like buildx, and anything else docker releases after this answer).
Each stage, building a distinct image, starts from a FROM line in the Dockerfile. One stage doesn't inherit anything done in previous stages, it is based on its own base image. So if you have the following:
FROM alpine as stage1
RUN apk add your_tool
FROM alpine as stage2
RUN your_tool some args
you will get an error since your_tool is not installed in the second stage.
Which stage do you get as output from the build? By default the last stage, but you can change that with the docker image build --target stage1 . to build the stage with the name, stage1 in this example. The classic docker build will run from the top of the Dockerfile until if finishes the target stage. Buildkit builds a dependency graph and builds stages concurrently and only if needed, so do not depend on this ordering to control something like a testing workflow in your Dockerfile (buildkit can see if nothing in the test stage is needed in your release stage and skip building the test).
What's the value of multiple stages? Typically, its done to separate the build environment from the runtime environment. It allows you to perform the entire build inside of docker. This has two advantages.
First, you don't require an external Makefile and various compilers and other tools installed on the host to compile the binaries that then get copied into the image with a COPY line, anyone with docker can build your image.
And second, the resulting image doesn't include all the compilers or other build time tooling that isn't needed at runtime, resulting in smaller and more secure images. The typical example is a java app with maven and a full JDK to build, a runtime with just the jar file and the JRE.
If each stage makes a separate image, how do you get the jar file from the build stage to the run stage? That comes from a new option to the COPY command, --from. An oversimplified multi-stage build looks like:
FROM maven as build
COPY src /app/src
WORKDIR /app/src
RUN mvn install
FROM openjdk:jre as release
COPY --from=build /app/src/target/app.jar /app
CMD java -jar /app/app.jar
With that COPY --from=build we are able to take the artifact built in the build stage and add it to the release stage, without including anything else from that first stage (no layers of compile tools like JDK or Maven get added to our second stage).
How is the FROM x as y and the COPY --from=y /a /b working together? The FROM x as y is defining an image name for the duration of this build, in this case y. Anywhere later in the Dockerfile that you would put an image name, you can put y and you'll get the result of this stage as your input. So you could say:
FROM upstream as mybuilder
RUN apk add common_tools
FROM mybuilder as stage2
RUN some_tool arg2
FROM mybuilder as stage3
RUN some_tool arg3
FROM minimal_base as release
COPY --from=stage2 /bin2 /
COPY --from=stage3 /bin3 /
Note how stage2 and stage3 are each FROM mybuilder that is the output of the first stage.
The COPY --from=y allows you to change the context where you are copying from to be another image instead of the build context. It doesn't have to be another stage. So, for example, you could do the following to get a docker binary in your image:
FROM alpine
COPY --from=docker:stable /usr/local/bin/docker /usr/local/bin/
Further documentation on this is available at: https://docs.docker.com/develop/develop-images/multistage-build/
a build stage starts at a FROM statement and ends at the step before the next FROM statement
stage | steɪdʒ |
noun
a point, period, or step in a process or development
Take a practical example: you want to build an image which contains a production ready web server with Typescript files compiled to Javascript. You want to build that Typescript within a Docker container to simplify dependency management. So you need:
node.js
Typescript
any dependencies needed for compilation
Webpack or whatever
nginx/Apache/whatever
In your final image you only really need the compiled .js files and, say, nginx. But to get there, you need all that other stuff first. When you upload that final image, it will contain all the intermediate layers, even if they're unnecessary for the final product.
Docker build stages now allow you to actually separate those stages, or steps, into separate images, while still using just one Dockerfile and not needing to glue several Dockerfiles together with external shell scripts or such. E.g.:
FROM node as builder
RUN npm install ...
# whatever you need to build your files
FROM nginx as production
COPY --from=builder /final.js /var/www/html
The final result of this Dockerfile is a small image with nginx as its base plus just the final .js file. It does not contain all the unnecessary stuff like node.js and the npm dependencies.
builder here is the first stage, production is the second stage. In this case the first stage will be discarded at the end of the process, but you can also choose to build a specific stage using docker build --target=builder. A new FROM introduces a new, separate stage. They're essentially separate Dockerfiles, but they can share data using COPY --from.

Resources