> ## Documentation Index
> Fetch the complete documentation index at: https://docs.envzero.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Run Docker in custom flows

> Run docker build and docker push in custom flow steps on the env zero self-hosted Kubernetes agent by adding a Docker-in-Docker sidecar to the deployment pod.

To run Docker commands such as `docker build` and `docker push` in [custom flow](https://docs.envzero.com/guides/admin-guide/custom-flows.md) steps, add a Docker-in-Docker (`docker:dind`) sidecar to the deployment pod through the agent's Helm values. The sidecar runs the Docker daemon, and your custom flow reaches it over TLS on `localhost`.

## Prerequisites

* A self-hosted Kubernetes agent. env zero-hosted agents don't support this, because it requires changing the agent's Helm values.
* Kubernetes 1.29 or later. The sidecar is a [native sidecar container](https://kubernetes.io/docs/concepts/workloads/pods/sidecar-containers/), which Kubernetes enables by default from 1.29.
* A cluster policy that allows privileged pods in the agent's namespace.

> [!WARNING]
>
> The `docker:dind` container runs privileged. Anyone who can change a custom flow that runs on this agent can use the Docker daemon to gain root access to the node. To limit the impact, schedule deployment pods on dedicated nodes with [`deploymentAffinity`](https://docs.envzero.com/guides/admin-guide/self-hosted-kubernetes-agent/custom-optional-configuration#deploymentaffinity) and [`deploymentTolerations`](https://docs.envzero.com/guides/admin-guide/self-hosted-kubernetes-agent/custom-optional-configuration#deploymenttolerations).

If your cluster blocks privileged pods, build images with a tool that doesn't need a Docker daemon, such as [Buildah](https://buildah.io).

## Add the Docker sidecar

**1. Add the sidecar to your Helm values**

Add the following to your `values.customer.yaml`:

```yaml values.customer.yaml theme={null}
podAdditionalEnvVars:
  DOCKER_HOST: tcp://localhost:2376
  DOCKER_TLS_VERIFY: "1"
  DOCKER_CERT_PATH: /certs/client

customVolumeMounts:
  - name: docker-certs
    mountPath: /certs/client
  - name: docker-data
    mountPath: /var/lib/docker

initContainer:
  - name: dind
    image: docker:29.9.0-dind
    restartPolicy: Always
    securityContext:
      privileged: true
    resources:
      requests:
        cpu: 500m
        memory: 1Gi
      limits:
        cpu: "1"
        memory: 2Gi
    volumeMounts:
      - name: docker-certs
        mountPath: /certs/client
      - name: docker-data
        mountPath: /var/lib/docker
    startupProbe:
      tcpSocket:
        port: 2376
      periodSeconds: 2
      failureThreshold: 30
```

* `restartPolicy: Always` makes the init container a sidecar. It starts before the deployment container, and Kubernetes stops it when the deployment ends.
* The startup probe holds the deployment until the Docker daemon accepts connections.
* At startup, `docker:dind` generates TLS certificates and writes the client certificates to `/certs/client`. `customVolumeMounts` shares that directory with the deployment container.
* The `docker-data` volume holds the images Docker pulls and builds in an `emptyDir` instead of the container's filesystem. `customVolumeMounts` also mounts it in the deployment container, which doesn't use it.
* Size `resources` for your builds. In a namespace with a `ResourceQuota`, Kubernetes rejects the pod if the sidecar doesn't set them.
* `podAdditionalEnvVars` points the Docker CLI at the sidecar. Custom flow steps receive only allowlisted environment variables from the pod, and `podAdditionalEnvVars` adds these to the allowlist.

`docker:29.9.0-dind` is the version this page was tested with. For current versions, see the [`docker` image tags on Docker Hub](https://hub.docker.com/_/docker/tags?name=dind).

**2. Apply the values**

Upgrade the release with the same flags you used to install it:

```shell theme={null}
helm upgrade <YOUR_RELEASE_NAME> env0/env0-agent --namespace <YOUR_NAMESPACE> --set-string agentAccessToken='<YOUR_AGENT_ACCESS_TOKEN>' -f values.customer.yaml
```

The change applies to deployments that start after the upgrade.

**3. Install the Docker CLI**

The deployment image doesn't include the Docker CLI. Install it in one of two ways:

* **In the custom flow.** Run `sudo apk add docker-cli docker-cli-buildx` at the start of the step, as in the example below. The packages download on every deployment.
* **In a custom image.** Add `RUN apk add --no-cache docker-cli docker-cli-buildx` to your Dockerfile and set `dockerImage` to the new image. See [Extending deployment image](https://docs.envzero.com/guides/admin-guide/self-hosted-kubernetes-agent/extending-deployment-image.md).

If `strictSecurityContext` is `true`, `sudo` is blocked, so use a custom image.

**4. Run Docker commands in your custom flow**

This example builds an image from a `Dockerfile` next to `env0.yml` and pushes it to your registry before `terraform init`. Store the registry password in an env zero [sensitive environment variable](https://docs.envzero.com/guides/admin-guide/variables.md#secrets) named `REGISTRY_PASSWORD`.

```yaml env0.yml theme={null}
version: 2

deploy:
  steps:
    terraformInit:
      before:
        - name: Build and push image
          run: |
            sudo apk add docker-cli docker-cli-buildx
            echo "$REGISTRY_PASSWORD" | docker login <YOUR_REGISTRY> -u <YOUR_USERNAME> --password-stdin
            docker build -t <YOUR_REGISTRY>/<YOUR_IMAGE>:<YOUR_TAG> .
            docker push <YOUR_REGISTRY>/<YOUR_IMAGE>:<YOUR_TAG>
```

The step log shows the build output, followed by the pushed image digest.

## Troubleshooting

| Error | Cause and fix |
| - | - |
| `helm upgrade` fails with `startupProbe: Forbidden: may not be set for init containers without restartPolicy=Always` | The cluster runs Kubernetes older than 1.29, which ignores `restartPolicy: Always` on init containers. Upgrade the cluster to 1.29 or later. |
| `Cannot connect to the Docker daemon` | `DOCKER_HOST` is missing or doesn't point to `tcp://localhost:2376`. `docker:dind` listens with TLS on port 2376, not 2375. Set all three variables under `podAdditionalEnvVars` as shown above. |
| `open /certs/client/ca.pem: no such file or directory` | The deployment container doesn't mount the certificates. Add the `customVolumeMounts` entry, and mount the same volume name in the `dind` container. |
| `sudo: The "no new privileges" flag is set` | `strictSecurityContext` is `true`. Install the Docker CLI in a custom image instead. |


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.