Docker multistage build vs. keeping artifacts in git - docker

My target container is a build environment container, so my team would build an app in a uniform environment.
This app doesn't necessarily run as a container - it runs on physical machine. The container is solely for building.
The app depends on third parties.
Some I can apt-get install with Dockerfile RUN command.
And some I must build myself because they require special building.
I was wondering which way is better.
Using multistage build seems cool; Dockerfile for example:
From ubuntu:18.04 as third_party
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
...
ADD http://.../boost.tar.gz /
RUN tar boost.tar.gz && \
... && \
make --prefix /boost_out ...
From ubuntu:18.04 as final
COPY --from=third_party /boost_out/ /usr/
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
...
CMD ["bash"]
...
Pros:
Automatically built when I build my final container
Easy to change third party version (boost in this example)
Cons
ADD command downloads ~100MB file each time, makes image build process slower
I want to use --cache-from so I would be able to cache third_party and build from different docker host machine. Meaning I need to store ~1.6GB image in a docker registry. That's pretty heavy to pull/push.
On the other hand
I could just build boost (with this third_party image) and store its artifacts on some storage, git for example. It would take ~200MB which is better than storing 1.6GB image.
Pros:
Smaller disc space
Cons:
Cumbersome build
Manually build and push artifacts to git when changing boost version.
Somehow link Docker build and git to pull newest artifacts and COPY to the final image.
In both ways I need a third_party image that uniformly and automatically builds third parties. In 1. the image bigger than 2. that will contain just build tools, and not build artifacts.
Is this the trade-off?
1. is more automatic but consumes more disk space and push/pull time,
2. is cumbersome but consumes less disk space and push/pull time?
Are there any other virtues for any of these ways?

I'd like to propose changing your first attempt to something like this:
FROM ubuntu:18.04 as third_party
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
...
RUN wget http://.../boost.tar.gz -O /boost.tar.gz && \
tar xvf boost.tar.gz && \
... && \
make --prefix /boost_out ... && \
find -name \*.o -delete && \
rm /boost.tar.gz # this is important!
From ubuntu:18.04 as final
COPY --from=third_party /boost_out/ /usr/
RUN apt-get update && apt-get install -y --no-install-recommends \
build-essential \
...
CMD ["bash"]
This way, you are paying for the download of boost only once (when building the image without a cache), and you do not pay for the storage/pull-time of the original tar-ed sources. Additionally, you should remove unneeded target files (.o?) from the build in the same step in which they are generated. Otherwise, they are stored and pulled as well.
If you are at liberty posting the whole Dockerfile, I'll gladly take a deeper look at it and give you some hints.

Related

docker ERROR: "/${DIR_NAME}" not found: not found - How to avoid huge images

I'm running into a problem with docker where I'm using some variables in the dockerfile. The variables work everywhere except in the source= argument of a bind mount.
I'm installing a large (40+ GB) software package. I have three different versions to work with so I made a variable to identify the version directory. I'm using a bind mount to avoid sending the installation source to the image.
What is the best way to debug? --progress=plain and --no-cache don't really show much more on the error message.
Is there a better way to do this whole thing? I don't want to create 150 GB images if I can avoid that. If I have all three versions of software in one directory that could be a long build and a big image since I need to select the version at build time. I guess I may need a more involved build process than allowed here--what would that look like?
How do I get around this variable issue?
The command that causes the problem is here:
ARG SOFTWARE_DIR=MySoftwareDirectory #this lives in the same directory as the *.docker
ARG SOFTWARE_VER=2022.3
FROM centos:latest AS base
RUN yum update -y \
&& yum upgrade -y \
&& yum install -y libXext libXrender libXtst \
&& yum clean all
RUN mkdir /tools
FROM base as final
ARG SOFTWARE_VER
ARG SOFTWARE_DIR
COPY ./install_config_${SOFTWARE_VER}.txt /tools/install_config.txt
RUN --mount=type=bind,target=/mnt/${SOFTWARE_DIR},readonly,source=${SOFTWARE_DIR} \
/mnt/${SOFTWARE_DIR}/setup --batch
ENTRYPOINT ["/tools/software/${SOFTWARE_VER}/bin/software"]
The error is:
ERROR: "/${SOFTWARE_DIR}" not found: not found
...
failed to computer cache key: "/${SOFTWARE_DIR}" not found: not found
This works:
ARG SOFTWARE_DIR=MySoftwareDirectory #this lives in the same directory as the *.docker
ARG SOFTWARE_VER=2022.3
FROM centos:latest AS base
RUN yum update -y \
&& yum upgrade -y \
&& yum install -y libXext libXrender libXtst \
&& yum clean all
RUN mkdir /tools
FROM base as final
ARG SOFTWARE_VER
ARG SOFTWARE_DIR
COPY ./install_config_${SOFTWARE_VER}.txt /tools/install_config.txt
RUN --mount=type=bind,target=/mnt/${SOFTWARE_DIR},readonly,source=MySoftwareDirectory \
/mnt/${SOFTWARE_DIR}/setup --batch
ENTRYPOINT ["/tools/software/${SOFTWARE_VER}/bin/software"]
I am building with:
DOCKER_BUILDKIT=1 docker build -t software -f software.docker .
My directory structure looks like this:
software.docker
MySoftwareDirectory
I did chmod 777 on MySoftwareDirectory to correct permissions issue. (I know not the best way)
Docker version is 20.10.21. Host is CentOS 7 fully patched.

