Deploy a Secure Container to Kubernetes

Containerize a Linux script with Docker and deploy it to Kubernetes using Terraform.

Introduction

30 Second Summary

Every application running in the cloud lives inside a container. Knowing how to build one securely and deploy it to a cluster is one of the most in-demand skills in cloud engineering.

In this project, you will build a security-hardened Docker container that runs a fun deployment-simulator script, deploy it to a local Kubernetes cluster using Terraform, and push the whole project to GitHub with a professional README.

What You'll Build

A security-hardened containerized deployment mission simulator deployed to a local Kubernetes cluster using Terraform, with the entire project stored in a GitHub repository.

By the end of this project, you'll have:

  • A Docker image that runs as a non-root user and packages a Linux deployment-simulator script.
  • A Terraform configuration that deploys your container as a Kubernetes Job with security contexts and resource limits.
  • A GitHub repository with a professional README ready to share on LinkedIn.
  • Secret Mission: Schedule your mission as a CronJob that runs on a timer and collects logs from multiple runs.

Are there any prerequisites?

You should have basic command-line familiarity and some exposure to Kubernetes and Terraform concepts. No hands-on experience with either is required.

You will need Terraform, Git, and a GitHub account already set up on your machine. This project will guide you through installing Docker Desktop, which provides both Docker and a local Kubernetes cluster.

Install Docker Desktop and Verify Tools

Docker Desktop bundles everything you need to build containers and run a local Kubernetes cluster in a single install.

In this step, you will install Docker Desktop, confirm that your existing tools (Terraform and Git) are working, and switch on the built-in Kubernetes cluster that ships with Docker Desktop.

In this step, get ready to:
  • Install Docker Desktop for Windows.
  • Verify Docker, Terraform, and Git return version numbers.
  • Enable the built-in Kubernetes cluster and confirm it is running.
Download and install Docker Desktop

Docker Desktop provides the Docker Engine, the Docker CLI, and a built-in single-node Kubernetes cluster with kubectl included.

  • Open your browser and go to the Docker Desktop for Windows install page:
  • https://docs.docker.com/desktop/install/windows-install/
  • Click the download button to get Docker Desktop Installer.exe.
  • Double-click the installer to run it.
  • When prompted, choose the per-user installation (recommended, no admin rights needed).
  • Let the installer finish. It will set up WSL 2 as the backend automatically.
  • Once installation completes, press the Windows key to open your search bar.
  • Type Docker Desktop and press Enter to open it.

What is WSL 2?

WSL 2 (Windows Subsystem for Linux 2) is a lightweight Linux kernel built into Windows. Docker Desktop uses it to run Linux containers natively on your machine without a full virtual machine.

Docker Desktop takes a moment to start up the first time. Look for the whale icon in your system tray (bottom-right corner of your taskbar). When the whale stops animating, Docker is ready.

Don't see the whale icon?

If Docker Desktop does not appear in your system tray after a minute, try restarting your computer. The installer sometimes needs a reboot to finish configuring WSL 2.

help me troubleshoot Docker Desktop not starting on Windows

Verify your tools in PowerShell

With Docker Desktop running, you can now confirm that all three CLI tools respond correctly.

  • Press the Windows key to open your search bar.
  • Type PowerShell and press Enter to open it.
  • Verify Docker is installed by running this command:
docker --version

✔️ I see a version number

Docker is installed and running. You are good to continue.

ⓧ I see an error or 'command not found'

This usually means Docker Desktop is not running yet. Check that the whale icon is visible in your system tray.

  • If the whale icon is missing, open Docker Desktop from your Start menu and wait for it to finish starting.
  • If you see a message about WSL 2, follow the link in the error to install the WSL 2 Linux kernel update, then restart Docker Desktop.
  • Close and reopen PowerShell after Docker Desktop starts, then try the command again.

help me fix Docker not being recognized in PowerShell

  • Verify Terraform is installed by running:
terraform --version
  • Verify Git is installed by running:
git --version

Both commands should print a version number. You already have Terraform and Git on your machine, so these checks confirm they are accessible from PowerShell.

Terraform or Git not recognized?

If either command fails, the tool may not be on your system PATH. Close PowerShell, reinstall the tool using its official installer, and make sure to check the option to add it to PATH during installation. Then reopen PowerShell and try again.

help me fix Terraform or Git not found in PowerShell on Windows

Enable Kubernetes in Docker Desktop

Docker Desktop ships with a single-node Kubernetes cluster you can switch on from the settings. This gives you a real cluster to deploy to without needing a cloud provider.

  • Open the Docker Desktop window (click the whale icon in your system tray).
  • Click the gear icon at the top to open Settings.
  • Select the Kubernetes tab in the left sidebar.
  • Check the Enable Kubernetes checkbox.
  • Click Apply & Restart.

Docker Desktop will download the Kubernetes components and start the cluster. This can take a few minutes the first time. Wait for the green Kubernetes running indicator to appear at the bottom-left of the Docker Desktop window.

  • Back in PowerShell, verify the cluster is ready by running:
kubectl get nodes

✔️ I see docker-desktop in Ready status

Your Kubernetes cluster is up and running. You should see one node named docker-desktop with a Ready status.

ⓧ I see an error or no nodes

  • If you see a connection refused error, Kubernetes is still starting. Wait another minute, then try the command again.
  • If the node shows NotReady, wait 30 seconds and run the command again. The cluster needs a moment to initialize.
  • If kubectl is not recognized, close and reopen PowerShell. Docker Desktop adds kubectl to your PATH when Kubernetes is enabled.

