Kubernetes Without Scale: Setting Up a Personal Cluster on a Single EC2 Instance (Part 2)

Kubernetes

YOUR COSMETIC CARE STARTS HERE

Find the Best Cosmetic Hospitals

Trusted • Curated • Easy

Looking for the right place for a cosmetic procedure? Explore top cosmetic hospitals in one place and choose with confidence.

“Small steps lead to big changes — today is a perfect day to begin.”

Explore Cosmetic Hospitals Compare hospitals, services & options quickly.

✓ Shortlist providers • ✓ Review options • ✓ Take the next step with confidence

This is Part 2 of a series on running Kubernetes at personal scale. In Part 1, we covered the rationale: Kubernetes is worth running even for low-traffic, single-user workloads because it makes installing third-party apps as simple as helm install and gives you strong infrastructure-as-code foundations. Here we walk through the full setup — from launching an EC2 instance to deploying a real application.

Our target environment is a single t2.small EC2 instance (2 GB RAM, 1 vCPU, ~$15/month) running Ubuntu 18.04. We will use KIND (Kubernetes IN Docker) to run the cluster, add cert-manager and nginx-ingress for TLS and routing, and finish by deploying HackMD — an open-source collaborative Markdown editor — as a proof of concept.

Set Up a Machine

These instructions target AWS EC2, but the same approach works on any cloud provider or bare-metal host that can run Docker.

Launch the Instance

  1. Open the EC2 console and click Launch Instance.
  2. Choose Ubuntu Server 18.04 LTS as the AMI.
  3. Select the t2.small instance type. Do not go smaller — Kubernetes has significant baseline memory overhead. If you plan to run memory-hungry workloads (databases, CI runners, etc.) consider t2.medium or larger.
  4. Increase the root volume from the default if you intend to store significant data on the node.
  5. In the security group, open port 22 (SSH), port 80 (HTTP), and port 443 (HTTPS).
  6. Download and store your SSH key pair safely.

Assign an Elastic IP

EC2 instances get a new public IP every time they restart. An Elastic IP gives your instance a stable address that you can map to a DNS name.

  1. In the EC2 console, go to Elastic IPs → Allocate Elastic IP address.
  2. Associate the new address with your new instance.

Create DNS Records

Head to Route 53 (or your DNS provider) and create two A records:

  • *.example.com → your Elastic IP (wildcard record covers every subdomain)
  • example.com → your Elastic IP (optional, for the apex domain)

With DNS in place, every subdomain you configure on the cluster will automatically resolve to your server.

Install Dependencies

SSH into your instance, then install the following tools in order.

Docker

KIND runs Kubernetes inside Docker containers, so Docker is the foundational dependency.

sudo apt update
sudo apt install -y apt-transport-https ca-certificates curl software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"
sudo apt update
sudo apt install -y docker-ce
sudo usermod -aG docker $USER
newgrp dockerCode language: PHP (php)

KIND

KIND (Kubernetes IN Docker) creates a fully functional Kubernetes cluster using Docker containers as nodes. This is ideal for a single-machine setup.

curl -Lo ./kind https://github.com/kubernetes-sigs/kind/releases/download/v0.5.1/kind-$(uname)-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kindCode language: JavaScript (javascript)

kubectl

kubectl is the primary CLI for communicating with any Kubernetes cluster. It reads your kubeconfig to know which cluster to talk to.

curl -LO "https://storage.googleapis.com/kubernetes-release/release/$(curl -s https://storage.googleapis.com/kubernetes-release/release/stable.txt)/bin/linux/amd64/kubectl"
chmod +x ./kubectl
sudo mv ./kubectl /usr/local/bin/kubectlCode language: JavaScript (javascript)

Helm

Helm is the package manager for Kubernetes. Helm charts package up all the manifests needed to deploy an application (WordPress, Ghost, MySQL, GitLab, etc.) into a single, versioned, configurable unit.

sudo snap install helm --classic

Reckoner

Reckoner is a Python tool that lets you declare multiple Helm releases in a single YAML file (called a course file), making it easier to manage several charts together with shared configuration and install hooks.

sudo apt-get install -y python3 python3-pip
export PATH=$PATH:~/.local/bin/
pip3 install reckonerCode language: JavaScript (javascript)

