Building a Robust Flutter CI/CD Pipeline Using Docker for Automated Deployment

Setting up a reliable CI/CD pipeline is one of the most impactful upgrades you can make to your Flutter development workflow. Whether you’re deploying to the web, publishing mobile apps, or generating internal builds, automating your build steps ensures higher reliability, repeatability, and team efficiency.

In this guide, we’ll walk through building a CI/CD pipeline for Flutter using Docker, covering:

  • Why Docker is helpful for Flutter CI
  • Building a custom Flutter Docker image
  • Running tests and builds inside Docker
  • Setting up CI (example: GitHub Actions)
  • Deployment options (Web, APK/AAB, internal delivery)

Why Use Docker for Flutter CI/CD?

Flutter installations vary by OS, and installing the SDK each time in CI can cost minutes or even break builds. Docker provides a stable, predictable environment.

Benefits

  • ✔ Consistent environment across local and CI builds
  • ✔ Faster builds after initial image pull
  • ✔ No need to install Flutter every pipeline run
  • ✔ Works for web, Android, Linux, and desktop targets
  • ✔ Easy to extend with tools (Firebase CLI, Fastlane, etc.)

Step 1: Create a Custom Docker Image for Flutter

You can use the official Flutter Docker base image, or create your own.

Dockerfile

FROM debian:stable

