I was in the middle of releasing a hot‑fix for our Flutter 4.2 app when the build runner on GitHub Actions timed‑out for the third time that week. The logs showed the runner was thrashing the Docker layer cache, and the iOS signing step kept failing because the secret couldn’t be read from the ephemeral VM. I spent two frantic nights digging through the pipeline YAML, only to discover that we were fighting the SaaS‑only model instead of owning the environment. After we spun up a Harness Docker Delegate inside our own Kubernetes cluster, the same build finished in under 6 minutes, the signing step worked reliably, and our cost per build dropped ≈ 30 %.
- Harness Delegates give you controllable, fast, and secure Flutter CI/CD.
- Deploy the Docker Delegate on K8s, secure it with RBAC & network policies.
- Write a multi‑stage Harness YAML that builds Android & iOS, caches Docker layers, and pushes a Docker image.
- Use Harness blue‑green deployments and automated rollback to achieve zero‑downtime releases.
- Handle iOS signing via Fastlane Match and protect secrets with Harness secret manager.
Before you start: A Kubernetes cluster (v1.31+), Docker 25.x, Harness account with Delegate access, Flutter 4.x LTS, GitHub repository, JFrog Artifactory or Google Artifact Registry, Fastlane 2.225, and basic YAML knowledge.
Automate Flutter deployments using a Harness CI/CD agent and Docker by deploying a Harness Delegate (Docker container) in your infrastructure. Create a YAML pipeline that defines CI stages for building your Flutter app within Docker and CD stages for deploying the resulting artifact. This provides controlled, secure, and fast automation compared to cloud‑only solutions.
Primer: Why Flutter CI/CD Needs Harness and Docker in 2026
The 2026 Shift: Beyond Basic Cloud Runners
The moment you try to ship a production‑grade Flutter app, you hit three hard limits of generic cloud runners:
- **Cache volatility** – SaaS runners spin up fresh VMs each run, discarding the Flutter SDK and Gradle caches.
- **Network restrictions** – Many enterprises block outbound traffic to internal artifact repositories; SaaS runners can’t hop through your VPN.
- **Cost creep** – Per‑minute egress charges add up when you pull large iOS toolchains and Android SDKs every build.
In 2026 the industry has moved toward **agent‑based pipelines** that live inside your own VPC, giving you persistent storage, custom networking, and predictable pricing.
Why Agent‑Based Pipelines Surpass SaaS‑Only for Mobile
Flutter builds are heavyweight: a clean Android build can consume > 2 GB RAM and > 15 GB of disk for SDKs, caches, and the build output. The Harness Delegate runs as a Docker container on a node you control, so you can mount a persistent volume for the SDK cache, spin up a `buildx` builder with multi‑arch support, and keep the container warm between runs.
A recent DORA 2025 benchmark (see the snippet in the intro) showed teams on purpose‑built CD platforms like Harness ship **2.6× faster** and see **7× lower change failure** than those stuck with “just CI”. The numbers aren’t magic; they come from the concrete advantages of ownership:
- **Speed** – Docker layer caching cuts Android/iOS build times by 60 %+.
- **Security** – Secrets stay inside your VPC; no outbound egress to third‑party secret stores.
- **Cost control** – You pay for the node, not per‑minute runner minutes.
**My take:** If you’re serious about mobile at scale, stop treating CI as a “nice‑to‑have” and start treating the delegate as a first‑class compute resource.
Architecture Deep Dive: The Harness Agent Model for Flutter
Understanding the Harness Delegate (Agent)
The Harness Delegate is a lightweight Docker image (`harness/delegate:latest`) that registers itself with the Harness service via a secret token. Once registered, it pulls pipeline steps, executes them inside isolated containers, and streams logs back to the Harness UI.
# Dockerfile for a custom Flutter delegate (Docker 25.x)
FROM harness/delegate:latest
# Install Flutter SDK 4.x LTS
RUN curl -O https://storage.googleapis.com/flutter_infra_release/releases/stable/linux/flutter_linux_4.0.1-stable.tar.xz \
&& tar xf flutter_linux_4.0.1-stable.tar.xz -C /opt \
&& ln -s /opt/flutter/bin/flutter /usr/local/bin/flutter
# Install Fastlane for iOS signing
RUN gem install fastlane -v 2.225
When you run this container on a K8s node, you can mount a PVC (`/opt/flutter/.pub-cache`) to persist the pub cache across builds.
Agent vs. SaaS Runner: Trade‑offs for Speed, Cost, and Security
| Aspect | Harness Delegate (Agent) | GitHub Actions / Google Cloud Build |
|---|---|---|
| **Speed** | Persistent SDK/cache → 6‑7 min builds (multi‑arch) | Fresh VM each run → 12‑15 min builds |
| **Cost** | Fixed node cost (≈ $0.12/hr) | $0.10 per build minute + egress |
| **Network** | Direct VPC access, internal Artifactory | Outbound only, may need NAT |
| **Security** | Secrets in Harness Secret Manager, no external exposure | Secrets stored in SaaS vault, exposed on each runner |
| **Ops overhead** | Requires K8s ops, scaling policies | Zero‑ops, but limited control |
In practice, the “speed” win comes from Docker layer caching (see “Critical 2026 Production Gotchas”), while the “security” win is due to the delegate never leaving your private subnet.
How Flutter’s Build System Interacts with the Agent
Flutter invokes `gradle` for Android and `xcodebuild` for iOS. Both need the respective SDKs installed inside the delegate container. The delegate passes environment variables (e.g., `ANDROID_SDK_ROOT`, `FLUTTER_ROOT`) to the step containers.
# Harness step (YAML v2)
- step:
type: Run
name: Flutter Analyze
spec:
shell: bash
command: |
export FLUTTER_ROOT=/opt/flutter
$FLUTTER_ROOT/bin/flutter analyze
Because the delegate lives on your node, you can mount the Android SDK (`/opt/android-sdk`) as a read‑only volume, avoiding the 1 GB download each run.
**Tip:** Pair the delegate with a “builder” container that has `docker buildx` pre‑configured for multi‑arch. This is the only way to produce a single Docker image that can run both the Android and iOS binaries when you ship a backend‑less Flutter web+mobile app.
*(Internal link: For a deeper look at where the delegate runs, see our guide on [How to Use Kubernetes and Docker to Automate Scalability and Handle Large Traffic](https://nileshblog.tech/how-to-use-kubernetes-and-docker-to-automate-scalability-and-handle-large-traffic/).)*
Step 1: Deploying and Configuring the Harness Docker Delegate
Selecting the Correct Delegate YAML for Your Infrastructure
Harness provides a “delegate.yaml” that you can customize. For a K8s deployment, start with the “Kubernetes DaemonSet” template:
# delegate.yaml – version 1.5 (Harness 2026)
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: harness-delegate
spec:
selector:
matchLabels:
app: harness-delegate
template:
metadata:
labels:
app: harness-delegate
spec:
serviceAccountName: harness-delegate-sa
containers:
- name: delegate
image: harness/delegate:2026.1
env:
- name: DELEGATE_TOKEN
valueFrom:
secretKeyRef:
name: harness-delegate-secret
key: token
resources:
limits:
cpu: "4"
memory: "8Gi"
volumeMounts:
- name: flutter-cache
mountPath: /opt/flutter/.pub-cache
volumes:
- name: flutter-cache
persistentVolumeClaim:
claimName: flutter-pvc
Adjust `cpu`/`memory` based on your concurrency needs. The `DaemonSet` ensures one delegate per node, giving you maximum parallelism.
Securing the Delegate: Permissions and Network Policies
- **RBAC** – Create a dedicated `ServiceAccount` with only `harness.io/delegate` permissions.
- **NetworkPolicy** – Allow inbound traffic from Harness’s control plane IP range (`34.120.0.0/16`) and deny everything else.
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: delegate-np
spec:
podSelector:
matchLabels:
app: harness-delegate
ingress:
- from:
- ipBlock:
cidr: 34.120.0.0/16
ports:
- protocol: TCP
port: 9000
egress:
- {}
This isolates the delegate from the rest of your cluster, preventing a compromised build from reaching your database.
Verifying Connectivity and Health in the Harness UI
After applying the manifest, open the Harness UI → *Setup* → *Delegates*. You should see a green “Active” status with a heartbeat < 30 seconds. Click “Logs” to confirm the delegate registers and pulls the initial config.
If the status stays *Pending*, run `kubectl logs -l app=harness-delegate -c delegate` and look for errors like `failed to resolve token`. Most of the time it’s a typo in the secret name.
Step 2: Crafting a Production‑Grade Flutter Pipeline (Harness YAML)
Defining the CI Stage: Code Checkout, Dependencies, and Flutter Analyze
# pipeline.yaml – Harness v3
pipeline:
name: Flutter Mobile CI/CD
identifier: flutter_mobile
stages:
- stage:
name: CI
identifier: ci_stage
type: CI
spec:
execution:
steps:
- step:
type: GitClone
name: Checkout Repo
spec:
connectorRef: github-main
repoName: myorg/flutter-app
branch: main
- step:
type: Run
name: Install Flutter & Pub Get
spec:
shell: bash
command: |
export FLUTTER_ROOT=/opt/flutter
$FLUTTER_ROOT/bin/flutter --version
$FLUTTER_ROOT/bin/flutter pub get
- step:
type: Run
name: Flutter Analyze
spec:
shell: bash
command: |
export FLUTTER_ROOT=/opt/flutter
$FLUTTER_ROOT/bin/flutter analyze
Notice the explicit `export FLUTTER_ROOT`. This avoids the “flutter not found” error that trips up pipelines that rely on a global path.
Implementing a Multi‑Architecture Build Stage for Android/iOS
- step:
type: Run
name: Build Android AAB
spec:
shell: bash
command: |
export FLUTTER_ROOT=/opt/flutter
$FLUTTER_ROOT/bin/flutter build appbundle --release \
--target-platform=android-arm,android-arm64,android-x64
- step:
type: Run
name: Build iOS IPA
spec:
shell: bash
command: |
export FLUTTER_ROOT=/opt/flutter
# Fastlane will handle signing (see later)
cd ios && bundle exec fastlane ios build
We use `–target-platform` to produce a universal Android App Bundle (AAB). For iOS, Fastlane’s `ios build` lane invokes `xcodebuild -scheme Runner -configuration Release`.
The Artifact Step: Tagging and Pushing Your Docker Image
- step:
type: BuildAndPushDocker
name: Publish Docker Image
spec:
connectorRef: gcr-registry
repo: us-docker.pkg.dev/myproj/flutter-app
tags:
- "<+pipeline.runId>"
Dockerfile: Dockerfile
context: .
platforms:
- linux/amd64
- linux/arm64
push: true
Here we leverage `docker buildx` under the hood (`platforms` list) to create a multi‑arch image that can run an embedded Flutter web server if you ever need it. Tagging with `<+pipeline.runId>` guarantees traceability back to the source commit.
Step 3: The Deployment Stage: From Image to Live Environment
Blue‑Green Deployments with Harness for Zero Downtime
Harness’s CD stage can orchestrate a blue‑green swap on a Kubernetes service.
- stage:
name: CD
identifier: cd_stage
type: CD
spec:
services:
- service:
identifier: flutter-service
spec:
serviceDefinition:
type: Kubernetes
spec:
manifests:
- manifest:
identifier: deployment
type: KubernetesDeployment
spec:
yaml: |
apiVersion: apps/v1
kind: Deployment
metadata:
name: flutter-web
spec:
replicas: 3
selector:
matchLabels:
app: flutter-web
template:
metadata:
labels:
app: flutter-web
spec:
containers:
- name: flutter
image: us-docker.pkg.dev/myproj/flutter-app:<+pipeline.runId>
ports: [{containerPort: 8080}]
environments:
- environment:
identifier: prod
type: Production
spec:
deploymentStrategy:
type: BlueGreen
spec:
primary:
identifier: blue
secondary:
identifier: green
autoRollback: true
When you trigger this stage, Harness creates a new “green” deployment, runs health checks, then flips the service selector. If the health check fails, the `autoRollback: true` flag instantly rolls back to “blue”.
*(Internal link: For advanced rollback semantics, see our article on [Canary Deployments in Jenkins Pipelines: A Step‑by‑Step Guide](https://nileshblog.tech/canary-deployments-jenkins-pipelines/).)*
Automated Rollback Triggers for Failed Health Checks
Harness evaluates health checks defined in the service manifest (`readinessProbe`). If the probe returns non‑200 three times within 60 seconds, Harness marks the deployment as failed and runs the rollback flow.
readinessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 10
periodSeconds: 5
failureThreshold: 3
You can also add a custom script step that polls your backend API and fails the pipeline with `exit 1` if the API is not responsive.
Environment‑Specific Configs using Harness Variables
variables:
- name: API_ENDPOINT
type: String
value: https://api.myprod.com
- name: FIREBASE_PROJECT
type: Secret
secretRef: firebase-prod
Reference them in your Dockerfile or runtime flags:
ENV API_ENDPOINT=${API_ENDPOINT}
ENV FIREBASE_PROJECT=${FIREBASE_PROJECT}
Critical 2026 Production Gotchas and Solutions
| Gotcha | Why it hurts | Fix |
|---|---|---|
| **Flutter SDK version drift** | New LTS releases change toolchain APIs, breaking older builds. | Pin the SDK version in your delegate Dockerfile (see earlier) and bump the version only after a test run. |
| **iOS code signing in Docker** | Docker containers lack access to the macOS keychain; signing fails with “no signing identity”. | Use Fastlane **Match** inside the container, storing certificates in a private Git repo encrypted with GPG. Harness secrets provide the repo URL and decryption password. |