help me troubleshoot kubectl not connecting to Docker Desktop Kubernetes

What did kubectl just show you?

The kubectl get nodes command lists every machine in your Kubernetes cluster. Since Docker Desktop runs a single-node cluster, you see one node named docker-desktop with a Ready status. This is the same node where your container will run in later steps.

Your environment is ready. Next up, you will write the deployment mission script and build a security-hardened Docker image around it.

Write the Mission Script and Secure Dockerfile

Your environment is locked and loaded. Now it is time to create the actual workload your container will run.

In this step, you will write a bash script that simulates a deployment mission with styled terminal output and real system data. Then you will package it inside a security-hardened Docker image that runs as a non-root user.

In this step, get ready to:
  • Create the project folder and write the mission script.
  • Write a security-hardened Dockerfile with a non-root user.
  • Build the Docker image and verify it exists locally.
Create the project folder and mission script

Every containerized application starts with the code it will run. Your mission script uses basic Linux commands to print a movie-style deployment sequence with real system information.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir k8s-mission-deploy
cd k8s-mission-deploy
  • Confirm the folder exists by running:
ls

You should see the k8s-mission-deploy folder listed on your Desktop.

  • Create a new file called mission.sh inside your k8s-mission-deploy folder by running:
New-Item -Name "mission.sh" -ItemType "File"
  • Open mission.sh in your text editor and paste the following script:
#!/bin/sh

echo ""
echo "╔══════════════════════════════════════════╗"
echo "║   MISSION: SYSTEM DEPLOYMENT v1.0       ║"
echo "╚══════════════════════════════════════════╝"
echo ""

sleep 1
echo "[*] Initializing secure connection......... OK"
sleep 1
echo "[*] Scanning network targets............... OK"
sleep 1
echo "[*] Loading deployment payload............. OK"
sleep 1
echo "[*] Verifying system integrity............. OK"
sleep 1
echo ""
echo "──────────────────────────────────────────"
echo "  SYSTEM INTEL REPORT"
echo "──────────────────────────────────────────"
echo ""
echo ">> Hostname: $(hostname)"
echo ">> OS:       $(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
echo ">> Kernel:   $(uname -r)"
echo ">> User:     $(whoami) (non-root)"
echo ">> Memory:"
free -h | grep Mem | awk '{print "   Total: " $2 "  Used: " $3 "  Free: " $4}'
echo ">> Disk:"
df -h / | tail -1 | awk '{print "   Size: " $2 "  Used: " $3 "  Avail: " $4 "  Use%: " $5}'
echo ">> Network:"
ip -4 addr show | grep inet | awk '{print "   " $2 " on " $NF}'
echo ""
sleep 1
echo "[*] Deploying to target cluster........... OK"
sleep 1
echo "[*] Running post-deploy checks............ OK"
sleep 1
echo "[*] Security scan......................... PASSED"
sleep 1
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║        MISSION ACCOMPLISHED             ║"
echo "║   All systems operational. Status: GO   ║"
echo "╚══════════════════════════════════════════╝"
echo ""

What does this script do?

The script prints a styled banner, then pauses between each step using sleep to simulate a real deployment sequence.

The "SYSTEM INTEL REPORT" section uses real Linux commands to pull live data from inside the container: hostname for the container ID, free -h for memory stats, df -h / for disk usage, and ip -4 addr show for network interfaces.

  • Save mission.sh.

Seeing odd characters in the file?

Make sure your text editor is saving the file with UTF-8 encoding. The box-drawing characters (╔, ║, ╚) require UTF-8 to display correctly.

Also check that you pasted the entire script starting from #!/bin/sh on the very first line.

Help me fix encoding issues with my mission.sh script

Create the security-hardened Dockerfile

A Dockerfile is a recipe that tells Docker how to build your image. Your Dockerfile will start from a minimal Alpine Linux base, install the tools your script needs, and create a non-root user so the container never runs with admin privileges.

  • Create a new file called Dockerfile in your k8s-mission-deploy folder by running:
New-Item -Name "Dockerfile" -ItemType "File"
  • Open Dockerfile in your text editor and paste the following:
# Start from a minimal Alpine Linux image
FROM alpine:3.21

# Install the tools our script needs for system info
RUN apk add --no-cache procps iproute2

# Create a non-root user (security best practice)
RUN adduser -D -u 1000 mission

# Copy our script into the container
COPY mission.sh /mission.sh
RUN chmod +x /mission.sh

# Switch to the non-root user
USER mission

# Run the script when the container starts
ENTRYPOINT ["/mission.sh"]

What does this Dockerfile do?

  • FROM alpine:3.21 starts from a tiny ~5MB Linux image, minimizing the attack surface.
  • apk add --no-cache procps iproute2 installs procps (for the free command) and iproute2 (for the ip command).
  • adduser -D -u 1000 mission creates a user called mission with UID 1000. The USER mission line switches to that user so the container never runs as root.
  • ENTRYPOINT ["/mission.sh"] tells Docker to run your script automatically when the container starts.

Why run as a non-root user?

By default, containers run as root. If an attacker escapes the container, they land on the host machine with root privileges. Running as a non-root user limits the damage a compromised container can do.

This is a production security best practice that many organizations require before any container touches a real cluster.

  • Save Dockerfile.

✔️ Awesome, I've got everything!