# Install dependencies
RUN apt-get update && apt-get install -y \
    curl git unzip xz-utils zip libglu1-mesa openjdk-17-jdk \
    && rm -rf /var/lib/apt/lists/*

# Install Flutter
RUN git clone https://github.com/flutter/flutter.git /usr/local/flutter -b stable
ENV PATH="/usr/local/flutter/bin:/usr/local/flutter/bin/cache/dart-sdk/bin:${PATH}"

# Pre-download Flutter artifacts
RUN flutter doctor -v

WORKDIR /app

Build and push your Docker image

docker build -t yourdockerhub/flutter-ci .
docker push yourdockerhub/flutter-ci


Step 2: Local Build and Test Using Docker

Run tests

docker run --rm -v $PWD:/app yourdockerhub/flutter-ci flutter test

Build APK

docker run --rm -v $PWD:/app yourdockerhub/flutter-ci flutter build apk

Build Web

docker run --rm -v $PWD:/app yourdockerhub/flutter-ci flutter build web


Step 3: Set Up CI/CD (Example: GitHub Actions)

Here’s a complete GitHub Actions workflow using your Docker image:

.github/workflows/flutter-ci.yml

name: Flutter CI/CD

on:
  push:
    branches: [ "main" ]
  pull_request:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source
        uses: actions/checkout@v4

      - name: Use custom Flutter Docker image
        uses: addnab/docker-run-action@v3
        with:
          image: yourdockerhub/flutter-ci
          options: -v ${{ github.workspace }}:/app
          run: |
            flutter pub get
            flutter test
            flutter build apk --release
            flutter build web

This pipeline:

  • Pulls your Docker image
  • Installs dependencies
  • Runs tests
  • Builds APK + Web artifacts

Step 4: Deploying Your Flutter App

(A) Deploy Flutter Web to GitHub Pages

Add this job to your workflow:

deploy_web:
  needs: build
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4

    - name: Upload web build
      uses: JamesIves/github-pages-deploy-action@v4
      with:
        folder: build/web
        branch: gh-pages


(B) Uploading APK/AAB to Firebase App Distribution

Add Firebase CLI to your Dockerfile:

RUN curl -sL https://firebase.tools | bash

Then use it in GitHub Actions:

- name: Upload APK to Firebase App Distribution
  run: |
    firebase appdistribution:distribute build/app/outputs/flutter-apk/app-release.apk \
      --app ${{ secrets.FIREBASE_APP_ID }} \
      --groups "qa-team"
  env:
    FIREBASE_TOKEN: ${{ secrets.FIREBASE_TOKEN }}


(C) Deploy to Play Store Using Fastlane

Add Fastlane to your image or run it separately:

- name: Deploy AAB to Play Store
  run: |
    cd android
    fastlane deploy
  env:
    JSON_KEY_FILE: ${{ secrets.GOOGLE_PLAY_JSON }}
    PACKAGE_NAME: "com.example.app"


Step 5: Optional – Versioning & Tagging

You can automatically bump versions:

- name: Auto-version
  run: |
    echo "BUILD_NUMBER=${{ github.run_number }}" >> $GITHUB_ENV

Or update pubspec.yaml before build.


Conclusion

By combining Docker and CI/CD:

  • Your Flutter builds become highly reproducible
  • You avoid environment inconsistencies
  • You simplify onboarding
  • Deployment becomes automatic and reliable

This setup works with GitHub Actions, GitLab CI, Bitbucket Pipelines, Jenkins, or any system that can run Docker.

——————-

Here are the detail setup:

✅ A fully working GitLab CI version
✅ Bitbucket Pipelines version
✅ Jenkinsfile
✅ A production-grade Dockerfile
✅ Diagrams for the CI/CD flow

Here, created ready-to-use CI/CD configs and a production-ready Dockerfile for your Flutter + Docker pipeline, plus a Jenkinsfile and a CI/CD flow diagram (Mermaid) you can drop into docs or a README.

Below you’ll find:

  1. Production-grade Dockerfile for Flutter CI (multi-stage, Android SDK, caching, Fastlane & Firebase CLI)
  2. gitlab-ci.yml (GitLab CI) — builds/test, produces APK/AAB/web, deploys to Firebase App Distribution & GitLab Pages
  3. bitbucket-pipelines.yml (Bitbucket Pipelines) — similar pipeline using your Docker image
  4. Jenkinsfile (declarative Jenkins pipeline) — builds, tests, artifacts, deploy steps
  5. Mermaid diagram for the CI/CD flow + quick notes about secrets and runners

Use these as templates — replace yourdockerhub/flutter-ci, package names, and secret names with your values.


1) Production-ready Dockerfile (multi-stage)

# Stage 1: base with Flutter + Android SDK + tools
FROM ubuntu:24.04 AS base

ENV DEBIAN_FRONTEND=noninteractive \
    ANDROID_SDK_ROOT=/opt/android-sdk \
    JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
    FLUTTER_HOME=/opt/flutter \
    PATH=/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:${PATH}

# Install basic dependencies
RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates curl git unzip xz-utils zip libgtk-3-0 libglu1-mesa \
    openjdk-17-jdk wget gnupg2 python3 python3-pip ruby \
    && rm -rf /var/lib/apt/lists/*

# Install Android command line tools and platforms (API 33) and build-tools
RUN mkdir -p ${ANDROID_SDK_ROOT}/cmdline-tools \
 && cd /tmp \
 && curl -sSL "https://dl.google.com/android/repository/commandlinetools-linux-9975646_latest.zip" -o cmdline-tools.zip \
 && unzip cmdline-tools.zip -d ${ANDROID_SDK_ROOT} \
 && mv ${ANDROID_SDK_ROOT}/cmdline-tools ${ANDROID_SDK_ROOT}/cmdline-tools/latest \
 && yes | ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} --licenses || true \
 && ${ANDROID_SDK_ROOT}/cmdline-tools/latest/bin/sdkmanager --sdk_root=${ANDROID_SDK_ROOT} "platform-tools" "platforms;android-33" "build-tools;33.0.2"

# Install Flutter (stable)
RUN git clone --depth 1 --branch stable https://github.com/flutter/flutter.git ${FLUTTER_HOME} \
 && ${FLUTTER_HOME}/bin/flutter doctor -v

# Install fastlane & firebase tools (optional)
RUN gem install fastlane -NV || true
RUN curl -sL https://firebase.tools | bash || true

WORKDIR /workspace
# End of base image

# Stage 2: slimmer build image - copy Flutter + Android SDK from base
FROM ubuntu:24.04 AS flutter-ci

ENV ANDROID_SDK_ROOT=/opt/android-sdk \
    FLUTTER_HOME=/opt/flutter \
    PATH=/opt/flutter/bin:/opt/flutter/bin/cache/dart-sdk/bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:${PATH}

# Copy installed tools from base stage
COPY --from=base /opt /opt
COPY --from=base /usr/lib/jvm /usr/lib/jvm
COPY --from=base /usr/local/bundle /usr/local/bundle || true

# Install lightweight extras
RUN apt-get update && apt-get install -y --no-install-recommends \
    gettext-base ca-certificates curl git unzip xz-utils zip libglu1-mesa \
    && rm -rf /var/lib/apt/lists/*

# Accept Android licenses (at image build time)
RUN yes | flutter doctor --android-licenses || true

# Create a non-root user for better security
RUN useradd -m -u 1000 builder
USER builder
WORKDIR /home/builder/app

# Cache directories (helpful when mounting volumes during CI runs)
VOLUME ["/home/builder/.pub-cache","/home/builder/.gradle","/home/builder/.android"]

# Entrypoint to show flutter version by default (override at docker run)
ENTRYPOINT ["flutter"]
CMD ["--version"]

Notes

  • This image contains Flutter + Android SDK + Fastlane + Firebase CLI. Build smaller images by removing optional tools.
  • Use image caching in your registry to speed up CI.
  • The Dockerfile accepts Android licenses at build time; in CI you still may need to set ANDROID_HOME/ANDROID_SDK_ROOT envs and mount gradle cache.

2) gitlab-ci.yml (GitLab CI)

image: yourdockerhub/flutter-ci:latest

stages:
  - prepare
  - test
  - build
  - deploy

variables:
  FLUTTER_CHANNEL: "stable"
  ANDROID_SDK_ROOT: "/opt/android-sdk"
  # ensure gradle does not ask interactive questions
  GRADLE_USER_HOME: "${CI_PROJECT_DIR}/.gradle"

cache:
  key: ${CI_COMMIT_REF_SLUG}
  paths:
    - .pub-cache/
    - .gradle/
    - build/

before_script:
  - flutter --version
  - flutter pub get

prepare:
  stage: prepare
  script:
    - echo "Preparing build..."
  artifacts:
    paths:
      - .pub-cache/
      - .gradle/

test:
  stage: test
  script:
    - flutter test --coverage
  artifacts:
    reports:
      junit: junit-report.xml
    paths:
      - build/
    when: always

build_android:
  stage: build
  script:
    - flutter build apk --release -v
    - flutter build appbundle --release
  artifacts:
    paths:
      - build/app/outputs/**/*.apk
      - build/app/outputs/**/*.aab
    expire_in: 1 week

