How to Deploy an Application to Kubernetes on RHEL 7

Deploying applications to Kubernetes involves more than simply running a container — it means defining how many replicas should run, how the application is exposed to network traffic, how it recovers from failure, and how updates are rolled out without downtime. Kubernetes provides two core objects for this: the Deployment, which manages the desired state of your application Pods, and the Service, which provides a stable network endpoint to reach them. This tutorial covers both imperative (kubectl create) and declarative (YAML manifest) approaches for deploying an application on a RHEL 7 Kubernetes cluster, along with monitoring rollout status, troubleshooting Pods, and understanding the different Service types available.

Prerequisites

  • A running Kubernetes cluster (kubeadm or k3s) with at least one worker node
  • kubectl configured and pointing at your cluster
  • Basic familiarity with YAML syntax
  • The application container image available in a registry (Docker Hub used in examples)

Step 1: Create a Deployment Imperatively with kubectl

The fastest way to get an application running is to use kubectl create deployment. This is ideal for testing and learning, though the declarative YAML approach is preferred for production.

# Deploy an nginx container with 3 replicas
kubectl create deployment my-nginx 
  --image=nginx:1.25 
  --replicas=3

# Verify the deployment was created
kubectl get deployments

# Expected output
NAME       READY   UP-TO-DATE   AVAILABLE   AGE
my-nginx   3/3     3            3           30s

# List the pods created by the deployment
kubectl get pods -l app=my-nginx

Step 2: Expose the Deployment as a Service with kubectl expose

A Deployment alone does not expose your application to network traffic. Use kubectl expose to create a Service that routes traffic to the Deployment’s Pods.

# Create a ClusterIP service (internal only)
kubectl expose deployment my-nginx 
  --type=ClusterIP 
  --port=80 
  --name=my-nginx-svc

# Create a NodePort service (accessible from outside the cluster)
kubectl expose deployment my-nginx 
  --type=NodePort 
  --port=80 
  --name=my-nginx-nodeport

# List services
kubectl get svc

# Example output
NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
my-nginx-svc         ClusterIP   10.96.45.12     <none>        80/TCP         20s
my-nginx-nodeport    NodePort    10.96.88.34     <none>        80:31456/TCP   5s

Step 3: Write a Deployment Manifest (Declarative YAML)

For production use, always manage Kubernetes resources with YAML manifests committed to version control. The following manifest creates a Deployment with resource limits and a liveness probe.

cat > deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: my-nginx
  namespace: default
  labels:
    app: my-nginx
    version: "1.25"
spec:
  replicas: 3
  selector:
    matchLabels:
      app: my-nginx
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 1
      maxUnavailable: 0
  template:
    metadata:
      labels:
        app: my-nginx
        version: "1.25"
    spec:
      containers:
        - name: nginx
          image: nginx:1.25
          ports:
            - containerPort: 80
              protocol: TCP
          resources:
            requests:
              cpu: "100m"
              memory: "128Mi"
            limits:
              cpu: "250m"
              memory: "256Mi"
          livenessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 10
            periodSeconds: 10
          readinessProbe:
            httpGet:
              path: /
              port: 80
            initialDelaySeconds: 5
            periodSeconds: 5
EOF

Step 4: Write Service Manifests (ClusterIP, NodePort, LoadBalancer)

A Service YAML file defines how the application is exposed. The three most common type values serve different networking scenarios.

cat > service.yaml <<'EOF'
# --- ClusterIP: reachable only within the cluster ---
apiVersion: v1
kind: Service
metadata:
  name: my-nginx-clusterip
spec:
  type: ClusterIP
  selector:
    app: my-nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
---
# --- NodePort: reachable from outside using <NodeIP>:<NodePort> ---
apiVersion: v1
kind: Service
metadata:
  name: my-nginx-nodeport
spec:
  type: NodePort
  selector:
    app: my-nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
      nodePort: 30080   # Must be in range 30000-32767