Great. Make sure both files are saved before moving on to the build step.

ⓧ I'd like to double check the full code

Here is the complete mission.sh:

#!/bin/sh

echo ""
echo "╔══════════════════════════════════════════╗"
echo "║   MISSION: SYSTEM DEPLOYMENT v1.0       ║"
echo "╚══════════════════════════════════════════╝"
echo ""

sleep 1
echo "[*] Initializing secure connection......... OK"
sleep 1
echo "[*] Scanning network targets............... OK"
sleep 1
echo "[*] Loading deployment payload............. OK"
sleep 1
echo "[*] Verifying system integrity............. OK"
sleep 1
echo ""
echo "──────────────────────────────────────────"
echo "  SYSTEM INTEL REPORT"
echo "──────────────────────────────────────────"
echo ""
echo ">> Hostname: $(hostname)"
echo ">> OS:       $(cat /etc/os-release | grep PRETTY_NAME | cut -d= -f2 | tr -d '"')"
echo ">> Kernel:   $(uname -r)"
echo ">> User:     $(whoami) (non-root)"
echo ">> Memory:"
free -h | grep Mem | awk '{print "   Total: " $2 "  Used: " $3 "  Free: " $4}'
echo ">> Disk:"
df -h / | tail -1 | awk '{print "   Size: " $2 "  Used: " $3 "  Avail: " $4 "  Use%: " $5}'
echo ">> Network:"
ip -4 addr show | grep inet | awk '{print "   " $2 " on " $NF}'
echo ""
sleep 1
echo "[*] Deploying to target cluster........... OK"
sleep 1
echo "[*] Running post-deploy checks............ OK"
sleep 1
echo "[*] Security scan......................... PASSED"
sleep 1
echo ""
echo "╔══════════════════════════════════════════╗"
echo "║        MISSION ACCOMPLISHED             ║"
echo "║   All systems operational. Status: GO   ║"
echo "╚══════════════════════════════════════════╝"
echo ""

And here is the complete Dockerfile:

FROM alpine:3.21

RUN apk add --no-cache procps iproute2

RUN adduser -D -u 1000 mission

COPY mission.sh /mission.sh
RUN chmod +x /mission.sh

USER mission

ENTRYPOINT ["/mission.sh"]
Build the Docker image

A Docker image is the packaged result of your Dockerfile. Building converts your recipe into a reusable snapshot that can be run as a container anywhere Docker is installed.

  • Make sure you are inside the k8s-mission-deploy folder in PowerShell (where your Dockerfile and mission.sh live).
  • Build the Docker image by running this command:
docker build -t mission-deploy:1.0 .

What does this command do?

  • docker build tells Docker to read the Dockerfile in the current directory and build an image.
  • -t mission-deploy:1.0 tags the image with the name mission-deploy and version 1.0.
  • . (the dot) tells Docker that the build context is the current directory.

Docker will download the alpine:3.21 base image the first time you build. This may take 10-20 seconds.

  • Verify the image was built successfully by running:
docker images

You should see mission-deploy with the tag 1.0 in the list of images.

Build failed or image not showing?

  • Make sure Docker Desktop is running. Check for the whale icon in your system tray.
  • Confirm you are in the k8s-mission-deploy folder where both Dockerfile and mission.sh exist.
  • Check that your file is named exactly Dockerfile with no file extension. Some editors may add .txt automatically.

Help me debug my Docker build failure

Your Docker image is built and ready. Next up, you will run the container and see the deployment simulation in action.

Run and Test Your Container Locally

Your Docker image is built and ready to go. Now it is time to actually run it and confirm that the security measures you baked in are working.

In this step, you will launch the container, watch the deployment simulation play out in real time, and verify that the container runs as a non-root user. You will also observe how each container gets its own isolated environment.

In this step, get ready to:
  • Run the deployment simulation and watch the terminal output.
  • Verify the container runs as a non-root user.
  • Observe container isolation by comparing hostnames across runs.
Run the deployment simulation

The mission-deploy:1.0 image packages your script inside a lightweight Alpine Linux container. Running it launches the full deployment sequence.

  • Run your container by executing this command in PowerShell:
docker run --rm mission-deploy:1.0

What does this command do?

This command starts a new container from your mission-deploy:1.0 image.

  • The --rm flag automatically removes the container after it finishes. This keeps your system clean.
  • Docker creates a fresh isolated environment, runs the /mission.sh entrypoint as the mission user, and streams the output to your terminal.

Watch your terminal for about 10 seconds. You should see the full deployment simulation play out, starting with the mission banner, progressing through status checks, printing system information, and ending with the "MISSION ACCOMPLISHED" banner.

What should I see?

The output starts with a bordered banner saying MISSION: SYSTEM DEPLOYMENT v1.0. It then prints several status lines with OK messages, followed by a SYSTEM INTEL REPORT showing the container's hostname, OS, kernel, user, memory, disk, and network details.

Notice the >> User: mission (non-root) line. This confirms your container is running as the non-root user you created in the Dockerfile.

Not seeing any output?

  • Make sure Docker Desktop is running. Look for the whale icon in your system tray.
  • If you see an error like "image not found", confirm the image exists by running docker images and checking for mission-deploy with tag 1.0.
  • If the image is missing, go back to the previous step and rebuild it with docker build -t mission-deploy:1.0 ..

Still stuck? Help me troubleshoot why my Docker container is not producing output.

Verify non-root execution