Dependency Summary

  • Docker — runs isolated containers (and hosts the KIND cluster nodes).
  • KIND — bootstraps a Kubernetes control plane and worker node inside Docker.
  • kubectl — CLI for managing Kubernetes resources.
  • Helm — installs and upgrades packaged Kubernetes applications.
  • Reckoner — manages multiple Helm charts declaratively from a single YAML file.

Build the Cluster

Create a file called cluster.yaml. Storing this in version control is strongly recommended — it is a complete description of your cluster topology.

cluster.yaml

kind: Cluster
apiVersion: kind.sigs.k8s.io/v1alpha3
nodes:
- role: control-plane
- role: worker
  extraMounts:
  - containerPath: /opt/local-path-provisioner
    hostPath: /home/ubuntu/kind-disk
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    listenAddress: "0.0.0.0"
  - containerPort: 443
    hostPort: 443
    listenAddress: "0.0.0.0"Code language: JavaScript (javascript)

Key decisions in this config:

  • Two roles — a control-plane node runs the Kubernetes API server, scheduler, and controller manager; the worker node runs your application pods.
  • Persistent storage mount — /home/ubuntu/kind-disk on the host is mounted into the worker container so database volumes survive pod restarts. Adjust this path to suit your environment.
  • Port mappings — ports 80 and 443 are forwarded from the host into the worker container so external HTTP/HTTPS traffic reaches the cluster.

Create the cluster and point kubectl at it:

kind create cluster --config cluster.yaml
export KUBECONFIG="$(kind get kubeconfig-path --name='kind')"Code language: JavaScript (javascript)

Verify both nodes are ready (this can take a minute or two):

kubectl get nodesCode language: JavaScript (javascript)

Expected output:

NAME                 STATUS   ROLES    AGE    VERSION
kind-control-plane   Ready    master   109s   v1.15.3
kind-worker          Ready    <none>   73s    v1.15.3Code language: CSS (css)

Configure the Cluster

Install Tiller (Helm’s In-Cluster Component)

Helm 2 requires a server-side component called Tiller. We create a dedicated namespace and give it cluster-admin privileges via RBAC.

tiller.rbac.yaml

apiVersion: v1
kind: Namespace
metadata:
  name: tiller
---
apiVersion: v1
kind: ServiceAccount
metadata:
  name: tiller
  namespace: tiller
---
kind: ClusterRoleBinding
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
  name: tiller-clusterrolebinding
subjects:
- kind: ServiceAccount
  name: tiller
  namespace: tiller
roleRef:
  kind: ClusterRole
  name: cluster-admin
  apiGroup: ""Code language: JavaScript (javascript)
kubectl apply -f tiller.rbac.yaml
export TILLER_NAMESPACE=tiller
helm init --service-account tillerCode language: JavaScript (javascript)

Configure Persistent Storage

KIND ships with a default standard storage class that does not persist data across container restarts. We replace it with Rancher’s local-path-provisioner, which writes data to the host directory we mounted in cluster.yaml.

# Install the local-path-provisioner
kubectl apply -f https://raw.githubusercontent.com/rancher/local-path-provisioner/master/deploy/local-path-storage.yaml

# Remove the default flag from the built-in 'standard' storage class
kubectl patch storageclass standard -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"false", "storageclass.beta.kubernetes.io/is-default-class":"false"}}}'

# Make local-path the new default
kubectl patch storageclass local-path -p '{"metadata": {"annotations":{"storageclass.kubernetes.io/is-default-class":"true", "storageclass.beta.kubernetes.io/is-default-class":"true"}}}'Code language: PHP (php)

Install cert-manager and nginx-ingress

These two components handle the two things every public web service needs: TLS certificates and HTTP routing.

  • cert-manager — automatically provisions and renews TLS certificates from Let’s Encrypt.
  • nginx-ingress — proxies incoming HTTP/HTTPS requests to the correct Kubernetes service based on the hostname.

First, create issuer.yaml to tell cert-manager to use the Let’s Encrypt ACME v2 API. Replace you@example.com with your real email address (Let’s Encrypt uses it for expiry notices).

issuer.yaml

apiVersion: cert-manager.io/v1alpha2
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: you@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
    - http01:
        ingress:
          class: nginx

Do not apply this manifest yet — cert-manager’s CRDs must exist first. Next, create core.course.yaml for Reckoner. The post_install hook on cert-manager applies issuer.yaml automatically after the chart is ready.