---
# --- LoadBalancer: requests an external LB from the cloud provider ---
apiVersion: v1
kind: Service
metadata:
  name: my-nginx-lb
spec:
  type: LoadBalancer
  selector:
    app: my-nginx
  ports:
    - protocol: TCP
      port: 80
      targetPort: 80
EOF

Step 5: Apply Manifests with kubectl apply

The kubectl apply command is idempotent — it creates the resource if it does not exist, or updates it if it does. Always prefer apply over create for declarative workflows.

# Apply the deployment manifest
kubectl apply -f deployment.yaml

# Apply the service manifest
kubectl apply -f service.yaml

# Apply an entire directory of manifests
kubectl apply -f ./manifests/

# Perform a dry run to preview changes without applying
kubectl apply -f deployment.yaml --dry-run=client

Step 6: Monitor Rollout Status

After applying a Deployment, use kubectl rollout status to watch the rolling update complete. This is especially important during upgrades.

# Watch rollout progress
kubectl rollout status deployment/my-nginx

# Expected output when successful
Waiting for deployment "my-nginx" rollout to finish: 1 of 3 updated replicas are available...
Waiting for deployment "my-nginx" rollout to finish: 2 of 3 updated replicas are available...
deployment "my-nginx" successfully rolled out

# View rollout history
kubectl rollout history deployment/my-nginx

# Roll back to the previous version if needed
kubectl rollout undo deployment/my-nginx

Step 7: Scale the Deployment

# Scale to 5 replicas imperatively
kubectl scale deployment my-nginx --replicas=5

# Or edit the YAML and re-apply
# Change spec.replicas to 5 in deployment.yaml then:
kubectl apply -f deployment.yaml

# Verify scaling
kubectl get deployment my-nginx
kubectl get pods -l app=my-nginx

Step 8: Inspect and Troubleshoot Pods with kubectl describe

When a Pod is not starting or behaving unexpectedly, kubectl describe pod provides detailed event logs and status information.

# List all pods with status
kubectl get pods -l app=my-nginx -o wide

# Describe a specific pod (replace pod name with actual)
kubectl describe pod my-nginx-6d8f4b9b7c-xk2pq

# View pod logs
kubectl logs my-nginx-6d8f4b9b7c-xk2pq

# Follow logs in real time
kubectl logs -f my-nginx-6d8f4b9b7c-xk2pq

# View logs from a previous crashed container
kubectl logs my-nginx-6d8f4b9b7c-xk2pq --previous

# Execute a shell inside a running pod for debugging
kubectl exec -it my-nginx-6d8f4b9b7c-xk2pq -- /bin/bash

Step 9: Update the Application Image

To deploy a new version of your application, update the container image in the Deployment. Kubernetes performs a rolling update, replacing Pods one at a time with zero downtime.

# Update image imperatively
kubectl set image deployment/my-nginx nginx=nginx:1.26

# Or update deployment.yaml and re-apply:
# Change image: nginx:1.25 to image: nginx:1.26 then:
kubectl apply -f deployment.yaml

# Monitor the rolling update
kubectl rollout status deployment/my-nginx

Step 10: Clean Up Resources

# Delete the deployment
kubectl delete deployment my-nginx

# Delete services
kubectl delete svc my-nginx-clusterip my-nginx-nodeport my-nginx-lb

# Delete using the manifest files (removes all resources defined in them)
kubectl delete -f deployment.yaml
kubectl delete -f service.yaml

You now have a solid foundation for deploying and managing applications in Kubernetes on RHEL 7. The combination of Deployment manifests with replica management, rolling update strategies, resource limits, and health probes ensures your application runs reliably and recovers automatically from failures. Service objects — ClusterIP for internal communication, NodePort for direct external access, and LoadBalancer for cloud environments — give you flexible control over network exposure. As your application complexity grows, explore Ingress controllers for HTTP routing, ConfigMaps and Secrets for externalized configuration, and Horizontal Pod Autoscalers for automatic scaling based on CPU and memory metrics.