Deploying Containers with kubectl on GKE: Beginner Step-by-Step Guide

This page shows you how to deploy a containerised application to Google Kubernetes Engine using kubectl. You will write a Deployment manifest, apply it to your cluster, expose the application with a Service, verify the rollout, and learn how to update and roll back. All from the command line.

Simple explanation

kubectl is a command-line tool that sends instructions to the Kubernetes API server. When you run kubectl apply -f deployment.yaml, kubectl reads your YAML file, sends it to the API server, and Kubernetes creates or updates the resources described, such as Deployments, Pods, and Services.

Analogy

Think of the Kubernetes API server as a site manager on a building project. kubectl is how you hand that manager a blueprint. The manager reads it, assigns workers (pods), and makes sure the site always matches the plan. You do not direct each worker individually — you hand over the blueprint and the manager handles the rest. If a worker quits (pod crashes), the manager hires a replacement automatically.

On GKE, the full workflow is:

  1. Connect kubectl to your cluster using gcloud container clusters get-credentials
  2. Write a YAML manifest describing what you want to deploy
  3. Apply it with kubectl apply -f
  4. Verify that pods are running with kubectl get pods
  5. Expose the application with a Service if it needs to receive traffic

That is the full loop. The rest of this page fills in the detail.

What kubectl is and how it works

kubectl (pronounced “kube control” or “kube cuttle”) is the official Kubernetes CLI. It sends HTTP requests to the Kubernetes API server, which then creates, updates, or deletes objects in the cluster.

Every kubectl command reads your kubeconfig file to know which cluster to target and which credentials to use. By default this file lives at ~/.kube/config. It contains one or more contexts, where each context combines:

  • A cluster entry: the API server address and certificate authority data
  • A user entry: credentials such as a token or client certificate
  • An optional namespace: the default namespace for that context
Analogy

Think of kubeconfig as a contact book. Each entry (context) has an address (the cluster’s API server), credentials (the user), and a preferred department (namespace). When you run kubectl, it opens the current page of the contact book to know where to send the request. Running gcloud container clusters get-credentials adds a new page for that cluster and opens it.

View your active context:

kubectl config current-context

List all configured contexts:

kubectl config get-contexts

When you run gcloud container clusters get-credentials, gcloud writes the cluster details into your kubeconfig and sets it as the active context, so kubectl immediately targets that cluster.

Before you start

To follow this guide you need:

  • A GCP project with billing enabled
  • A running GKE cluster. See Creating Your First GKE Cluster if you do not have one yet
  • The gcloud CLI installed and authenticated
  • kubectl installed (gcloud components install kubectl)
  • IAM permission to access the cluster (roles/container.developer or equivalent)
  • A container image already built and stored in Artifact Registry or another accessible registry

kubectl deploys Kubernetes resources. It does not build container images. The image must exist before kubectl can pull it. See Building Docker Images with Cloud Build for how to build and push images as part of a pipeline.

Connecting kubectl to a GKE cluster

When you create a GKE cluster, kubectl is not automatically configured to reach it. Run the following to fetch credentials and set the active context:

gcloud container clusters get-credentials my-cluster \
  --region europe-west2 \
  --project my-gcp-project

This writes a new entry into ~/.kube/config, including the cluster’s API server address, certificate authority, and an access token, and sets it as the current context. After this, kubectl get nodes returns the nodes in your cluster.

If you work across multiple clusters (dev, staging, and production), each get-credentials call adds a new context. Use kubectl config use-context to switch between them.

Note

GKE clusters configured with authorised networks or private endpoints require you to run kubectl from a machine within the allowed IP range. For private clusters, connect via Cloud Shell, a bastion host, or an Identity-Aware Proxy tunnel. See Private GKE Clusters for details.

When to use kubectl for deployments

kubectl works well for:

  • Learning Kubernetes: running commands directly makes the underlying primitives visible and tangible
  • Manual and one-off deployments: applying a manifest is the fastest path from image to running pod
  • Debugging and emergency fixes: exec into containers, port-forward to test locally, read crash logs
  • Simple pipelines: small teams often run kubectl apply -f in a CI step without needing additional tooling

As teams and environments grow, kubectl alone becomes harder to manage:

  • Manifests for different environments need templating. Helm handles this with values files and chart packaging
  • Multi-environment promotion needs controlled workflows. Cloud Deploy adds approval gates and release history
  • GitOps tools such as ArgoCD or Flux apply manifests from Git automatically, removing manual steps entirely