Seeing the user printed in the script output is one thing. Proving it from outside the script gives you certainty that the container process itself is non-root.

  • Override the container's entrypoint to run whoami directly:
docker run --rm --entrypoint whoami mission-deploy:1.0

What does this command do?

The --entrypoint whoami flag temporarily replaces your /mission.sh script with the whoami command. Instead of running the full simulation, the container just prints the name of the user it is running as, then exits.

This is a quick way to audit any container's runtime identity without modifying the image.

You should see a single word printed: mission. This confirms the container process runs as the non-root user you defined in your Dockerfile with the USER directive.

Why does non-root matter?

If a container runs as root and an attacker finds a vulnerability, they have full privileges inside the container. In some scenarios, this can lead to escaping the container and compromising the host system.

Running as a non-root user means even if the container is compromised, the attacker's permissions are limited. This is a fundamental Docker security best practice.

Seeing 'root' instead of 'mission'?

  • Check that your Dockerfile contains the line USER mission after the COPY and RUN chmod lines.
  • If you edited the Dockerfile, rebuild the image with docker build -t mission-deploy:1.0 . and run the whoami check again.

Need help? Why is my Docker container running as root instead of my custom user?

Observe container isolation

Every container gets its own hostname, filesystem, and network stack. Running the same image twice produces two completely independent environments.

  • Run the container again and pay attention to the >> Hostname: line in the output:
docker run --rm mission-deploy:1.0
  • Compare the hostname from this run to the hostname from your first run.

The hostnames are different. Each container gets a unique hostname (a random hex string) because Docker creates a brand new isolated environment every time you run the image.

Why are the hostnames different?

Docker assigns each container a random ID that doubles as its hostname. Since --rm deletes the container after it exits, each run creates a fresh container with a new identity.

This isolation is what makes containers so powerful for deployment. Each instance is clean, reproducible, and independent of every other instance.

Your container is verified: it runs as a non-root user and creates a fresh isolated environment every time. Next up, you will write a Terraform configuration that deploys this container to Kubernetes with full security contexts and resource limits.

Write the Terraform Configuration with Security

You have confirmed your container runs as a non-root user and produces the mission output correctly. Now it is time to deploy that container to your local Kubernetes cluster.

Instead of running manual kubectl commands, you will define the entire deployment as code using Terraform. This approach is called Infrastructure as Code (IaC). It means your deployment is repeatable, version-controlled, and easy to tear down with a single command.

In this step, get ready to:
  • Declare the Kubernetes provider in a new main.tf file.
  • Define a Job resource with security contexts and resource limits.
  • Initialize Terraform and preview the deployment plan.
Create the provider configuration

Every Terraform project needs a provider block that tells Terraform which plugin to download and how to connect to the target system. In this case, you need the hashicorp/kubernetes provider pointed at your Docker Desktop cluster.

  • Create a new file called main.tf inside your k8s-mission-deploy folder and paste this provider configuration:
terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = ">= 3.0"
    }
  }
}

provider "kubernetes" {
  config_path    = "~/.kube/config"
  config_context = "docker-desktop"
}

What does this code do?

The terraform block declares that this project requires the hashicorp/kubernetes provider at version >= 3.0. When you run terraform init, Terraform downloads this plugin automatically.

The provider block tells the plugin where your cluster lives. config_path points to the kubeconfig file that Docker Desktop created. config_context specifies the docker-desktop context so Terraform connects to the right cluster.

  • Save main.tf.
  • You will not see a change yet. The next chunk adds the Job resource that uses this provider.

Seeing red squiggles in your editor?

If your editor shows syntax warnings on .tf files, it may not have an HCL extension installed. This will not affect Terraform. The file will still work correctly when you run terraform init.

help me fix HCL syntax highlighting in my editor

Add the Kubernetes Job resource with security

A Kubernetes Job is the right primitive for your script because the container runs to completion and exits. You will add security contexts that enforce non-root execution, a read-only filesystem, and resource limits.

  • In main.tf, add this resource block below the provider configuration:
resource "kubernetes_job_v1" "mission" {
  metadata {
    name = "mission-deploy-job"
  }

  spec {
    template {
      metadata {}
      spec {
        # Pod-level security: reject if container tries to run as root
        security_context {
          run_as_non_root = true
          run_as_user     = 1000
        }

        container {
          name              = "mission"
          image             = "mission-deploy:1.0"
          image_pull_policy = "Never"

          # Container-level security: read-only filesystem, no privilege escalation
          security_context {
            read_only_root_filesystem  = true
            allow_privilege_escalation = false
            capabilities {
              drop = ["ALL"]
            }
          }

          # Resource limits prevent the container from consuming excessive resources
          resources {
            limits = {
              cpu    = "200m"
              memory = "128Mi"
            }
            requests = {
              cpu    = "100m"
              memory = "64Mi"
            }
          }
        }
        restart_policy = "Never"
      }
    }
    backoff_limit = 2
  }

  wait_for_completion = true

  timeouts {
    create = "2m"
  }
}

What does this code do?

  • kubernetes_job_v1 creates a Kubernetes Job that runs your container once to completion.
  • image_pull_policy = "Never" tells Kubernetes to use the local Docker image instead of trying to pull from a remote registry.
  • The pod-level security_context enforces that the pod must run as non-root (UID 1000). Kubernetes will reject the pod if it tries to run as root.
  • The container-level security_context makes the filesystem read-only, blocks privilege escalation, and drops all Linux capabilities.
  • The resources block caps CPU at 200 millicores and memory at 128Mi. This prevents one container from starving others.
  • wait_for_completion = true means terraform apply will block until the Job finishes running.