build_web:
  stage: build
  script:
    - flutter build web --release
  artifacts:
    paths:
      - build/web
    expire_in: 1 week

deploy_firebase:
  stage: deploy
  needs: ["build_android"]
  only:
    - main
  script:
    - echo $FIREBASE_TOKEN > firebase_token.json
    - firebase appdistribution:distribute build/app/outputs/flutter-apk/app-release.apk --app $FIREBASE_APP_ID --groups "qa" --token $FIREBASE_TOKEN
  environment:
    name: staging
  when: manual
  allow_failure: false

pages:
  stage: deploy
  dependencies:
    - build_web
  script:
    - mv build/web public
  artifacts:
    paths:
      - public
  only:
    - main

Secrets to set in GitLab CI (Project → Settings → CI/CD → Variables)

  • FIREBASE_TOKEN (for Firebase CLI)
  • FIREBASE_APP_ID
  • GOOGLE_PLAY_JSON (if using Fastlane & Play Store)
  • KEYSTORE_* envs if signing APK/AAB (or store file in protected artifacts)

3) bitbucket-pipelines.yml (Bitbucket Pipelines)

image: yourdockerhub/flutter-ci:latest

pipelines:
  default:
    - step:
        name: Install & Test
        caches:
          - docker
          - gradle
        script:
          - flutter --version
          - flutter pub get
          - flutter test
    - step:
        name: Build Android
        script:
          - flutter build apk --release
          - pipe: atlassian/upload-artifact:0.2.3
            variables:
              ARTIFACT_PATH: 'build/app/outputs/flutter-apk/app-release.apk'
    - step:
        name: Build Web
        script:
          - flutter build web --release
          - pipe: atlassian/upload-artifact:0.2.3
            variables:
              ARTIFACT_PATH: 'build/web/**'
  branches:
    main:
      - step:
          name: Deploy to Firebase (App Distribution)
          script:
            - pipe: atlassian/git:0.9.1
            - echo $FIREBASE_TOKEN > firebase_token.json
            - firebase appdistribution:distribute build/app/outputs/flutter-apk/app-release.apk --app $FIREBASE_APP_ID --groups "qa" --token $FIREBASE_TOKEN

