> ## 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](/guides/admin-guide/custom-flows) 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`](/guides/admin-guide/self-hosted-kubernetes-agent/custom-optional-configuration#deploymentaffinity) and [`deploymentTolerations`](/guides/admin-guide/self-hosted-kubernetes-agent/custom-optional-configuration#deploymenttolerations).
</Warning>

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

<Steps>
  <Step title="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).
  </Step>

  <Step title="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.
  </Step>

  <Step title="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](/guides/admin-guide/self-hosted-kubernetes-agent/extending-deployment-image).

    If `strictSecurityContext` is `true`, `sudo` is blocked, so use a custom image.
  </Step>

  <Step title="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](/guides/admin-guide/variables#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.
  </Step>
</Steps>

## 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. |

## Next steps

* [Custom flows overview](/guides/admin-guide/custom-flows) - Define custom flow steps in `env0.yml`.
* [Extending deployment image](/guides/admin-guide/self-hosted-kubernetes-agent/extending-deployment-image) - Bake the Docker CLI into your deployment image.
* [Custom/optional configuration](/guides/admin-guide/self-hosted-kubernetes-agent/custom-optional-configuration#initcontainer) - Reference for `initContainer`, `customVolumeMounts`, and `podAdditionalEnvVars`.


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