Base Flutter dev container image with Android SDK, Web, Java, Node and Firebase. amd64 + arm64.
1.3K
Blank-canvas base Docker image for VS Code Dev Containers. One image, shared across all your Flutter projects.
Flutter (stable) · Dart · Android SDK 36 · Java 21 (Temurin) · Node.js 24 LTS · pnpm (Corepack) · Firebase CLI · FlutterFire CLI · GitHub CLI · Starship
docker pull alihaidar199527/flutter-devcontainer:latest
Every Flutter project needs the same developer tooling: Flutter SDK, Android SDK, Dart, Java, Gradle, Firebase, and a productive terminal. Configuring all of this from scratch on every machine — or worse, on every project — wastes time and produces inconsistent environments.
This repository solves that problem with a single shared base image. Merge an image change to main and every project that uses latest gets the upgrade on its next docker pull — without touching any project code. Projects pinned to a permanent tag keep their image until you bump the tag.
This repo has one job: build and publish the base development Docker image to Docker Hub.
It contains no Flutter project files, no pubspec.yaml, no application code, and no app-level CI. Flutter packages, app configuration, and deployment workflows all belong in the project repositories that consume this image.
The fastest route is the flutter-template repository, which already wires this image into a Dev Container. To use the image in your own project, point a Dev Container at it:
// .devcontainer/devcontainer.json
{
"name": "Flutter",
"image": "alihaidar199527/flutter-devcontainer:latest",
"forwardPorts": [8080]
}
The image's metadata supplies the developer user, the Dart and Flutter extensions and the Flutter SDK path, so nothing else is required. Port 8080 is where frunw serves the web target. latest always moves to the newest build; a project that must keep building on the same image pins a permanent tag instead (flutter-X.Y.Z.R or a digest), for example "image": "alihaidar199527/flutter-devcontainer:flutter-3.47.6.1".
Without Dev Containers, run any tool straight from the image:
docker run --rm -it -v "$PWD:/workspace" alihaidar199527/flutter-devcontainer:latest bash
This image is one half of a two-repo system.
| Repo | Responsibility |
|---|---|
flutter-devcontainer ← you are here | Build and publish the base dev image |
flutter-template | GitHub Template — the starting point for every new Flutter project |
When you open a Flutter project that uses this image, the container starts via Docker Compose with your project folder mounted at /workspace and your Git identity and SSH keys available for pushing to GitHub.
The container user is developer with UID/GID 1000 (the base image's node account, renamed), which matches the first user on most Linux hosts, so files created in /workspace belong to you on the host as well. On Docker Desktop (Windows/macOS) bind-mount ownership is translated by Docker and the UID does not matter. If your Linux user has a different UID, Dev Containers remaps developer to it automatically.
The image carries Dev Container metadata (devcontainer.metadata label), so a devcontainer.json or Compose service that uses it inherits remoteUser: developer, the Dart and Flutter VS Code extensions and dart.flutterSdkPath without repeating them.
Runtime defaults baked into the image, so consuming projects need no startup workarounds for them:
| Default | Effect |
|---|---|
git config --system safe.directory /workspace | Git works in the bind-mounted workspace (no "dubious ownership" error), even when ~/.gitconfig is mounted read-only |
~/.gradle, ~/.pub-cache, ~/Android, ~/.shell_history exist and belong to developer | A named volume mounted over them starts writable instead of root-owned |
CHROME_EXECUTABLE=/usr/local/bin/chrome-browser | Chromium is found by every process, including IDE-launched ones |
/etc/profile.d/10-dev-toolchain.sh | Flutter, Java, Node and the Android SDK tools stay on PATH in login shells too (docker exec -l, su -l, SSH), where Debian's /etc/profile would otherwise reset it |
ANDROID_HOME (and the deprecated ANDROID_SDK_ROOT, same value) | Android tooling finds the SDK at /home/developer/Android/sdk |
COREPACK_ENABLE_DOWNLOAD_PROMPT=0 | Corepack never waits for an interactive confirmation |
pnpm prepared for developer (Corepack cache) | pnpm install works offline when the project's packageManager matches the image's pnpm version |
Named volumes are initialised from the image only when they are first created. After pulling a new image, recreate volumes that cache image content (for example the Android SDK or pub cache) with docker compose down -v to pick the new content up.
| Tool | Version | Purpose |
|---|---|---|
| Flutter SDK | stable channel | Flutter framework + Dart SDK |
| Dart SDK | bundled with Flutter | Language runtime (included in Flutter) |
| Java (Eclipse Temurin) | 21 | Required by Android build toolchain and Gradle |
| Node.js | 24 LTS (Debian 13 trixie-slim) | Required by Firebase CLI and FlutterFire CLI |
| pnpm | 12.18.0 (via Corepack) | Package manager for repo-level tooling (Husky, commitlint) in consuming projects |
| Tool | Version | Purpose |
|---|---|---|
| Android SDK | API 36 | Latest Android platform |
| Android Build Tools | 36.0.0 | APK/AAB compilation |
| Android Platform Tools | latest | adb, fastboot |
| Android Cmdline Tools | latest (bootstrapped from build 15859902) | sdkmanager, avdmanager |
| Gradle | per project | No standalone Gradle is installed: each Flutter project's wrapper (android/gradlew) downloads the version Flutter chose into the persisted ~/.gradle cache on the first build |
| Tool | Version | Purpose |
|---|---|---|
| Chromium | Debian 13 build (amd64 and arm64) | flutter test --platform chrome, flutter build web checks, headless browser for web integration tests |
| chromedriver | matches Chromium | flutter drive -d web-server (web integration tests) |
| lcov | Debian 13 build | genhtml turns flutter test --coverage into an HTML report |
ftestc && genhtml coverage/lcov.info -o coverage/html # HTML coverage report
chromedriver --port=4444 & # web integration tests
flutter drive --driver=test_driver/integration_test.dart \
--target=integration_test/app_test.dart -d web-server --browser-name=chrome --headless
| Tool | Version | Purpose |
|---|---|---|
| Firebase CLI | 15.35.0 | Firebase project management and deployment |
| FlutterFire CLI | latest | Configure Firebase in Flutter projects |
| pnpm | 12.18.0 | Fast, disk-efficient package manager, activated via Corepack at build time (cached for the developer user — no download on first use) |
| GitHub CLI | latest | gh pr create, gh run watch, gh auth login |
| openssh-client | — | git push via SSH from inside the container |
| Starship | 1.27.0 | Terminal prompt — git branch and status, Dart and Node versions |
| Utilities | — | curl, git, jq, less, nano, htop, tree, procps, bash-completion (git and gh tab completion) |
These are intentionally absent. Add them to your pubspec.yaml per project:
provider / riverpod / bloc → flutter pub add provider
go_router → flutter pub add go_router
dio / http → flutter pub add dio
hive / isar / drift → flutter pub add hive
firebase_core → flutter pub add firebase_core
any_other_package → flutter pub add <package>
| Platform | Build target | Status |
|---|---|---|
| Android APK / AAB | flutter build apk, flutter build appbundle | ✅ Full support |
| Web | flutter build web, flutter run -d web | ✅ Full support |
| iOS | flutter build ipa | ❌ Requires macOS + Xcode — impossible in Linux containers |
| Linux Desktop | flutter build linux | ❌ Not included — a container cannot show a desktop window, so the toolchain is left out |
| macOS Desktop | flutter build macos | ❌ Requires macOS |
| Windows Desktop | flutter build windows | ❌ Requires Windows host |
The three desktop platforms are switched off in the image's Flutter configuration, so flutter create generates only the android, ios and web folders and flutter doctor reports no desktop toolchains as missing.
Apple's build toolchain (Xcode, codesign, xcodebuild) only runs on macOS by Apple's own enforcement — this is not a tooling gap. iOS is handled at the CI layer:
macos-latest GitHub Actions runner in your flutter-template CI workflow to build, sign, and upload to TestFlight automatically on every push to main.flutter doctor detects it automatically. No image changes needed.| Platform | Architecture |
|---|---|
| Windows · Linux · GCP | linux/amd64 |
| Apple Silicon Mac | linux/arm64 |
Docker pulls the correct platform automatically.
| Tag | Published when |
|---|---|
latest | Every publish — always the newest build |
flutter-X.Y.Z.R | Every publish — immutable: Flutter release X.Y.Z and image revision R (for example flutter-3.47.6.1). R starts at 1 for each Flutter release and every new build of that release takes the next free number (.2, .3, …). The tag to pin an app to; the dotted numeric form lets Dependabot in a consuming repository order and bump it |
buildcache-amd64, buildcache-arm64 | Internal layer cache for the publish workflow — not meant to be pulled |
Every publish from main for a push, or for the first image of a new Flutter release, also gets a GitHub Release with the same name as the tag.
Only latest moves. flutter-X.Y.Z.R and the image digest never change, and no published tag is ever deleted or overwritten, so an app pinned to one of them pulls the same image today and a year from now. A revision number is never reused: the publish workflow takes the next revision that is free on Docker Hub and can be created as a git tag, so revisions always increase but can skip numbers (for example after wiping Docker Hub, because GitHub keeps a tag name used by an immutable release reserved for good). Every image also has a git tag of the same name. The digest (alihaidar199527/flutter-devcontainer@sha256:…) is the strictest pin and appears in every run summary.
Which commit built an image? Every image carries it in the org.opencontainers.image.revision label, and the publish run's summary shows it next to the digest:
docker inspect --format '{{ index .Config.Labels "org.opencontainers.image.revision" }}' alihaidar199527/flutter-devcontainer:flutter-X.Y.Z.R
Choosing a tag: use latest to always get the newest Flutter and tools; use flutter-X.Y.Z.R in projects that must keep building on a known image, and bump it deliberately (the revision increases when the same Flutter release is rebuilt with operating-system or tool updates). The build date is in the image's org.opencontainers.image.created label and on Docker Hub. Keep Docker Hub tag-retention or inactive-image clean-up disabled for this repository, otherwise old tags can expire outside this pipeline.
latestIf a build reaches latest and turns out to be bad, repoint latest to the last good permanent tag. No rebuild is needed: the tag is moved on the registry, the digest (and with it the signature and attestations) is unchanged, and both architectures are kept.
# 1. find the last good build and confirm what it points to
docker buildx imagetools inspect alihaidar199527/flutter-devcontainer:flutter-X.Y.Z.R
# 2. repoint latest to it (needs push access to the Docker Hub repository)
docker buildx imagetools create \
-t alihaidar199527/flutter-devcontainer:latest \
alihaidar199527/flutter-devcontainer:flutter-X.Y.Z.R
The next publish from main moves latest forward again, so fix the cause on develop and promote it, or disable the Docker workflow until then. Projects pinned to a permanent tag or digest are not affected by any of this.
Published images are signed with Cosign using GitHub's OIDC identity (keyless), and carry SBOM and provenance attestations. To check that an image was built by this repository's publish workflow on main:
cosign verify alihaidar199527/flutter-devcontainer:latest \
--certificate-oidc-issuer https://token.actions.githubusercontent.com \
--certificate-identity-regexp '^https://github.com/alihaidar0/flutter-devcontainer/\.github/workflows/docker\.yml@refs/heads/main$'
Inspect the attestations with docker buildx imagetools inspect alihaidar199527/flutter-devcontainer:latest --format '{{ json .Provenance }}' (or .SBOM).
All aliases are defined in scripts/shell_setup.sh and baked into the image.
| Alias | Expands to | Notes |
|---|---|---|
fl | flutter | Short flutter prefix |
fget | flutter pub get | Install all dependencies |
fadd | flutter pub add | Add a package |
frm | flutter pub remove | Remove a package |
fupgrade | flutter pub upgrade | Upgrade packages |
foutdated | flutter pub outdated | Check for outdated packages |
frun | flutter run | Run on connected device |
frunw | flutter run -d web-server ... | Run web server (Docker-friendly, port 8080) |
frunc | flutter run -d chrome | Run in Chrome |
fbuild | flutter build | Build prefix |
fbuildapk | flutter build apk --release | Release APK |
fbuildaab | flutter build appbundle --release | Release App Bundle |
fbuildweb | flutter build web --release | Release web build |
ftest | flutter test | Run tests |
ftestc | flutter test --coverage | Run tests with coverage |
fanalyze | flutter analyze --fatal-infos | Static analysis (info-level lints fail, as in CI) |
fformat | dart format . | Format all Dart files |
fformatcheck | dart format --set-exit-if-changed . | Format check (CI mode) |
fdoctor | flutter doctor -v | Verbose environment check |
fclean | flutter clean | Clean build cache |
fcreate | flutter create | Create new project |
fdevices | flutter devices | List connected devices |
fupgrade_sdk | flutter upgrade | Upgrade Flutter SDK |
| Alias | Expands to |
|---|---|
dpub | dart pub |
dget | dart pub get |
dformat | dart format . |
danalyze | dart analyze |
dtest | dart test |
drun | dart run |
dcompile | dart compile |
dglobal | dart pub global |
| Alias | Expands to |
|---|---|
fblogin | firebase login |
fbdeploy | firebase deploy |
fbserve | firebase serve |
fbuse | firebase use |
fblist | firebase projects:list |
ffinit | flutterfire configure |
| Alias | Expands to |
|---|---|
adbdevices | adb devices |
adblog | adb logcat |
adbinstall | adb install |
adbrestart | adb kill-server && adb start-server |
| Alias | Expands to |
|---|---|
gs | git status |
ga | git add |
gc | git commit -m |
gp | git push |
gpl | git pull |
gl | git log --oneline --graph --decorate |
gco | git checkout |
gb | git branch |
gd | git diff |
| Alias | Expands to |
|---|---|
ll | ls -alFh --color=auto |
la | ls -A --color=auto |
cls | clear |
Symptom: denied: requested access to the resource is denied
docker-hubDOCKERHUB_USERNAME and DOCKERHUB_TOKEN are present as environment secretsSymptom: the publish job fails at the push step with denied or repository does not exist, for example after the Docker Hub repository was deleted.
Create the repository alihaidar199527/flutter-devcontainer as Public (see docs/github-setup.md), then re-run the failed jobs. After the first successful publish, run Docker Hub Description once so the overview shows the README.
docker-hubSymptom: a manual Docker or Docker Hub Description run stops with "Branch develop is not allowed to deploy to docker-hub".
The Branch dropdown of Run workflow defaults to the default branch, develop, and the environment accepts main only. Choose main and run again.
The release for a tag is created once and never replaced, so a re-run of the release job for a tag that already has a release shows this notice and does nothing. It is harmless.
The job decides the image name before anything is pushed:
Unexpected answer from Docker Hub (HTTP …): Docker Hub was unreachable or rate-limited. Re-run the failed jobs.Could not reserve flutter-… with Resource not accessible by integration: the job lacks permission to create the git tag. It requests contents: write itself; check Settings → Actions → General and any rulesets that restrict creating flutter-* tags (tags-protect must not restrict creations).No free revision: 99 revisions of one Flutter release are taken. Wait for a newer Flutter or raise MAX_REVISION in .github/scripts/reserve-tag.sh.A name that GitHub refuses because it belongs to a deleted immutable release is skipped automatically (a notice says so); it is not an error.
/workspace or in a named volumeImages published before the user-ID change ran developer as UID 1001. A named volume created with such an image stays owned by 1001, and a Linux host user with UID 1000 could not write the project files. Current images use UID/GID 1000.
id must show uid=1000(developer) gid=1000(developer). If it does not, pull the current image and rebuild the dev container.docker compose down -v, then start the container again. Only cached data is lost (pub packages, Gradle artifacts, shell history).updateRemoteUserUID is not set to false in devcontainer.json; the default remaps the container user to yours.Symptom: sha256sum: WARNING: 1 computed checksum did NOT match while building the Android or Starship layer.
A pinned download no longer matches the checksum recorded in docker/Dockerfile.dev. This is the check doing its job: either the version and checksum were bumped separately (copy both from the official source — see Updating the Image), or the upstream file changed unexpectedly, in which case do not bump the checksum until you know why.
flutter doctor shows Android licenses not acceptedInside the container, run:
yes | flutter doctor --android-licenses
This is already handled at image build time but may be needed after an sdkmanager update.
pnpm: command not found in a project's postCreateCommandThis means the image was built before Corepack/pnpm activation was added, or the container is running against a stale cached image. Pull the latest image (docker pull alihaidar199527/flutter-devcontainer:latest) and rebuild the dev container. If it's still missing, run corepack --version inside the container to confirm Corepack itself is present before filing an issue.
If pnpm starts downloading itself on first use, the image predates the build-time cache fix (pnpm is now prepared as the developer user). Pull a current image, or check with COREPACK_ENABLE_NETWORK=0 pnpm --version, which must print the pinned version.
Run the Android emulator on your host machine, then inside the container:
adb connect host.docker.internal:5555
adbdevices
If shown as unauthorized, accept the prompt on the emulator screen. If shown as offline:
adbrestart
adb connect host.docker.internal:5555
On Windows, ensure your firewall allows inbound TCP on ports 5037 and 5555 from the Docker network.
flutter run -d chrome shows no windowThe container has no display, so a browser window cannot open inside it. Use the web server target and open the page in your host browser:
frunw # flutter run -d web-server --web-port 8080 --web-hostname 0.0.0.0
Then open http://localhost:8080. Chromium is still used headlessly inside the container for flutter test --platform chrome and web integration tests.
git push fails — permission denied (publickey)ssh-add -l # check loaded keys
ssh -T [email protected] # verify auth
On Windows, ensure the SSH agent is running and your key is loaded before opening VS Code:
sc config ssh-agent start= auto
net start ssh-agent
ssh-add "$env:USERPROFILE\.ssh\id_ed25519"
Flutter SDK + Android SDK together are ~4–5 GB. Ensure Docker Desktop has at least 20 GB of disk image space allocated: Docker Desktop → Settings → Resources → Disk image size
| Repo | Purpose |
|---|---|
flutter-devcontainer | ← You are here — builds the Docker image |
flutter-template | Flutter project template — pulls this image for development |
Flutter stable · Dart · Android API 36 · Java 21 Temurin · Node.js 24 LTS · pnpm 12.18.0 · Debian 13 Trixie · 2026
The full documentation (the build pipeline, updating the image, the repository setup) is in the GitHub README.
Content type
Image
Digest
sha256:afc8b0d28…
Size
2.2 GB
Last updated
23 minutes ago
docker pull alihaidar199527/flutter-devcontainer