Docker: should I combine my apt-get install / build / cleanup steps into one big RUN?

I have a Dockerfile that looks like this:
FROM debian:stable-slim
RUN apt-get update && \
apt-get install -y --no-install-recommends fp-compiler fp-units-fcl fp-units-net libc6-dev
COPY src /whatwg/wattsi/src
WORKDIR /whatwg/wattsi/src
RUN ./build.sh
RUN rm -rf /whatwg/wattsi/src && \
apt-get purge -y fp-compiler fp-units-fcl fp-units-net libc6-dev && \
apt-get autoremove -y
ENTRYPOINT ["/whatwg/wattsi/bin/wattsi"]
As you can see, there are three separate RUN steps: one to install dependencies, one to build, and one to cleanup after building.
I've been poking around to try to figure out why the resulting image is relatively large, and it seems like it's because, even though I do a cleanup step, a layer is retained containing all the installed dependencies.
Should I restructure my Dockerfile like so?
FROM debian:stable-slim
COPY src /whatwg/wattsi/src
WORKDIR /whatwg/wattsi/src
RUN apt-get update && \
apt-get install -y --no-install-recommends fp-compiler fp-units-fcl fp-units-net libc6-dev && \
./build.sh && \
rm -rf /whatwg/wattsi/src && \
apt-get purge -y fp-compiler fp-units-fcl fp-units-net libc6-dev && \
apt-get autoremove -y
ENTRYPOINT ["/whatwg/wattsi/bin/wattsi"]
This feels a bit "squashed", and I can't find any documentation explicitly recommending it. All the documentation that says "minimize RUN commands" seems to focus on not doing multiple apt-get steps; it doesn't talk about squashing everything into one. But maybe it's the right thing to do?
Each layer in a Docker image is like a commit in version control, in can't change previous layers just like deleting a file in Git won't remove it from from history. So deleting a file from a previous layer doesn't make the image smaller.
Since layers are created at the end of RUN, doing what you're doing is indeed one way to make smaller images. The other, as someone mentioned, is multi-stage builds.
The downside of the single RUN variant is that you have to rerun the whole thing every time source code changes. So you need to apt-get all those packages each time instead of relying on Docker's build caching (I wrote a thing explaining the caching here: https://pythonspeed.com/articles/docker-caching-model/).
So multi-stage lets you have both faster builds via caching, and small images. But it's complicated to get right, what you did is simpler and easier.

Up to date (lts-13.0) minimal base docker image for haskell/stack?