kubectl remains the foundation. Even when using Helm or Cloud Deploy, it is what you reach for to inspect and debug running workloads.

Declarative vs imperative: choose declarative

kubectl supports two approaches.

Imperative: tell Kubernetes exactly what to do right now:

kubectl run my-pod --image=nginx:1.25
kubectl expose pod my-pod --port=80

Imperative commands are useful for quick experiments, but they leave no record and cannot be re-run safely if the resource already exists.

Declarative: describe desired state in a YAML manifest, then apply it:

kubectl apply -f deployment.yaml

Kubernetes compares your manifest against current state and makes only the necessary changes. Running kubectl apply -f again after editing the file updates the live resource. The manifest is version-controllable, reviewable, and reproducible.

Analogy

Imperative is like calling a restaurant and dictating every step: “add two onions, now add garlic, now stir”. Declarative is like sending the chef a finished recipe and saying “make this”. The chef figures out the steps. With Kubernetes, you want to be the person sending the recipe — not the one calling out instructions mid-cook.

Use declarative manifests for all real deployments. Reserve imperative commands for temporary experiments.

Step-by-step: deploying a container with kubectl

Here is a practical end-to-end flow for deploying a containerised application to GKE.

Step 1: Write a Deployment manifest

A Deployment tells Kubernetes how many replicas to run, which image to use, and what resources each pod requires.

apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-app
  namespace: production
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-app
  template:
    metadata:
      labels:
        app: my-app
    spec:
      containers:
        - name: my-app
          image: europe-west2-docker.pkg.dev/my-project/my-repo/my-app:v1.2.0
          ports:
            - containerPort: 8080
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "500m"
              memory: "256Mi"

The selector.matchLabels and template.metadata.labels must match. This is how the Deployment tracks which pods belong to it.

The image reference uses Artifact Registry format: REGION-docker.pkg.dev/PROJECT/REPOSITORY/IMAGE:TAG. Always pin to a specific image tag rather than latest so deployments are reproducible.

Step 2: Apply the manifest

kubectl apply -f deployment.yaml

Kubernetes schedules the pods across your nodes. If the namespace does not yet exist, create it first:

kubectl create namespace production

Step 3: Verify the rollout

kubectl rollout status deployment/my-app -n production

Then check that pods are running:

kubectl get pods -n production

A healthy pod shows Running under STATUS with all containers ready (for example 1/1). If you see Pending, CrashLoopBackOff, or ErrImagePull, jump to the Troubleshooting section below.

Step 4: Expose the application with a Service

A Deployment creates pods but does not make them reachable by default. A Service provides a stable endpoint in front of your pods.

For internal cluster access only (ClusterIP):

apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: production
spec:
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

For external access via a GCP load balancer (LoadBalancer):

apiVersion: v1
kind: Service
metadata:
  name: my-app
  namespace: production
spec:
  type: LoadBalancer
  selector:
    app: my-app
  ports:
    - port: 80
      targetPort: 8080

Apply it:

kubectl apply -f service.yaml

Get the assigned external IP:

kubectl get service my-app -n production

For routing external traffic across multiple services, consider an Ingress Controller rather than a separate LoadBalancer per service.

Step 5: Update the image

To deploy a new version, update the image tag in your manifest and re-apply:

kubectl set image deployment/my-app \
  my-app=europe-west2-docker.pkg.dev/my-project/my-repo/my-app:v1.3.0 \
  -n production

Or edit the YAML file and run kubectl apply -f deployment.yaml again. Kubernetes performs a rolling update by default, so new pods start before old ones stop. No downtime.

Step 6: Roll back if something goes wrong

kubectl rollout undo deployment/my-app -n production

To roll back to a specific revision:

kubectl rollout history deployment/my-app -n production
kubectl rollout undo deployment/my-app --to-revision=3 -n production

Essential kubectl commands

Inspect cluster state

# List pods in a namespace
kubectl get pods -n my-namespace

# List all pods across all namespaces
kubectl get pods -A

# List deployments, services, and configmaps together
kubectl get deployments,services,configmaps -n my-namespace

# Watch resources update in real time
kubectl get pods -n my-namespace -w

# Inspect a specific resource in detail
kubectl describe pod my-pod-name -n my-namespace

View logs

# Stream logs from a running pod
kubectl logs -f my-pod -n my-namespace

# Read logs from a specific container in a multi-container pod
kubectl logs my-pod -c sidecar-container -n my-namespace

