The architectural intersection of local containerization and orchestration often manifests as a critical networking challenge when a CI/CD pipeline attempts to communicate with a local Kubernetes cluster. Specifically, the hostname kubernetes.docker.internal serves as a specialized DNS entry designed to facilitate communication between the host machine and the Kubernetes API server provided by Docker Desktop. In a standard local environment, this hostname is resolved automatically by the Docker Desktop DNS resolver, allowing developers to interact with their cluster via kubectl from the host terminal without manual IP configuration. However, this seamless resolution fails catastrophically when the execution context shifts from the host machine to a containerized CI/CD runner, such as a GitLab Runner executing a job in an Alpine-based image.
The failure of kubernetes.docker.internal resolution is not a failure of the Kubernetes cluster itself, but rather a limitation of the DNS scope within the containerized runner. When a job runs inside a Docker container, it possesses its own isolated network namespace and its own /etc/resolv.conf configuration. The internal DNS resolver of the runner container does not inherently know the mapping for kubernetes.docker.internal because that mapping is a convenience provided by the Docker Desktop host process to the host OS. Consequently, when a curl command or a kubectl call is initiated from within the runner, the request is sent to the configured DNS server, which returns a "Could not resolve host" error. This disconnection prevents the pipeline from executing essential before_script validations and blocks the deployment of Kubernetes manifests, such as namespaces and services, effectively stalling the delivery pipeline.
The Anatomy of the DNS Resolution Failure
The technical failure manifests during the deploy stage of a CI/CD pipeline, specifically when utilizing a container image such as lachlanevenson/k8s-kubectl:latest. The sequence of failure occurs during the execution of a curl command intended to verify connectivity to the Kubernetes API server.
- The Direct Fact: A GitLab CI pipeline fails during the
before_scriptphase when executingcurl -k https://kubernetes.docker.internal:6443, resulting in the errorcurl: (6) Could not resolve host: kubernetes.docker.internal. - Impact Layer: For the developer, this means the pipeline cannot verify the availability of the cluster before attempting to apply configurations. This results in a job failure that prevents the deployment of the application's
namespace.ymlandservice.ymlfiles, meaning the application is never updated in the target environment. - Contextual Layer: This failure is paradoxical because the developer may find that
kubectlworks perfectly from their local terminal. This is because the host terminal is outside the container's network isolation and leverages the Docker Desktop DNS resolution, whereas the GitLab Runner is trapped within a container that lacks this specific host-mapping.
The environmental context of this failure is defined by a specific set of versions and configurations:
| Component | Version / Value |
|---|---|
| Kubernetes Client Version | v1.30.1 |
| Kubernetes Server Version | v1.29.2 |
| Docker Image (Build) | docker:27.0.0-rc.1-alpine3.20 |
| Docker Service (Dind) | docker:27.0.0-rc.1-dind-alpine3.20 |
| Runner Base Image (Deploy) | lachlanevenson/k8s-kubectl:latest |
| Target API Port | 6443 |
Docker Desktop Kubernetes Integration
To understand why kubernetes.docker.internal exists, one must analyze the integration of Kubernetes within Docker Desktop on Windows and macOS. Docker Desktop provides a streamlined, single-node Kubernetes cluster that is integrated directly into the Docker engine.
- The Direct Fact: Enabling Kubernetes is achieved via Docker Desktop -> Options -> Kubernetes -> Enable Kubernetes.
- Impact Layer: This action installs a fully functional 1-node local Kubernetes cluster. It removes the need for the user to manually set up a complex cluster using tools like kubeadm or Minikube, significantly reducing the barrier to entry for local development.
- Contextual Layer: Once enabled, Docker Desktop creates a virtualized network bridge. The hostname
kubernetes.docker.internalis registered within this bridge to point to the API server of the internal Kubernetes node. This is why the developer's local terminal can reach the server, as it resides on the same host network.
Further configuration of this local environment involves several initialization scripts and services:
init-kubernetes-dashboard.sh: This script installs the Kubernetes Dashboard and configures the necessary Role-Based Access Control (RBAC) authorization.init-ingress-nginx.sh: This script installs the Nginx Ingress controller and sets it as the default ingress controller for the cluster.kubectl proxyorrun-proxy.sh: Running the proxy as a daemon allows the user to access the Kubernetes Dashboard via a local browser athttp://localhost:8001/api/v1/namespaces/kubernetes-dashboard/services/https:kubernetes-dashboard:/proxy/#/node?namespace=_all.get-bearer-token.sh: This utility generates an actual bearer token, stored in./tokens/bearer-token.txt.user, which is required for authenticating with the dashboard.
GitLab CI Pipeline Configuration and Failure Points
The failure occurs within a structured GitLab CI YAML configuration. The pipeline is divided into two primary stages: build and deploy.
The build_image stage is successful. It utilizes docker:27.0.0-rc.1-alpine3.20 and the docker:27.0.0-rc.1-dind-alpine3.20 service. The script performs the following operations:
docker login -u $REGISTRY_USER -p $REGISTRY_PASSWORDdocker-compose builddocker tag portfolioapp-frontend $IMAGE_NAME_FRONTEND:$IMAGE_TAGdocker tag portfolioapp-backend $IMAGE_NAME_BACKEND:$IMAGE_TAGdocker push $IMAGE_NAME_FRONTEND:$IMAGE_TAGdocker push $IMAGE_NAME_BACKEND:$IMAGE_TAG
This stage succeeds because it only interacts with the Docker Registry and the local Docker-in-Docker (DinD) environment, which does not require resolution of the Kubernetes API server.
The deploy stage is where the failure occurs. The configuration for this stage is as follows:
- Variables used include
KUBE_TOKEN,CI_KUBE_TOKEN,KUBE_CONTEXT(set tomy-production-context),AGENT_ID,AGENT_TOKEN,KAS_URL, andK8SPROXY_URL(set tohttps://kubernetes.docker.internal:6443). - The
before_scriptcontains the problematic sequence:apk --no-cache add curlcurl -k https://kubernetes.docker.internal:6443
- The failure of the
curlcommand stops the execution of the subsequentkubectlconfiguration commands, including:kubectl config set-credentials gitlab-ci-serviceaccount --token=$CI_KUBE_TOKENkubectl config set-context my-production-context --cluster=my-production-cluster --user=gitlab-ci-serviceaccount --namespace=prodkubectl config use-context my-production-contextkubectl config set-credentials agent:$AGENT_ID --token="ci:${AGENT_ID}:${AGENT_TOKEN}"kubectl config set-cluster gitlab --server="${KAS_URL}"kubectl config set-context "$KUBE_CONTEXT" --cluster=gitlab --user="agent:${AGENT_ID}"kubectl config use-context "$KUBE_CONTEXT"
The script section, which is never reached due to the before_script failure, is intended to execute:
kubectl config current-contextkubectl config get-contextskubectl apply -f portfolio_kubernetes/namespace.ymlkubectl apply -f portfolio_kubernetes/service.yml
GitLab Agent for Kubernetes Installation
To bridge the gap between GitLab and the Kubernetes cluster, the GitLab Agent is employed. The installation is performed via Helm, which is the standard package manager for Kubernetes.
The installation command used is:
helm upgrade --install portfolioagent gitlab/gitlab-agent --namespace gitlab-agent-portfolioagent --create-namespace --set image.tag=v17.2.0-rc1 --set config.token=glagent-mytoken --set config.kasAddress=wss://kas.gitlab.com --set config.gitlabUrl=https://gitlab.com./ --set-file config.kasCaCert=tls.crt
This deployment establishes a connection between the cluster and the GitLab KAS (Kubernetes Agent Server) at wss://kas.gitlab.com. While the agent is successfully connected and the developer can reach the Kubernetes server from their local terminal, the CI pipeline remains unable to resolve the kubernetes.docker.internal host.
Technical Analysis of Network Pathing
The discrepancy between the terminal's success and the pipeline's failure is rooted in the networking layers of the environment.
Host Terminal Path:
Host Terminal -> Docker Desktop DNS Resolver -> Mapping forkubernetes.docker.internal-> Kubernetes API Server (IP: 127.0.0.1 or internal VM IP).GitLab Runner Path:
GitLab Runner Container -> Container DNS Resolver (/etc/resolv.conf) -> DNS Server (e.g., Google DNS or Docker DNS) -> No mapping forkubernetes.docker.internal-> NXDOMAIN (Non-Existent Domain).
The runner is attempting to resolve a name that is only valid within the host's context. To resolve this, the runner would need an explicit mapping of the hostname to the IP address of the Kubernetes API server, or the pipeline must be configured to use a reachable IP address instead of the kubernetes.docker.internal alias.
Conclusion: Analysis of the Resolution Failure
The error curl: (6) Could not resolve host: kubernetes.docker.internal is a classic manifestation of a DNS scope mismatch. The developer is attempting to use a host-level convenience hostname inside a containerized environment that does not share the host's DNS resolver. While Docker Desktop facilitates local development by providing this alias, it does not inject this alias into every container spawned by a CI/CD runner.
The failure is compounded by the use of before_script as a validation gate. Because the curl command is placed before the kubectl configuration, the pipeline terminates before it can utilize the KAS_URL or other connectivity methods that might be functioning. The fact that the GitLab Agent is connected and kubectl works on the host proves that the cluster is healthy and accessible; the issue is strictly limited to the network namespace of the GitLab runner.
To rectify this, the pipeline must stop relying on kubernetes.docker.internal for its connectivity checks. Instead, it should utilize the actual IP address of the API server or leverage the GitLab Agent's connectivity (KAS) which is already configured and functional. The reliance on an internal Docker Desktop hostname for a pipeline intended for Digital Ocean deployment indicates a configuration drift where local development shortcuts are being applied to a remote deployment strategy. For a production-ready pipeline, the K8SPROXY_URL and curl checks should be pointed toward the public endpoint of the Digital Ocean Kubernetes cluster rather than a local Docker Desktop internal address.