I'm would like to deploy my haskell application on docker and the base image fco/stack-build that I've found takes 9GB ! Do you know a base image more minimal than that ??
stack-build is as large as it is, because it contains the required system dependencies of all packages on Stackage.
I am using the following base image for building and deploying:
FROM ubuntu:18.04
RUN apt-get update
# Build dependencies
RUN apt-get install --assume-yes curl
RUN curl -sSL https://get.haskellstack.org/ | sh
RUN apt-get install --assume-yes libtinfo-dev
# Without this haddock crashes for modules containing
# non-ASCII characters.
ENV LANG C.UTF-8
It's not really minimal if you just want to use the image during runtime as you wouldn't need stack in that case.
Firstly, that image might only be necessary to build the excutable, once you have it built you could use a multistage docker build or just copy the executable directly to a more slim image.
The dockerfile is available here: https://github.com/commercialhaskell/stack/blob/master/etc/dockerfiles/stack-build/lts-13.0/Dockerfile
You could remove these commands (which probably add up to the bulk of the size):
# Use Stackage's debian-bootstrap.sh script to install system libraries and
# tools required to build any Stackage package.
#
RUN apt-get update && \
apt-get install -y wget && \
wget -qO- https://raw.githubusercontent.com/fpco/stackage/$BOOTSTRAP_COMMIT/debian-bootstrap.sh | bash && \
rm -rf /var/lib/apt/lists/*

How to create the smallest possible Docker image after installing apt dependencies

I've created a Docker image using debian as the parent image. In my Dockerfile I've installed some dependencies using apt and pip.
Now, I want to get rid off everything that is not completely necessary to run my app, which of course, needs the dependencies installed.
For now I have the following lines in my Dockerfile after installing the dependencies.
RUN rm -rf /var/lib/apt/lists/* \
&& rm -Rf /usr/share/doc && rm -Rf /usr/share/man \
&& apt-get clean
I've also installed the dependencies using the --no-install-recommends option.
Anything else I can do to reduce the footprint of my Docker image?
PS: just in case, this is how I installed the dependencies:
RUN apt-get update \
&& apt-get install -y --no-install-recommends \
sudo systemd \
build-essential libffi-dev libssl-dev \
python-pip python-dev python-setuptools python-wheel
To reduce the size of the image, you need to combine your RUN commands into one. When you create files in one layer and delete them in another, the files still exist on the drive and are shipped over the network. Their existence is just hidden when the layers of the filesystem are assembled for your container.
The Dockerfile best practices explain this in more detail: https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#run
I'd also recommend building with docker build --rm=false --no-cache . (temporarily) and then reviewing the output of docker diff on each of the created images to see what files are created in each layer.

docker-compose update from S3 bucket

Our Dockerfile invokes a python script which copies a binary from S3 to /usr/bin. This works fine the first time. But from then on "docker-compose build" does nothing because everything is cached. This is a problem if the binary has changed.
Short of building with --no-cache, what is the best way to make sure "docker-compose build" will always pick up the new binary if there is one. We don't mind if it unnecessarily downloads the binary even if unchanged, so long as it does work then the binary has changed.
Seems like we want a Dockerfile step that always executes?
FROM ubuntu:trusty
RUN apt-get update
RUN apt-get -y install software-properties-common
RUN apt-get -y install --reinstall ca-certificates
RUN add-apt-repository ppa:fkrull/deadsnakes
RUN apt-get update && apt-get install -y \
curl \
wget \
vim \
git \
python3.5 \
python3-pip \
python3-setuptools \
libpcap0.8-dev
RUN ln -sf /usr/bin/python3.5 /usr/bin/python3
ADD . /app
WORKDIR /app
# Install Python Requirements
RUN pip3 install -r etc/python/requirements.txt
# Download/Install processor and associated libs
RUN python3 setup_processor.py
RUN mkdir -p /logs
ENTRYPOINT ["/app/entrypoint.sh"]
Where setup_processor.py downloads directly from S3 to /usr/bin.
So as of now there is no direct feature like this. But there is a workaround to your solution.
Add Build argument before your download step
ARG BUILD_ON=now
# Download/Install processor and associated libs
RUN python3 setup_processor.py
While building the image use below
docker build --build-arg BUILD_ON=$(date) ....
This will always make sure that you get a change in the ARG step and all steps cache after that will be invalidated
A feature has already been requested and being worked out on below thread
https://github.com/moby/moby/issues/1996

Resources