# Read the last 100 lines
kubectl logs my-pod --tail=100 -n my-namespace

# Read logs from the previous (crashed) container instance
kubectl logs my-pod --previous -n my-namespace

The --previous flag is essential for debugging CrashLoopBackOff. It retrieves logs from the terminated container instance, not the newly started replacement. See Logging in Kubernetes for how to ship logs to Cloud Logging.

Exec into containers

kubectl exec -it my-pod -n my-namespace -- bash

Opens an interactive shell inside the running container. Useful for inspecting environment variables, testing connectivity, or checking the filesystem. Use sh if bash is not available in the image.

Port forwarding

kubectl port-forward pod/my-pod 8080:8080 -n my-namespace

Tunnels traffic from your local port 8080 to port 8080 on the pod, so you can test the application at http://localhost:8080 without exposing it publicly. Also works against Services (svc/my-service) and Deployments.

Manage rollouts

# Check rollout progress
kubectl rollout status deployment/my-app -n my-namespace

# View rollout history
kubectl rollout history deployment/my-app -n my-namespace

# Roll back to the previous version
kubectl rollout undo deployment/my-app -n my-namespace

# Roll back to a specific revision
kubectl rollout undo deployment/my-app --to-revision=3 -n my-namespace

# Restart all pods with zero downtime
kubectl rollout restart deployment/my-app -n my-namespace

Delete resources

# Delete via manifest (preferred; explicit about what is removed)
kubectl delete -f deployment.yaml

# Delete by name
kubectl delete pod my-pod -n my-namespace
kubectl delete deployment my-app -n my-namespace

Working with namespaces

Namespaces partition a single cluster into logical environments. Common patterns: one namespace per team, per environment (dev/staging/production), or per application.

Analogy

Think of namespaces as floors in an office building. Each floor has its own team, resources, and rules. Someone on the staging floor cannot accidentally affect the production floor. The building (cluster) is shared, but the floors are isolated. When you forget to specify a floor (-n), you end up in reception (the default namespace) — and wonder why nobody you know is there.

# List all namespaces
kubectl get namespaces

# Create a namespace
kubectl create namespace my-team

# Run commands in a specific namespace
kubectl get pods -n my-team

# Set a default namespace for your current context
kubectl config set-context --current --namespace=my-team

Resources such as Pods, Deployments, Services, ConfigMaps, and Secrets are namespaced. Some resources (Nodes, PersistentVolumes, ClusterRoles) are cluster-scoped and exist outside any namespace.

Forgetting -n is one of the most common beginner mistakes. If your workload is in production and you omit -n production, commands target the default namespace and return nothing. Setting a default namespace on your context removes the need to type -n on every command when working intensively in one environment.

Note

Namespace names must be DNS-compatible: lowercase letters, numbers, and hyphens only. Names like production, team-backend, and monitoring are all valid.

kubectl vs Helm vs Cloud Deploy

ToolWhat it doesWhen to use it
kubectlDirect CLI access to the Kubernetes APILearning, debugging, simple deployments
HelmPackages manifests as charts with templatingMulti-environment deployments; reusable configurations
Cloud DeployManaged release pipelines with promotion gatesProduction workflows; compliance and audit requirements

kubectl is the foundation. Everything else builds on top of it. Use it to learn, debug, and operate clusters directly.

Helm solves the templating problem. Instead of maintaining separate YAML files per environment, a Helm chart uses values files to produce environment-specific manifests. Most projects adopt Helm once they manage more than a handful of services.

Cloud Deploy adds controlled promotion. You define a pipeline (dev to staging to production) with manual approval gates, release history, and one-click rollbacks. It is a GCP-managed service designed for regulated or multi-environment delivery.

GKE vs Cloud Run: if you are still deciding whether you need Kubernetes at all, see GKE vs Cloud Run. Cloud Run is simpler for stateless HTTP services and has no cluster to manage. GKE is the right choice when you need fine-grained control over workload scheduling, custom networking, or background processing.

Troubleshooting deployment problems

Warning

Never use kubectl edit to fix production problems and then consider it done. Changes made via kubectl edit are not reflected in your source-controlled YAML. The next kubectl apply -f will revert them silently. Fix the manifest, commit it, and re-apply.

Pod is not starting: first checks

kubectl get pods -n my-namespace
kubectl describe pod POD_NAME -n my-namespace

The Events section at the bottom of describe output identifies the root cause in almost every case.