Why a Job instead of a Deployment?

Your mission script runs once, prints output, and exits. A Deployment is designed for long-running services that should always be available. It would keep restarting your container forever.

A Job is the correct Kubernetes primitive for run-to-completion workloads. It runs the container, waits for success, and stops.

  • Save main.tf.
  • You will verify everything works when you run terraform init in the next substep.

Unsure about the nesting?

HCL uses nested blocks instead of indentation-based syntax. The security_context block inside spec is the pod-level context. The security_context block inside container is the container-level context. Make sure both are present and at the correct depth.

help me understand the nesting of my Terraform security context blocks

✔️ Awesome, I've got everything!

Great. Double check you have saved main.tf with both the provider block and the resource block.

ⓧ I'd like to double check the full code

Here is the complete main.tf file so far:

terraform {
  required_providers {
    kubernetes = {
      source  = "hashicorp/kubernetes"
      version = ">= 3.0"
    }
  }
}

provider "kubernetes" {
  config_path    = "~/.kube/config"
  config_context = "docker-desktop"
}

resource "kubernetes_job_v1" "mission" {
  metadata {
    name = "mission-deploy-job"
  }

  spec {
    template {
      metadata {}
      spec {
        security_context {
          run_as_non_root = true
          run_as_user     = 1000
        }

        container {
          name              = "mission"
          image             = "mission-deploy:1.0"
          image_pull_policy = "Never"

          security_context {
            read_only_root_filesystem  = true
            allow_privilege_escalation = false
            capabilities {
              drop = ["ALL"]
            }
          }

          resources {
            limits = {
              cpu    = "200m"
              memory = "128Mi"
            }
            requests = {
              cpu    = "100m"
              memory = "64Mi"
            }
          }
        }
        restart_policy = "Never"
      }
    }
    backoff_limit = 2
  }

  wait_for_completion = true

  timeouts {
    create = "2m"
  }
}
Initialize Terraform and preview the plan

Terraform needs to download the Kubernetes provider plugin before it can do anything with your configuration. After that, you can preview exactly what Terraform will create without actually creating it.

  • Download the Kubernetes provider by running this command:
terraform init
  • You should see output that includes lines like Initializing provider plugins... followed by Installed hashicorp/kubernetes and ending with Terraform has been successfully initialized!.

Seeing an error during init?

  • Make sure your PowerShell is inside the k8s-mission-deploy folder where main.tf lives. Run ls to confirm you can see main.tf in the output.
  • If you see a syntax error, check that every opening brace { has a matching closing brace } in your main.tf file.
  • If you see a network error, make sure you are connected to the internet. Terraform needs to download the provider from the Terraform Registry.

help me fix my terraform init error

  • Preview what Terraform will create by running:
terraform plan
  • You should see a plan showing 1 resource to add. Look for kubernetes_job_v1.mission in the output.
  • Scroll through the plan output and notice the security_context settings showing run_as_non_root = true and the resources block with your CPU and memory limits.

What is terraform plan?

The terraform plan command is a dry run. It reads your configuration, connects to the cluster, and shows you exactly what it would create, change, or destroy. Nothing is actually deployed yet.

This gives you a chance to review the security settings before committing to the deployment.

Plan shows an error about the cluster?

  • Make sure Docker Desktop is running and Kubernetes shows the green indicator in the Docker Desktop dashboard.
  • Confirm your kubectl context is correct by running kubectl config current-context. It should print docker-desktop.
  • If it prints a different context, switch back by running kubectl config use-context docker-desktop.

help me fix my terraform plan connection error

Your Terraform configuration is ready and validated. Next up, you will run terraform apply to deploy the container to Kubernetes and watch the mission run inside your cluster.

Deploy to Kubernetes and Verify

In the previous step, you wrote and previewed your Terraform configuration. The plan confirmed that Terraform knows exactly what to create.

Now it's time to make it real. You'll run terraform apply to deploy your container as a Kubernetes Job, then use kubectl to confirm the security hardening is actually enforced at runtime.

In this step, get ready to:
  • Run terraform apply to deploy your Job to Kubernetes.
  • Inspect the Job and Pod with kubectl and read your mission output.
  • Verify that all security contexts are applied to the running Pod.
Deploy the Job with terraform apply

The terraform apply command takes the plan you previewed and creates the actual resources in your cluster. Because your configuration sets wait_for_completion = true, Terraform will block until the Job finishes running.

  • Deploy the Kubernetes Job by running this command:
terraform apply
  • Terraform will show you the same plan and ask for confirmation. Type yes and press Enter.

Terraform will show a "Creating..." message, then wait for the Job to complete. After about 10-15 seconds you should see a completion message confirming the resource was created.

What is happening behind the scenes?

Terraform sends a request to the Kubernetes API running inside Docker Desktop. Kubernetes creates a Job object, which in turn creates a Pod. The Pod pulls your local mission-deploy:1.0 image (no registry needed because image_pull_policy = "Never"), runs the script, and exits.

Terraform watches the Job status until it reports success, then records the created resource in a terraform.tfstate file in your project folder.