Bitbucket variables to set

  • FIREBASE_TOKEN
  • FIREBASE_APP_ID
  • GOOGLE_PLAY_JSON (as secure file or environment variable)

4) Jenkinsfile (Declarative pipeline)

pipeline {
  agent { docker { image 'yourdockerhub/flutter-ci:latest'; args '-u root:root' } }

  environment {
    ANDROID_SDK_ROOT = "/opt/android-sdk"
    GRADLE_USER_HOME = "${env.WORKSPACE}/.gradle"
  }

  options {
    buildDiscarder(logRotator(numToKeepStr: '10'))
    timestamps()
  }

  stages {
    stage('Checkout') {
      steps {
        checkout scm
      }
    }

    stage('Dependencies') {
      steps {
        sh 'flutter --version'
        sh 'flutter pub get'
      }
    }

    stage('Test') {
      steps {
        sh 'flutter test --coverage'
        junit allowEmptyResults: true, testResults: '**/test-*.xml'
      }
    }

    stage('Build APK/AAB') {
      steps {
        sh 'flutter build apk --release'
        sh 'flutter build appbundle --release'
      }
      post {
        success {
          archiveArtifacts artifacts: 'build/app/outputs/**/*.apk, build/app/outputs/**/*.aab', fingerprint: true
        }
      }
    }

    stage('Build Web') {
      steps {
        sh 'flutter build web --release'
      }
      post {
        success {
          archiveArtifacts artifacts: 'build/web/**', fingerprint: true
        }
      }
    }

    stage('Deploy') {
      when {
        branch 'main'
      }
      steps {
        script {
          // Example: Upload to Firebase App Distribution using token in Jenkins credentials
          withCredentials([string(credentialsId: 'FIREBASE_TOKEN_ID', variable: 'FIREBASE_TOKEN')]) {
            sh 'firebase appdistribution:distribute build/app/outputs/flutter-apk/app-release.apk --app $FIREBASE_APP_ID --groups "qa" --token $FIREBASE_TOKEN'
          }
        }
      }
    }
  }

  post {
    always {
      archiveArtifacts artifacts: 'build/**', allowEmptyArchive: true
    }
    failure {
      mail to: '[email protected]', subject: "Build failed: ${env.JOB_NAME} #${env.BUILD_NUMBER}", body: "Check Jenkins"
    }
  }
}

Jenkins notes

  • Add credential FIREBASE_TOKEN_ID (Secret text) and GOOGLE_PLAY_JSON (secret file) if needed.
  • Run the Jenkins agent with Docker support and privileged mode if you plan to launch emulators.

5) CI/CD Flow Diagram (Mermaid)

You can paste this into Markdown that supports Mermaid or into a mermaid.live editor.

flowchart LR
  A[Developer push to repo] --> B{CI Trigger}
  B --> |push/main| C[CI Runner pulls image yourdockerhub/flutter-ci]
  C --> D[flutter pub get]
  D --> E[Run tests]
  E --> F{tests ok?}
  F -->|no| G[Fail & notify]
  F -->|yes| H[Build artifacts]
  H --> H1[Android APK / AAB]
  H --> H2[Web build]
  H1 --> I[Sign (keystore / fastlane)]
  I --> J[Upload to Firebase App Distribution / Play Store]
  H2 --> K[Deploy to GitLab Pages / GitHub Pages / CDN]
  J --> L[QA + Release]
  K --> L
  G --> M[Open issue / notify via Slack]


Quick Implementation Checklist

  • Build and push Docker image (docker build -t yourdockerhub/flutter-ci . && docker push ...)
  • Add CI variables/secrets:
    • FIREBASE_TOKEN, FIREBASE_APP_ID
    • GOOGLE_PLAY_JSON (service account JSON for Play Store)
    • Keystore and signing configs (KEYSTORE_PASSWORD, KEY_ALIAS, KEY_PASSWORD) or use Fastlane match
  • Ensure CI runners allow Docker images that contain Android SDK (some shared runners may not allow privileged operations)
  • Configure artifact retention and caching (.pub-cache, .gradle) to speed up builds
  • (Optional) Protect deploy jobs to run only on main and require manual approvals for production

Leave a Reply

Your email address will not be published. Required fields are marked *