Image pull errors (ErrImagePull, ImagePullBackOff)

  • Verify the image name and tag are correct
  • Check that the GKE node’s service account has read access to the registry
  • For Artifact Registry, the default GKE service account needs roles/artifactregistry.reader

Pod crashes immediately (CrashLoopBackOff)

kubectl logs POD_NAME -n my-namespace --previous

Use --previous to read logs from the crashed instance, not the new replacement. Common causes: missing environment variables, failed database connections, or an application error at startup.

Readiness or liveness probe failures

  • Check kubectl describe pod. The Events section shows probe failure messages
  • Verify the probe path, port, and timing settings in your manifest
  • Increase initialDelaySeconds if the application takes longer to start

Pod is Pending and not scheduling

kubectl describe pod POD_NAME -n my-namespace

Common causes: insufficient CPU or memory on available nodes (look for Insufficient cpu or Insufficient memory in Events), or node selectors and taints that do not match any node.

Missing Secret or ConfigMap

If a pod references a Secret or ConfigMap that does not exist in the same namespace, it fails to start. Check with:

kubectl get secrets -n my-namespace
kubectl get configmaps -n my-namespace

See Managing Secrets in Kubernetes for how to create and reference Secrets correctly.

Cluster not reachable

If kubectl get nodes returns a connection error, re-run:

gcloud container clusters get-credentials my-cluster \
  --region europe-west2 \
  --project my-gcp-project

For private clusters, ensure you are connecting from an authorised network. See Private GKE Clusters.

Results are empty but resources should exist

Confirm you are targeting the right namespace:

kubectl config get-contexts
kubectl get pods -A | grep my-app

Common beginner mistakes

  1. Using kubectl create instead of kubectl apply in automation. kubectl create fails if the resource already exists, which breaks CI/CD pipelines. Always use kubectl apply for idempotent, repeatable deployments.
  2. Forgetting -n and operating on the wrong namespace. Commands default to the default namespace. If your workload is in production and you omit -n production, you see nothing and may wrongly conclude something is broken.
  3. Not using —previous when debugging CrashLoopBackOff. kubectl logs my-pod shows the new container’s logs, which are often empty. The crash logs are in the terminated instance: kubectl logs my-pod —previous.
  4. Not checking kubectl describe when a pod will not start. The Events section almost always reveals the root cause: image pull errors, failed probes, resource constraints, missing Secrets. Stopping at kubectl get pods output wastes debugging time.
  5. Editing live resources with kubectl edit instead of updating the manifest. kubectl edit applies changes immediately to the live resource, but those changes are not in your source-controlled YAML. The next kubectl apply -f will revert them.
  6. Deploying without a Service. A Deployment creates pods but does not expose them. Forgetting to create a Service means other pods and external traffic cannot reach your application.
  7. Using latest as the image tag. latest is not reproducible. Different nodes may pull different builds at different times. Always pin to a specific tag or image digest.

Frequently asked questions

What is the difference between kubectl apply and kubectl create?

kubectl create is imperative — it creates a resource and fails if it already exists. kubectl apply is declarative — it creates the resource if it does not exist, or updates it if it does. Always prefer kubectl apply in production so your manifests can be re-applied safely without errors.

How do I switch between GKE clusters?

Each cluster has a context in your kubeconfig file. Run kubectl config get-contexts to list all contexts, then kubectl config use-context CONTEXT_NAME to switch. The gcloud container clusters get-credentials command automatically adds and activates the context for a cluster.

Do I need a Service to access my app?

Yes, if you want other pods or external traffic to reach your application. A Deployment alone creates pods but does not expose them. A Service provides a stable network endpoint. For external access, use a LoadBalancer Service or an Ingress resource.

What should I check first if a pod is in CrashLoopBackOff?

Run kubectl describe pod POD_NAME -n NAMESPACE and read the Events section — it usually shows the exact reason (OOM killed, readiness probe failed, image pull error). Then run kubectl logs POD_NAME -n NAMESPACE --previous to read the logs from the crashed container instance, not the newly started replacement.

Should I use kubectl directly in production?

kubectl is valid for production, especially for smaller teams and simpler environments. The risk is that ad-hoc kubectl commands leave no audit trail and can diverge from source-controlled manifests. Larger teams typically move to GitOps workflows or Cloud Deploy to enforce controlled, repeatable promotion of changes.

Last verified: 23 March 2026 Cloud services change frequently. Verify details against official documentation before making infrastructure decisions.