The connection to the server localhost:8080 was refused almost never means a server is down. It means kubectl could not find a kubeconfig, so it fell back to its compiled-in default of localhost:8080 and nothing is listening there.
The port is the tell. You never configured 8080, and no modern cluster serves on it. Kubernetes API servers listen on 6443. Seeing localhost:8080 in the error is kubectl telling you it is guessing.
Confirm there is no usable config
kubectl config view --minify
If you get this, the diagnosis is done:
error: current-context is not set
or an empty config:
apiVersion: v1
clusters: null
contexts: null
kind: Config
users: null
Then check where kubectl was looking:
echo "${KUBECONFIG:-unset}"
ls -l ~/.kube/config
Three outcomes, each with a different fix:
| What you see | What it means |
|---|---|
KUBECONFIG unset, no ~/.kube/config | kubectl has no config at all |
KUBECONFIG set to a path that does not exist | kubectl silently ignores the missing file |
| Config exists, no current context | kubectl has clusters but no selected one |
Fix 1: No kubeconfig at all
Get one from wherever the cluster lives. The command differs per provider, and each of these writes or merges into ~/.kube/config for you:
# EKS
aws eks update-kubeconfig --region eu-west-1 --name production
# GKE
gcloud container clusters get-credentials production --region europe-west1
# AKS
az aks get-credentials --resource-group rg-prod --name production
# kind / minikube, which usually do this automatically
kind export kubeconfig --name dev
minikube update-context
Then confirm:
kubectl config current-context
kubectl get nodes
Fix 2: KUBECONFIG points somewhere wrong
This one is nastier because there is no warning. kubectl does not tell you the file is missing, it just behaves as though you have no config.
echo "$KUBECONFIG"
# /Users/me/.kube/configs/old-cluster.yaml
ls -l "$KUBECONFIG"
# ls: no such file or directory
A stale export in ~/.zshrc or ~/.bashrc is the usual source, often left behind after a cluster was deleted. Either fix the path or unset it:
unset KUBECONFIG
KUBECONFIG also accepts several paths separated by :, merged left to right:
export KUBECONFIG=~/.kube/config:~/.kube/staging.yaml
That is useful, but if any one path is wrong you get partial config and confusing results rather than an error.
Fix 3: Config exists but no context is selected
kubectl config get-contexts
CURRENT NAME CLUSTER AUTHINFO NAMESPACE
dev dev dev-admin
production production prod-admin
No asterisk in CURRENT means nothing is selected. Pick one:
kubectl config use-context production
When sudo is involved
Running sudo kubectl get pods reads root's home directory, not yours, so it looks for /root/.kube/config and finds nothing. This is a very common cause on a machine where the rest of the container tooling needs sudo.
kubectl does not need root. Drop the sudo.
The related errors
These three messages look different and have genuinely different causes. It is worth knowing which one you have:
The connection to the server localhost:8080 was refused. No config found. Everything above.
You must be logged in to the server (Unauthorized). Config found, cluster reached, credentials rejected. Usually an expired token or an exec credential plugin that cannot run. Check the users section of your config:
kubectl config view --minify -o jsonpath='{.users[0].user.exec.command}'
If that names aws or gke-gcloud-auth-plugin, the binary must be on your PATH. For EKS specifically, an expired session shows up here and aws sso login or refreshing credentials fixes it.
Unable to connect to the server: dial tcp ...: i/o timeout. Config found, correct address, nothing answered. The API server is genuinely unreachable: a private endpoint that needs a VPN, a security group that does not allow your IP, or a cluster that is actually down. Confirm the address first:
kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}'
Then test reachability independently of kubectl:
curl -k -m 5 https://B1A2C3.gr7.eu-west-1.eks.amazonaws.com/version
A timeout there is a network problem, not a kubectl problem.
In CI
A pipeline hitting localhost:8080 means the kubeconfig was never written into the job. Runners start clean, so a config on your laptop is irrelevant.
# GitLab CI
deploy:
script:
- aws eks update-kubeconfig --region eu-west-1 --name production
- kubectl apply -f k8s/
If you inject the config as a base64 secret instead, write it before the first kubectl call and keep the permissions tight:
mkdir -p ~/.kube
echo "$KUBE_CONFIG_B64" | base64 -d > ~/.kube/config
chmod 600 ~/.kube/config
chmod 600 matters because some credential plugins refuse a world-readable kubeconfig.
A checklist
kubectl config view --minify. Empty or erroring → no config.echo $KUBECONFIG. Set to a path that does not exist → unset it or fix it.kubectl config get-contexts. No asterisk →kubectl config use-context <name>.- Using
sudo? Stop. It reads/root/.kube/config. - Getting
Unauthorizedinstead → credentials expired, refresh them. - Getting
i/o timeoutinstead → network path to the API server, not kubectl. - In CI, write the kubeconfig inside the job before any kubectl command.
Frequently Asked Questions
Why does kubectl try to connect to localhost:8080?
Because that is its compiled-in default when it finds no kubeconfig. kubectl looks at the KUBECONFIG environment variable first, then ~/.kube/config. If neither yields a usable file it assumes an unsecured API server on the local machine at port 8080, which was how very old clusters worked. Nothing listens there on a modern setup, so the connection is refused. Seeing port 8080 in the message is therefore a reliable signal that the problem is missing configuration rather than a broken cluster.
How do I tell kubectl which kubeconfig to use?
Set the KUBECONFIG environment variable to the file path, or pass --kubeconfig /path/to/config on a single command. KUBECONFIG also accepts several paths separated by colons, merged left to right, which is how people keep one file per cluster. Be careful with that: if one path in the list does not exist, kubectl merges what it can and gives no warning, so you get a partial config and confusing results rather than a clear error.
Why does kubectl work for me but not with sudo?
Because sudo runs as root and root has a different home directory. Your config is at ~/.kube/config, which resolves to /root/.kube/config under sudo, and that file usually does not exist. kubectl does not need root privileges to talk to an API server, so the fix is simply to drop the sudo. If you genuinely need it, pass the path explicitly with sudo kubectl --kubeconfig ~/.kube/config, remembering that ~ will still expand to root's home unless you write the path out in full.
What is the difference between connection refused and i/o timeout?
Refused means something actively rejected the connection, which on localhost:8080 means nothing is listening. Timeout means the packets went out and nothing came back, so the address is plausible but unreachable: a private API endpoint needing a VPN, a security group that does not allow your IP, or a cluster that is down. Check the address kubectl is using with kubectl config view --minify -o jsonpath='{.clusters[0].cluster.server}', then test it with curl to separate a network problem from a kubectl problem.
Why does my kubeconfig work locally but fail in CI?
CI runners start from a clean image with no kubeconfig, so anything on your laptop is irrelevant. The job has to obtain credentials itself, either by calling the provider (aws eks update-kubeconfig, gcloud container clusters get-credentials) or by writing a config from a stored secret before the first kubectl command. If you write one from a secret, chmod 600 it: some credential plugins refuse to use a kubeconfig that is readable by other users.