Skip to main content

Optimize Container Images

CloudBase Cloud Hosting supports hosting any image, but we recommend that you optimize your images as much as possible to achieve faster startup speed, faster build speed, smaller container size, and better service performance.

The following are some recommended practices:

Select Smaller Base Images

Runtime containers for the same language can vary significantly in size depending on the base image chosen. For most business scenarios, we recommend using smaller base images.

Take Node.js as an example. The official image repository provides the following base images of different sizes:

REPOSITORY      TAG              IMAGE ID         CREATED          SIZE
node 15 ca36fba5ad66 2 days ago 941MB
node 15-slim 922b09b21278 2 days ago 165MB
node 15-buster 36754275e286 2 days ago 910MB
node 15-buster-slim eda2c7e487ff 2 days ago 179MB
node 15-alpine 1e8b781248bb 2 days ago 115MB

Note that the 15-slim, 15-buster-slim, and 15-alpine images have significantly smaller sizes because they use trimmed underlying operating system images. Using these trimmed and optimized images generally does not affect your service operation.

Reduce Image Layers

Each instruction in a Dockerfile generates a layer, and the number of layers in an image directly affects the image's size.

We recommend that you combine multiple instructions to reduce the number of layers. For example, you can merge the following instructions into one:

RUN cd my-app
RUN make
RUN make install
RUN rm -rf /tmp

After merging:

RUN cd my-app && \
make && \
make install && \
rm -rf /tmp

Leverage Layer Caching

Docker caches each layer of the image separately. If the layer content remains unchanged, it directly uses the previous cache to accelerate building and pushing the image. To maximize the reuse of this mechanism, we recommend that you isolate layers with infrequent changes.

For example, your application may contain many dependencies. Normally, these dependencies change little during the development process. Therefore, it is best to make a copy of dependencies as a separate layer instruction:

COPY ./deps deps
RUN make && make install

The above approach isolates the COPY statement. Each time the image is built and pushed, as long as the content of dependency files has not changed, the previous cache can be reused to improve building and pushing speeds.

Clean Up Unnecessary Files

You can use .dockerignore to exclude files during container builds, reducing the import of unnecessary files while enhancing security by preventing local sensitive files from being packaged into the image.

Multi-stage Build

For a Java service, only the final compiled jar file is needed during actual running, while the dependencies, expansion packages, and source code files used during the building phase are unnecessary.

At this point, we can use multi-stage builds to separate the build environment from the runtime environment, thereby reducing the size of the runtime container. For example:

# Use the official maven/Java 8 image as the building environment.
# https://hub.docker.com/_/maven
FROM maven:3.6-jdk-11 as builder

# Copy the code into the container.
WORKDIR /app
COPY pom.xml .
COPY src ./src

# Build the project.
RUN mvn package -DskipTests

# Use AdoptOpenJDK as the base image.
# https://hub.docker.com/r/adoptopenjdk/openjdk8
# https://docs.docker.com/develop/develop-images/multistage-build/#use-multi-stage-builds
FROM adoptopenjdk/openjdk11:alpine-slim

# Place the jar into the container.
COPY --from=builder /app/target/helloworld-*.jar /helloworld.jar

# Start the service.
CMD ["java", "-Djava.security.egd=file:/dev/./urandom", "-jar", "/helloworld.jar"]