Seeing a timeout or ImagePullBackOff error?

  • Make sure Docker Desktop is running and the Kubernetes indicator is green. If it's yellow or absent, go to Settings > Kubernetes and re-enable it.
  • If you see an image pull error, confirm your image exists by running docker images and checking for mission-deploy:1.0. The tag must match exactly.
  • If the Job times out, the 2-minute window may have expired because Kubernetes was still starting up. Run terraform destroy to clean up, then try terraform apply again.

Help me troubleshoot my terraform apply error

Inspect the Job and read the mission logs

Terraform told you the Job succeeded, but you can also inspect it directly through kubectl. This is how you'd monitor workloads in a real cluster.

  • Check the Job status by running:
kubectl get jobs

You should see mission-deploy-job with COMPLETIONS showing 1/1. This means the Job ran one Pod and it completed successfully.

  • Find the Pod that ran your script by running:
kubectl get pods

You should see a Pod with a name like mission-deploy-job-abc12 in Completed status. Copy this Pod name for the next command.

  • Read the mission output from your container by running:
kubectl logs [[PODNAME="your pod name e.g. mission-deploy-job-abc12"]]

You should see the full deployment simulation output in your terminal. This is the same output you saw when running the container with docker run, but now it ran inside your Kubernetes cluster.

Why is the hostname different from last time?

The hostname in the logs will be something like mission-deploy-job-abc12. Kubernetes assigns each Pod a unique hostname based on the Pod name. When you ran the container with Docker directly, Docker generated a random container ID as the hostname instead.

Pod not showing as Completed?

  • If the Pod shows Error status, check the logs with kubectl logs to see the error message from the container.
  • If you see no pods at all, the Job may still be pending. Wait a few seconds and run kubectl get pods again.

Help me figure out why my Kubernetes Pod isn't completing

Verify the security context is applied

Your Terraform configuration declared several security settings. You can confirm Kubernetes actually enforced them by inspecting the Pod's YAML spec.

  • View the full Pod specification by running:
kubectl get pod [[PODNAME="your pod name e.g. mission-deploy-job-abc12"]] -o yaml

This outputs the full YAML definition of the Pod as Kubernetes sees it. Scroll through the output and look for the securityContext sections.

What should I see in the security context?

You will find two securityContext sections in the output:

  • Pod-level: runAsNonRoot: true and runAsUser: 1000. This tells Kubernetes to reject the Pod if it tries to run as root.
  • Container-level: readOnlyRootFilesystem: true, allowPrivilegeEscalation: false, and all Linux capabilities dropped. This means the container cannot write to disk, cannot gain extra privileges, and has zero kernel capabilities.

These settings are production-grade. They ensure that even if an attacker compromised the container process, they could not escalate privileges or persist changes to the filesystem.

Why two levels of security context?

Kubernetes applies security at both the Pod level and the container level. Pod-level settings (like runAsNonRoot) apply to all containers in the Pod. Container-level settings (like readOnlyRootFilesystem) apply to a specific container only.

This layered approach lets you set a baseline for the whole Pod and then tighten individual containers further.

Your container is running inside Kubernetes with full security hardening confirmed. Next up, you'll push this project to GitHub with a professional README so it's ready to share on LinkedIn.

Push to GitHub with a Professional README

Your container is deployed, verified, and running securely inside Kubernetes. Now it's time to package everything into a Git repository and push it to GitHub so others can see your work.

A professional README turns a project folder into a portfolio piece. It shows recruiters and peers what you built, what security practices you applied, and how to run the project themselves.

In this step, get ready to:
  • Initialize a Git repository with a .gitignore file.
  • Create a professional README.md documenting your project.
  • Push your code to a public GitHub repository.
Initialize your Git repository

Before pushing to GitHub, you need a local Git repository to track your files. You also need a .gitignore file to keep Terraform state files out of version control.

  • Initialize a new Git repository in your k8s-mission-deploy folder by running this command:
git init
  • Create a new file called .gitignore in your k8s-mission-deploy folder and paste this content:
.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl

What does this file do?

The .gitignore file tells Git which files to skip when tracking changes.

  • .terraform/ is the provider plugin cache that Terraform downloads locally. It is large and reproducible.
  • *.tfstate and *.tfstate.backup contain your infrastructure state. These can hold sensitive data and should never be committed.
  • .terraform.lock.hcl is the dependency lock file. It is auto-generated and can be excluded for learning projects.
  • Save .gitignore.
  • Verify the file exists by running:
ls .gitignore

You should see .gitignore listed in the output.

Don't see the file?

On Windows, files starting with a dot can be hidden by default. Make sure you saved the file as .gitignore (with the leading dot) and not as gitignore.txt.

If your text editor added a .txt extension, rename the file by running:

Rename-Item gitignore.txt .gitignore

help me fix my .gitignore file on Windows

Create a professional README

A strong README explains what your project does, why it matters, and how to run it. This is what GitHub displays on your repository homepage.

  • Create a new file called README.md in your k8s-mission-deploy folder and paste the following content:
# Mission Deploy: Docker + Kubernetes + Terraform

A security-hardened containerized deployment-simulator deployed to a local Kubernetes cluster using Terraform. Built as a hands-on project to practice the Docker-to-Kubernetes pipeline with Infrastructure as Code and container security best practices.

## Technologies Used

- **Docker** - Containerization of a Linux bash script with non-root user
- **Kubernetes** - Orchestration via a local cluster (Docker Desktop)
- **Terraform** - Infrastructure as Code to deploy the K8s Job
- **Alpine Linux** - Lightweight container base image

## Security Practices

This project applies production-grade container security:

- **Non-root container**: The Docker image runs as user `mission` (UID 1000), never as root
- **Read-only filesystem**: The container filesystem is mounted read-only at runtime
- **Dropped capabilities**: All Linux capabilities are dropped (no privilege escalation possible)
- **Resource limits**: CPU and memory limits prevent the container from consuming excessive resources
- **Minimal base image**: Alpine Linux minimizes the attack surface

## What It Does

The container runs a bash script that simulates a deployment mission sequence while reporting real system information (hostname, OS, memory, disk, network). When deployed to Kubernetes, the Job runs to completion and the logs can be inspected with kubectl.

## How to Run

### Prerequisites

- Docker Desktop with Kubernetes enabled
- Terraform installed

### Steps

1. Build the Docker image:

```bash
docker build -t mission-deploy:1.0 .
```

2. Deploy to Kubernetes with Terraform:

```bash
terraform init
terraform apply
```

3. Check the logs:

```bash
kubectl get pods
kubectl logs <pod-name>
```

4. Verify security context:

```bash
kubectl get pod <pod-name> -o yaml | findstr "runAs"
```

5. Clean up:

```bash
terraform destroy
```

## Sample Output

```
╔══════════════════════════════════════════╗
║   MISSION: SYSTEM DEPLOYMENT v1.0       ║
╚══════════════════════════════════════════╝

[*] Initializing secure connection......... OK
[*] Scanning network targets............... OK
[*] Loading deployment payload............. OK
[*] Verifying system integrity............. OK

>> Hostname: mission-deploy-job-abc12
>> OS:       Alpine Linux v3.21
>> User:     mission (non-root)
...
[*] Security scan......................... PASSED

╔══════════════════════════════════════════╗
║        MISSION ACCOMPLISHED             ║
║   All systems operational. Status: GO   ║
╚══════════════════════════════════════════╝
```

## Architecture

```
[mission.sh] --> [Docker Image] --> [Kubernetes Job] --> [Pod runs to completion]
                      |                    |
              (non-root user)     (security context:
                                   read-only fs,
                                   drop ALL caps,
                                   resource limits)
                                         ^
                                         |
                                   [Terraform apply]
```

## Author

Built as part of my Cloud Engineering learning path.

What does this README cover?

  • Technologies Used lists the four tools in your stack.
  • Security Practices documents the five hardening measures you applied.
  • How to Run gives someone else step-by-step instructions to reproduce your deployment.
  • Sample Output and Architecture provide visual proof of what the project does.
  • Save README.md.
Commit and push to GitHub

With all your project files ready, you can commit them to Git and push to a remote GitHub repository.

  • Stage all your files and create your first commit by running these commands:
git add .
git commit -m "Initial commit: security-hardened K8s mission deploy"

What do these commands do?

  • git add . stages all files in the current folder (except those listed in .gitignore).
  • git commit -m saves a snapshot of your staged files with a descriptive message.
  • Open your browser and go to github.com.
  • Click the + icon in the top right corner, then select New repository.
  • Enter k8s-mission-deploy as the repository name.
  • Leave the repository set to Public.
  • Do not check any boxes for README, .gitignore, or license (you already have these files locally).
  • Click Create repository.

GitHub shows a page with setup instructions. You will use the commands for pushing an existing repository.

  • Connect your local repository to GitHub and push your code by running these commands:
git branch -M main
git remote add origin [[YOUR_REPO_URL="https://github.com/YOUR_USERNAME/k8s-mission-deploy.git"]]
git push -u origin main

What do these commands do?

  • git branch -M main renames your default branch to main.
  • git remote add origin connects your local repo to the GitHub repository you just created.
  • git push -u origin main uploads your commits to GitHub and sets main as the default push target.
  • Refresh your GitHub repository page in the browser.

You should see all five files listed (mission.sh, Dockerfile, main.tf, .gitignore, README.md) and your README rendered below them.

Push rejected or authentication error?

If Git asks for a username and password, GitHub no longer accepts passwords for HTTPS push. You need a Personal Access Token (PAT) instead.

  • Go to GitHub Settings then Developer settings then Personal access tokens then Tokens (classic).
  • Generate a new token with the repo scope.
  • When Git prompts for your password, paste the token instead.

help me authenticate with GitHub from PowerShell

✔️ Awesome, I've got everything!

Great. Double check you have saved all your files and that your GitHub repository page shows the rendered README.

ⓧ I'd like to double check the full code

Here is the complete .gitignore file:

.terraform/
*.tfstate
*.tfstate.backup
.terraform.lock.hcl

Here is the complete README.md file:

# Mission Deploy: Docker + Kubernetes + Terraform

A security-hardened containerized deployment-simulator deployed to a local Kubernetes cluster using Terraform. Built as a hands-on project to practice the Docker-to-Kubernetes pipeline with Infrastructure as Code and container security best practices.

## Technologies Used

- **Docker** - Containerization of a Linux bash script with non-root user
- **Kubernetes** - Orchestration via a local cluster (Docker Desktop)
- **Terraform** - Infrastructure as Code to deploy the K8s Job
- **Alpine Linux** - Lightweight container base image

## Security Practices

This project applies production-grade container security:

- **Non-root container**: The Docker image runs as user `mission` (UID 1000), never as root
- **Read-only filesystem**: The container filesystem is mounted read-only at runtime
- **Dropped capabilities**: All Linux capabilities are dropped (no privilege escalation possible)
- **Resource limits**: CPU and memory limits prevent the container from consuming excessive resources
- **Minimal base image**: Alpine Linux minimizes the attack surface

