Base Flutter dev container image with Android SDK, Web, Java, Node and Firebase. amd64 + arm64.
783
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.9.1 (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.32.1 | Firebase project management and deployment |
| FlutterFire CLI | latest | Configure Firebase in Flutter projects |
| pnpm | 12.9.1 | 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.26.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.flutter-devcontainer/
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report.yml ← Structured bug report form
│ │ ├── feature_request.yml ← Structured feature request form
│ │ └── config.yml ← Disables blank issues, links to flutter-template
│ ├── rulesets/
│ │ ├── main-protect.json ← Importable ruleset: protects main
│ │ ├── develop-protect.json ← Importable ruleset: protects develop
│ │ └── tags-protect.json ← Importable ruleset: protects release (`flutter-*`) tags
│ ├── scripts/
│ │ ├── audit_gate.py ← CI: fails on high/critical npm advisories that are not allowlisted
│ │ ├── check-sync.sh ← CI: README versions/aliases must match the implementation
│ │ ├── check_drift.py ← Compares pinned versions with upstream (used by dependency-drift.yml)
│ │ ├── reserve-tag.sh ← Picks the next free image revision and reserves its git tag (used by docker.yml)
│ │ ├── smoke-test.sh ← Toolchain smoke test shared by ci.yml and docker.yml
│ │ └── test-reserve-tag.sh ← CI: tests reserve-tag.sh with stubbed curl and gh
│ ├── workflows/
│ │ ├── ci.yml ← PR validation: lint, format, docs sync, build + smoke test + scan, npm audit → "CI passed"
│ │ ├── dependency-drift.yml ← Weekly: reports pinned tool versions that are behind upstream
│ │ ├── docker.yml ← Builds natively per arch, tests, pushes, signs (push to main when the image changes, or manual)
│ │ ├── dockerhub-description.yml ← Syncs README.md to Docker Hub on push to main
│ │ ├── labels.yml ← Syncs labels.yml to GitHub labels
│ │ ├── pr-labels.yml ← Adds labels to a pull request from its Conventional Commit title
│ │ └── release.yml ← Reusable: publishes the GitHub Release named after the image (called by docker.yml)
│ ├── CODE_OF_CONDUCT.md ← Contributor Covenant 2.1
│ ├── CODEOWNERS ← Auto-requests reviewer on every PR
│ ├── CONTRIBUTING.md ← Branching, commit and change guidelines
│ ├── grype.yaml ← Image-scan exceptions for components that cannot be fixed here, each with its reason
│ ├── npm-audit-allowlist.txt ← Accepted high/critical advisories, each with its reason
│ ├── PULL_REQUEST_TEMPLATE.md ← PR checklist (versions, platforms, scope)
│ ├── dependabot.yml ← Weekly grouped updates for GitHub Actions + Docker base image → develop
│ ├── renovate.json ← Weekly PRs for the pins Dependabot cannot see (firebase-tools, pnpm, node digest) → develop
│ ├── labels.yml ← Label definitions — name, color, description
│ └── release.yml ← Release-notes categories (by PR label)
├── docker/
│ ├── Dockerfile.dev ← The image recipe ← MAIN FILE
│ └── firebase-tools-overrides.json ← npm overrides that patch firebase-tools' transitive dependencies
├── docs/
│ └── github-setup.md ← Setup and recovery guide: GitHub, Docker Hub and Renovate settings, rulesets, bootstrap order
├── scripts/
│ └── shell_setup.sh ← Installs Starship (pinned + verified) and bakes aliases into the image
├── .dockerignore ← Allowlist: only what the Dockerfile COPYs enters the build context
├── .editorconfig ← Consistent indentation/line endings across editors
├── .gitattributes ← Forces LF line endings in every checkout
├── .gitignore ← Ensures secrets are never committed
├── .hadolint.yaml ← Dockerfile lint rules used by CI
├── LICENSE ← MIT — free to use, your name stays on it
├── README.md ← This file — also synced to Docker Hub
├── SECURITY.md ← Vulnerability reporting policy
└── repomix.config.json ← Repomix config for generating a single-file repository snapshot
| File | Trigger | What happens |
|---|---|---|
workflows/ci.yml | PR targeting develop or main | Lint, format, docs sync, build + smoke test + vulnerability scan (amd64 + arm64), npm audit, then the aggregate CI passed check |
workflows/ci.yml | Manual dispatch | Same checks on the selected branch |
workflows/docker.yml | Push to main (docker/, scripts/, .dockerignore or docker.yml changed) | Builds each architecture natively, smoke-tests, pushes :latest and a permanent :flutter-X.Y.Z.R tag, and signs the image |
workflows/docker.yml | Manual dispatch (from main) | Same pipeline, with force-rebuild and push toggle |
workflows/dependency-drift.yml | Every Monday 06:00 UTC / manual | Opens, updates or closes one issue listing pinned tool versions that are behind upstream |
workflows/dockerhub-description.yml | Push to main (README.md changed) | Updates Docker Hub description |
workflows/dockerhub-description.yml | Manual dispatch (from main) | Forces immediate Docker Hub sync |
workflows/release.yml | Called by docker.yml after a publish from main | Publishes a GitHub Release named after the image (flutter-X.Y.Z.R) with generated notes |
workflows/labels.yml | Push to main (.github/labels.yml changed) | Syncs all labels to GitHub |
workflows/labels.yml | Manual dispatch | Bootstrap all labels in one go |
workflows/pr-labels.yml | Pull request opened, edited or reopened (into develop or main) | Adds labels from the Conventional Commit title so the release notes are grouped without manual work |
dependabot.yml | Every Monday 09:00 UTC | Scans GitHub Actions and the Docker base image, opens grouped PRs against develop |
renovate.json | Before 09:00 UTC on Mondays | Renovate opens one PR per bump of firebase-tools, pnpm or the node base digest (Dockerfile and README together) against develop |
Every third-party action is pinned to a full commit SHA (with a # vX.Y.Z comment) and every workflow declares least-privilege permissions:. Repository settings, rulesets and the branch workflow are documented in docs/github-setup.md.
workflows/ci.ymlRuns on every pull request into develop or main:
main must come from develop of this repository..hadolint.yaml), ShellCheck, a syntax check of the Python helper, and actionlint..editorconfig that tooling can check reliably)..github/scripts/check-sync.sh fails when README.md and the implementation disagree: a version pinned in Dockerfile.dev that the README does not mention, a Node major that differs between the Dockerfile and the workflows, or an alias missing from (or extra in) the README tables.linux/amd64 and linux/arm64 (no push) and runs .github/scripts/smoke-test.sh: Firebase CLI, pnpm and Starship versions against the pins in Dockerfile.dev (pnpm is resolved with the network disabled, proving the build-time cache works), the container user (UID/GID 1000, passwordless sudo), writable cache directories, Git's trust of /workspace, Node 24, Android platform, Chrome/Chromium, flutter doctor, the shell aliases and the Dev Container metadata label. On amd64 the image is then scanned with Grype; a critical vulnerability that already has a fix fails the check, unless .github/grype.yaml records why it is accepted (only for components that cannot be fixed from this repository, such as the Bouncy Castle library bundled inside Google's Android command-line tools; each entry names the exact version it covers, so it lapses when that component changes). Skipped when a PR touches neither docker/, scripts/, the smoke test nor the CI configuration.firebase-tools tree exactly as the image installs it (pinned version plus docker/firebase-tools-overrides.json). Any high or critical advisory fails the check unless .github/npm-audit-allowlist.txt records why it is accepted; moderate and low findings are listed for information. The result and the full report are written to the job summary.workflows/docker.ymlBuilds and publishes the multi-platform image (linux/amd64 + linux/arm64) to Docker Hub. Each architecture is built natively on its own runner (no QEMU emulation), smoke-tested with the same script the pull requests use, and only then pushed by digest. Before anything is pushed, a reserve-tag job picks the next free revision R (free on Docker Hub, and creatable as a git tag on GitHub: a name used by an immutable release stays reserved even after the release is deleted, so those numbers are skipped) and creates the git tag flutter-X.Y.Z.R with .github/scripts/reserve-tag.sh. A final job merges both digests into one manifest list, applies the tags and signs the result with a keyless Sigstore signature (see Verifying the image). Every build carries SBOM and maximum-detail provenance attestations, and the layer cache lives in buildcache-<arch> tags in the registry rather than in the size-limited GitHub Actions cache.
The push trigger is path-filtered (docker/, scripts/, .dockerignore and the workflow itself — everything the build copies or reads), so a README change never rebuilds the image. There is no schedule: the image is published only when an image change is merged to main, or when you run the workflow by hand. Pull requests are validated by ci.yml instead. The jobs use the docker-hub environment, which holds the Docker Hub credentials and is restricted to main.
Note:
IMAGE_NAMEis hardcoded (not read from a secret) so the image tag is always valid.
workflows/dependency-drift.ymlDependabot cannot see the tools pinned in Dockerfile.dev, so this workflow compares them with their official release channels every Monday and keeps one issue up to date. It covers firebase-tools, pnpm (within its pinned major line — a newer major is noted separately), Starship, the Android cmdline-tools build, the digest of the node base image, and every advisory accepted in .github/npm-audit-allowlist.txt (reported as soon as a patched release exists). The issue closes itself when every pin is current. Renovate normally opens the PR for firebase-tools, pnpm and the node digest; the issue is the safety net for those and the only prompt for Starship and the cmdline-tools build, whose downloads are checksum-verified and so need a deliberate bump. Bump what it lists as described in Updating the Image.
workflows/dockerhub-description.ymlSyncs README.md to the Docker Hub repository description page on every README.md change merged to main.
workflows/release.ymlA reusable workflow that docker.yml calls once the image is on Docker Hub. The release carries the same name as the image it describes: the release tag, the git tag and the Docker tag are all flutter-X.Y.Z.R (for example flutter-3.47.6.3), and the release is created from the git tag that docker.yml reserved before publishing. The notes show the docker pull command and the digest. A release is created for every push to main that changes the image (same path filter as docker.yml, so a documentation-only promotion produces none) and for the first image of each new Flutter release (for example from a manual run); other rebuilds only add an image revision. Notes are generated from the merged pull requests and grouped by label (.github/release.yml).
workflows/pr-labels.ymlAdds labels to a pull request from its title, so the generated release notes (grouped by label in .github/release.yml) never depend on someone remembering to set them. feat adds feature, fix adds bug, docs adds documentation, ci adds ci, chore/refactor/style/test/perf add chore; the scope deps adds dependencies and docker adds docker; a ! after the type, or a BREAKING CHANGE: line in the description, adds breaking change; and a develop → main promotion gets skip-changelog, because the pull requests it contains are already listed. The workflow only adds labels, so labels set by hand are kept, and a title that is not a Conventional Commit gets a warning instead of a label. It skips bot pull requests (Dependabot and Renovate configure their own labels) and pull requests from forks, which receive a read-only token. The title and description reach the script only through environment variables, and the job has pull-requests: write and nothing else. It is deliberately not part of CI passed: it validates nothing.
workflows/labels.ymlKeeps GitHub repository labels in sync with .github/labels.yml. Labels are version-controlled — add or rename a label in the file, push, and GitHub reflects the change automatically. Run manually to bootstrap on a new r
Content type
Image
Digest
sha256:4bd7f4f74…
Size
2.2 GB
Last updated
14 days ago
docker pull alihaidar199527/flutter-devcontainer