Build slim Docker images for Dart apps

Tony Pujals
Google Cloud - Community
4 min readOct 26, 2020

If you have used Docker to run your Dart app in a containerized environment (such as Google Kubernetes Engine or Cloud Run), you may have been surprised at the heft of the google/dart image.

Based on a Debian 10 “Buster” image, google/dart is oriented for development — not optimized for size — and currently weighs in at a substantial 682 MB!

google/dart image size

As Flutter, Google’s UI toolkit for building natively compiled applications for mobile, web, and desktop from a single codebase based on Dart continues to explode in popularity, interest in using Dart for implementing backend functionality has increased as well.

However, because of the focus on client-side development, there has been little guidance on how to optimize image size for server-side deployment.

In this article, I’ll introduce you to dart-scratch, a lightweight image based on scratch that you can use to build the slimmest possible image for your Dart app containers.

So why is optimizing image size important?

When you create a container on some (physical or virtual) machine for the first time, the image used to create the container must travel over the network from an image repository somewhere (usually a registry like Docker Hub or Google Artifact Registry, for example).

There is a delay in transferring large images that might not be an issue for long-running servers, but the performance tax in transferring large images in a dynamically scaling environment that creates new machine instances on demand can be detrimental.

Then, when the container is being constructed, the image layers are read from the machine’s image cache. Larger files obviously take more time to read, which means a longer delay until the container is ready to do its work.

Again, for long-running servers, this may not be an important issue, but for apps that need to respond quickly, delays in responsiveness add up and may be perceptible to users. Eliminating these delays are especially critical for scale-to-zero workloads that don’t consume resources (thus reducing costs), but must start and execute their tasks quickly.

JIT vs AOT performance

Finally, one more thing that adversely affects startup time is that Dart code is compiled just-in-time (JIT) when it runs on the Dart VM. Starting a server can take a number of seconds before it’s ready to begin listening on a socket.

As mentioned previously, this may not be an issue for some apps, but this can be highly undesirable in a dynamic, auto-scaling environment where responsiveness is important.

To get around this problem, a Dart app can be compiled ahead-of-time (AOT) into machine code using dart2native. This provides a dramatically improved startup time.

Unfortunately, there is one catch. Currently dart2native doesn’t support reflection (which relies on dart:mirrors). Metadata annotations tend to be popular in various web server, data modeling, serialization, and dependency injection frameworks and libraries, for example.

For the types of shorter, more narrow-focused apps that representative of typical Function-as-a-Service workloads, this may not be an issue at all. For larger, more complex types of web applications that need to leverage sophisticated frameworks, annotation support might be indispensable to you.

See this FAQ and docs page if you want more detail about JIT vs AOT performance and limitations.

Use dart-scratch for slim AOT- or JIT-based images

If you want the fastest possible application launch and you don’t require Dart reflection (for annotation support, for example), then use dart-scratch as shown below. This will build your app using dart2native.

A minimal server app image will be around 25 MB and when a container is created the app will launch in sub-second time. This is well-suited for Function-as-a-Service and other types of jobs which need to execute and scale quickly.

Multi-stage Dockerfile to build a slim image with AOT-compiled app:

FROM google/dart
# uncomment next line to ensure latest Dart and root CA bundle
#RUN apt -y update && apt -y upgrade
WORKDIR /app
COPY pubspec.* .
RUN pub get
COPY . .
RUN pub get --offline
RUN dart2native /app/bin/server.dart -o /app/bin/server
FROM subfuzion/dart-scratch
COPY --from=0 /app/bin/server /app/bin/server
# COPY any other directories or files you may require at runtime, ex:
#COPY --from=0 /app/static/ /app/static/
EXPOSE 8080
ENTRYPOINT ["/app/bin/server"]

As explained previously, if your app depends on dart:mirrors, then you can't use dart2native, so you will need to build an image that uses the Dart VM.

A minimal server app image will be around 50 MB and when a container is created the app can take a number of seconds before it’s ready to listen on a socket. Due to the increased start time this is better suited for longer-lived app, apps that are less sensitive to job launch time, or applications that require reflection support (for annotations, for example).

Multi-stage Dockerfile to build a slim image with JIT-compiled app:

FROM google/dart
# uncomment next line to ensure latest Dart and root CA bundle
#RUN apt -y update && apt -y upgrade
WORKDIR /app
COPY pubspec.* .
RUN pub get
COPY . .
RUN pub get --offline
FROM subfuzion/dart-scratch
COPY --from=0 /usr/lib/dart/bin/dart /usr/lib/dart/bin/dart
COPY --from=0 /root/.pub-cache /root/.pub-cache
# Copy over the entire app...
COPY --from=0 /app /app
# ...or copy specific files and directories you require at runtime, ex:
#COPY --from=0 /app/bin/server.dart /app/bin/server.dart
#COPY --from=0 /app/lib/ /app/lib/
#COPY --from=0 /app/static/ /app/static/
EXPOSE 8080
ENTRYPOINT ["/usr/lib/dart/bin/dart", "/app/bin/server.dart"]

In conclusion, I want to mention that this is currently experimental and not supported by Google while I iterate on this. I plan to focus next on implementing a buildpack that can be used to transform Dart code into images that can run in Cloud serverless environments.

If this dart-scratch doesn’t work for you, or if you have use cases you’d like to share, please open an issue here in the repo.

--

--

Tony Pujals
Google Cloud - Community

Engineer / JavaScript advocate lead @GoogleCloud. You can follow or DM me on Twitter @tonypujals. #nodejs #deno #bunjs #dart #serverless