## What It Does

The container runs a bash script that simulates a deployment mission sequence while reporting real system information (hostname, OS, memory, disk, network). When deployed to Kubernetes, the Job runs to completion and the logs can be inspected with kubectl.

## How to Run

### Prerequisites

- Docker Desktop with Kubernetes enabled
- Terraform installed

### Steps

1. Build the Docker image:

```bash
docker build -t mission-deploy:1.0 .
```

2. Deploy to Kubernetes with Terraform:

```bash
terraform init
terraform apply
```

3. Check the logs:

```bash
kubectl get pods
kubectl logs <pod-name>
```

4. Verify security context:

```bash
kubectl get pod <pod-name> -o yaml | findstr "runAs"
```

5. Clean up:

```bash
terraform destroy
```

## Sample Output

```
╔══════════════════════════════════════════╗
║   MISSION: SYSTEM DEPLOYMENT v1.0       ║
╚══════════════════════════════════════════╝

[*] Initializing secure connection......... OK
[*] Scanning network targets............... OK
[*] Loading deployment payload............. OK
[*] Verifying system integrity............. OK

>> Hostname: mission-deploy-job-abc12
>> OS:       Alpine Linux v3.21
>> User:     mission (non-root)
...
[*] Security scan......................... PASSED

╔══════════════════════════════════════════╗
║        MISSION ACCOMPLISHED             ║
║   All systems operational. Status: GO   ║
╚══════════════════════════════════════════╝
```

## Architecture

```
[mission.sh] --> [Docker Image] --> [Kubernetes Job] --> [Pod runs to completion]
                      |                    |
              (non-root user)     (security context:
                                   read-only fs,
                                   drop ALL caps,
                                   resource limits)
                                         ^
                                         |
                                   [Terraform apply]
```

## Author

Built as part of my Cloud Engineering learning path.

Your project is now live on GitHub with a polished README that documents your security practices and architecture. Next up, you will clean up your Kubernetes resources using Terraform.

Secret mission

Schedule Your Mission as a CronJob

Your deployment mission runs once and exits. What if it could run automatically every few minutes, like a real monitoring job? Upgrade your Terraform configuration to deploy a Kubernetes CronJob that runs on a schedule and collects logs from multiple runs.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely on your local machine, so there are no ongoing cloud costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Kubernetes Job mission-deploy-job (running in your local cluster).
  • Docker image mission-deploy:1.0 (stored in your local Docker cache).
  • Kubernetes enabled in Docker Desktop (uses system memory).
  • Docker Desktop application (installed in Step 1).
  • Project folder k8s-mission-deploy on your Desktop.
  • GitHub repository k8s-mission-deploy.

Keep everything running

No action needed. Choose this if you are still actively building or plan to extend this project soon.

  • Your Kubernetes Job has already completed, so it is not consuming CPU or memory.
  • Docker Desktop and its Kubernetes cluster will continue using system memory in the background.
  • Your project folder and GitHub repository remain as-is.

Pause - I'll come back to this later

Shut down running processes to free up system memory while keeping your files intact.

  • Remove the Kubernetes Job by running this command in PowerShell from your k8s-mission-deploy folder:
terraform destroy
  • Type yes when prompted to confirm.
  • Disable Kubernetes in Docker Desktop to free system memory. Open Docker Desktop, go to Settings, select the Kubernetes tab, uncheck Enable Kubernetes, and click Apply.
  • Optionally, quit Docker Desktop from the system tray to free additional memory.
  • Your project folder and GitHub repository remain intact for when you return.

Delete - I don't want to use this again

Remove all project resources and start fresh. Follow each step below.

  • Remove the Kubernetes Job by running this command in PowerShell from your k8s-mission-deploy folder:
terraform destroy
  • Type yes when prompted to confirm.
  • Verify the cleanup by running:
kubectl get jobs
kubectl get pods
  • Both commands should return "No resources found" or only show system pods.
  • Remove the Docker image by running:
docker rmi mission-deploy:1.0
  • Disable Kubernetes in Docker Desktop. Open Docker Desktop, go to Settings, select the Kubernetes tab, uncheck Enable Kubernetes, and click Apply.
  • To uninstall Docker Desktop entirely, open Windows Settings, go to Apps, then Apps & features. Select Docker Desktop and click Uninstall.
  • Delete the project folder by running:
cd ~/Desktop
Remove-Item -Recurse -Force k8s-mission-deploy
  • Delete the GitHub repository. Open your repository page on GitHub, click Settings, scroll down to the Danger Zone section, and click Delete this repository. Type the repository name to confirm.

Mission Accomplished!

Mission Accomplished!

Nice work! You have built a security-hardened Docker container, deployed it to a local Kubernetes cluster with Terraform, and published the whole project on GitHub.

Here is what you learned:

  • Built a security-hardened Docker image that runs as a non-root user with a minimal Alpine base, and verified container isolation by comparing hostnames across multiple runs.
  • Deployed a Kubernetes Job using Terraform with production-grade security contexts (read-only filesystem, dropped capabilities, resource limits) and cleaned everything up with a single terraform destroy command.
  • Published a portfolio-ready GitHub repository with a professional README documenting your architecture, security practices, and how to run the project.
  • Secret Mission: Scheduled a recurring CronJob that runs your mission on a timer and collects logs from multiple runs.

Ready to quiz yourself?