core.course.yaml

repositories:
  jetstack:
    url: https://charts.jetstack.io
namespace: default
charts:
  cert-manager:
    hooks:
      pre_install:
      - kubectl apply --validate=false -f https://raw.githubusercontent.com/jetstack/cert-manager/release-0.11/deploy/manifests/00-crds.yaml
      post_install:
      - sleep 30 && kubectl apply -f issuer.yaml
    namespace: cert-manager
    repository: jetstack
  nginx-ingress:
    namespace: nginx-ingress
    values:
      controller:
        hostNetwork: true
        service:
          type: LoadBalancerCode language: JavaScript (javascript)

Run Reckoner to install both charts:

reckoner plot core.course.yamlCode language: CSS (css)

Verify the pods are running (allow a minute or two for images to pull):

$ kubectl get pods -n nginx-ingress
NAME                                             READY   STATUS    RESTARTS   AGE
nginx-ingress-controller-664b77978-gbd59         1/1     Running   0          14s
nginx-ingress-default-backend-576b86996d-fjb4k   1/1     Running   0          14s

$ kubectl get pods -n cert-manager
NAME                                       READY   STATUS    RESTARTS   AGE
cert-manager-6b78b7c997-mblt4              1/1     Running   0          18s
cert-manager-cainjector-54c4796c5d-psdgt   1/1     Running   0          18s
cert-manager-webhook-77ccf5c8b4-8tkwt      1/1     Running   0          17sCode language: JavaScript (javascript)

Install an Application: HackMD

To validate the full stack, we’ll deploy HackMD (now known as CodiMD/HedgeDoc), an open-source, real-time collaborative Markdown editor. It is a lightweight, self-hosted alternative to Google Docs, and it stores all data in a PostgreSQL database that will live in your kind-disk directory.

Create the Ingress

The Ingress resource tells nginx-ingress how to route traffic for hackmd.example.com and instructs cert-manager to obtain a TLS certificate. Replace both occurrences of example.com with your real domain.

ingress.yaml

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubernetes.io/ingress.class: nginx
    cert-manager.io/cluster-issuer: letsencrypt-prod
    kubernetes.io/tls-acme: "true"
  name: hackmd
  namespace: hackmd
spec:
  rules:
    - host: hackmd.example.com
      http:
        paths:
          - backend:
              serviceName: hackmd
              servicePort: 3000
            path: /
  tls:
  - secretName: tls-prod-cert
    hosts:
    - hackmd.example.com

Create the Reckoner App Course File

Add HackMD to a separate course file for application-layer charts. As you deploy more apps, add them here.

apps.course.yaml

namespace: default
charts:
  hackmd:
    namespace: hackmd
    hooks:
      post_install:
      - kubectl apply -f ./ingress.yamlCode language: JavaScript (javascript)

Deploy it:

reckoner plot apps.course.yamlCode language: CSS (css)

After a few moments, HackMD should be accessible at https://hackmd.example.com. cert-manager will automatically obtain a Let’s Encrypt certificate. Data will be stored under /home/ubuntu/kind-disk on the host.

Adding More Applications

The pattern for every future application is the same:

  1. Add a chart entry to apps.course.yaml.
  2. Create a corresponding ingress.yaml pointing to your domain.
  3. Run reckoner plot apps.course.yaml.

Because all your configuration lives in YAML files committed to Git, you can recreate the entire cluster — including all applications — on a new machine by replaying the same commands. This is the core value proposition of infrastructure as code at personal scale.

Summary

ComponentRole
KINDRuns a Kubernetes cluster inside Docker on a single host
kubectlCLI for all cluster interactions
Helm + TillerInstalls and manages packaged Kubernetes applications
ReckonerDeclares multiple Helm releases in one YAML file
local-path-provisionerProvides persistent storage backed by a host directory
nginx-ingressRoutes external HTTP/HTTPS traffic to the right service
cert-managerAutomatically provisions and renews Let’s Encrypt TLS certs

Running Kubernetes at personal scale adds some operational complexity, but it pays off quickly: every application you self-host follows the same installation and upgrade pattern, your infrastructure is fully version-controlled, and migrating to a larger machine requires nothing more than replaying your playbooks.

Original article sourced from containerjournal.com (2019).

0 0 votes
Article Rating
Subscribe
Notify of
guest

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
0
Would love your thoughts, please comment.x