Migrating away from ghcr.io/cirruslabs/flutter docker images
Cirrus Labs was recently acquired by OpenAI. As a result, they will no longer be maintaining the ghcr.io/cirruslabs/android-sdk and ghcr.io/cirruslabs/flutter docker images that we’ve been using in our Gitlab CI/CD ci-flutter component. The best replacement I could find is ghcr.io/gmeligio/flutter-android, but I’m not excited about depending on an image maintained mostly by one individual for such a critical piece of our infrastructure (not least of all because of the recent supply chain debacles). This prompted me to update our ci-flutter component to build custom docker images based on the actual needs of the Flutter project being tested and built. I’m pretty happy with the result, so I thought I’d share how it works.
Custom Images #
Using the cirruslabs images as a starting point, I created a new build_images job that uses docker-in-docker to build the custom images. This is run before any of the other jobs. Given the opportunity to optimise the docker image, I decided to create three docker images, each building on the previous one:
flutter: This image is based onubuntuwith only Flutter installed extra.android-base: This image is based onflutterwith the default Android toolchain installed.android: This image is based onandroid-basewith the app’s Maven dependencies pre-downloaded and all required Android toolchain versions pre-installed.
The build_images job pushes the resulting images to the Gitlab Container Registry for use by subsequent jobs. By having three images, I updated most of the jobs to use the smaller flutter image, and I’m only using the beefy android image when needed. The Flutter version is pulled automatically from .tool-versions, so that if the app is upgraded to a new Flutter version the docker images will get automatically rebuilt. I used a multi-stage Dockerfile to make it easy to maintain these three images.
Default Android Toolchain #
One thing that I’m especially pleased about is that the android-base image includes the default Android toolchain that’s compatible with the current Flutter version. This is achieved by downloading the packages.txt file that’s published with each Flutter release and installing the Android toolchain versions that are referenced there. This file appears to be used for Flutter’s internal CI/CD. I found some scripts in gmeligio’s repo that first clued me into file. His project has some ongoing work (using Claude AI) that mentions packages.txt, but at the moment it doesn’t appear that those scripts are being used. I’m not sure why Flutter doesn’t automatically download the Android toolchain when running flutter precache --android.
Handling Dependencies #
Another thing that’s really cool is that the android image actually performs a full Android build of the Flutter app to pre-cache all of the Maven dependencies and any additional Android toolchain versions that may be required to build the dependencies. Because Flutter’s plugin system allows each library to be built with a different version of the Android SDK, it’s sometimes necessary to download several different versions of the Android SDK to build one app. While it’s a good idea to avoid this whenever possible, it’s often still required when depending on third-party packages. By doing a full app build, we ensure that as much as possible is pre-cached into the docker image to make subsequent builds as fast as possible. Of course, this makes the docker image bigger, but, in theory, this should still be faster than downloading all the dependencies one by one every time we make a build. In practice, I don’t see a dramatic increase in build speeds (since the docker images are necessarily larger), but I still appreciate having all the dependencies pre-downloaded to avoid depending on additional servers when making a build.
Importantly, the android image only contains the Maven dependencies and the Android toolchains not the app itself. This keeps the image size reasonable. This also means that as the app’s dependencies change over time, the build can simply download whatever is needed to make the build work. There’s usually no need to re-build the docker image after the first time unless the app’s dependencies significantly change.