DevOps & Cloud Topic

Docker and Kubernetes: Build a Small Microservices Lab

Imagine a shop with products, orders and users. In this lab, three small services return made-up data. You will run them with Docker Compose, then see how Kubernetes runs the same images.

What this example teaches: separate processes, ports, service names and deployment settings. It is not a complete shop. There is no payment system, login or database. The services do not call each other. A real order flow also needs validation, access checks and a plan for partial failures.

Lecturre learning image

Start with a working app

Use app.py and the Dockerfile from the Docker guide, or the example bundle. The APP_ROLE setting chooses products, orders or users. The same code serves different mock data. We reuse one image to keep the first exercise small; real services may have separate code and release schedules.

docker build -t lecturre-web:1.0 .

Run three services with Compose

Save this as compose.microservices.yaml. Build the image first, so all three services can use it.

services:
  products:
    build: .
    image: lecturre-web:1.0
    environment:
      APP_ROLE: products
    ports:
      - "127.0.0.1:8091:8080"
  orders:
    image: lecturre-web:1.0
    environment:
      APP_ROLE: orders
    ports:
      - "127.0.0.1:8092:8080"
  users:
    image: lecturre-web:1.0
    environment:
      APP_ROLE: users
    ports:
      - "127.0.0.1:8093:8080"
docker compose -f compose.microservices.yaml config
docker compose -f compose.microservices.yaml up -d
curl http://127.0.0.1:8091/
curl http://127.0.0.1:8092/
curl http://127.0.0.1:8093/

You should see a product, an order and a user in the three JSON responses. Ports 8091, 8092 and 8093 belong to your computer. Each container listens on its own port 8080. This works because each container has its own network space.

In this Compose network, a future orders service could call http://products:8080/. It would not call localhost:8091, which is a host-side address. Do not add that call just to make the diagram look like microservices: first decide what data the order service really needs and how a failure should work.

What changes in Kubernetes?

A Deployment keeps the requested number of Pods running. A Service gives a stable name to the matching Pods. A label joins them: the Service selector must match the Pod labels. A container port entry describes a port; it does not expose the app to the public internet.

Use the local kind cluster from the Kubernetes lesson. The example bundle has microservices.yaml with a Deployment and Service for each role. Each Deployment sets APP_ROLE and includes health checks and resource limits.

Save this complete file as microservices.yaml:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: products
  namespace: lecturre-lab
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: products
  template:
    metadata:
      labels:
        app: products
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: products
        image: lecturre-web:1.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 50m
            memory: 64Mi
          limits:
            cpu: 250m
            memory: 128Mi
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        startupProbe:
          httpGet:
            path: /health
            port: 8080
          failureThreshold: 12
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          periodSeconds: 10
        env:
        - name: APP_ROLE
          value: products
---
apiVersion: v1
kind: Service
metadata:
  name: products
  namespace: lecturre-lab
spec:
  selector:
    app: products
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: orders
  namespace: lecturre-lab
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: orders
  template:
    metadata:
      labels:
        app: orders
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: orders
        image: lecturre-web:1.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 50m
            memory: 64Mi
          limits:
            cpu: 250m
            memory: 128Mi
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        startupProbe:
          httpGet:
            path: /health
            port: 8080
          failureThreshold: 12
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          periodSeconds: 10
        env:
        - name: APP_ROLE
          value: orders
---
apiVersion: v1
kind: Service
metadata:
  name: orders
  namespace: lecturre-lab
spec:
  selector:
    app: orders
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: users
  namespace: lecturre-lab
spec:
  replicas: 1
  revisionHistoryLimit: 3
  selector:
    matchLabels:
      app: users
  template:
    metadata:
      labels:
        app: users
    spec:
      automountServiceAccountToken: false
      securityContext:
        runAsNonRoot: true
        runAsUser: 10001
        runAsGroup: 10001
        seccompProfile:
          type: RuntimeDefault
      containers:
      - name: users
        image: lecturre-web:1.0
        imagePullPolicy: IfNotPresent
        ports:
        - containerPort: 8080
        resources:
          requests:
            cpu: 50m
            memory: 64Mi
          limits:
            cpu: 250m
            memory: 128Mi
        securityContext:
          allowPrivilegeEscalation: false
          readOnlyRootFilesystem: true
          capabilities:
            drop:
            - ALL
        startupProbe:
          httpGet:
            path: /health
            port: 8080
          failureThreshold: 12
          periodSeconds: 5
        readinessProbe:
          httpGet:
            path: /health
            port: 8080
          periodSeconds: 5
        livenessProbe:
          httpGet:
            path: /health
            port: 8080
          periodSeconds: 10
        env:
        - name: APP_ROLE
          value: users
---
apiVersion: v1
kind: Service
metadata:
  name: users
  namespace: lecturre-lab
spec:
  selector:
    app: users
  ports:
  - port: 80
    targetPort: 8080
kind load docker-image lecturre-web:1.0 --name lecturre-lab
kubectl --context kind-lecturre-lab apply -f k8s/namespace.yaml
kubectl --context kind-lecturre-lab apply -f microservices.yaml
kubectl --context kind-lecturre-lab -n lecturre-lab get pods,services
kubectl --context kind-lecturre-lab -n lecturre-lab rollout status deployment/products
kubectl --context kind-lecturre-lab -n lecturre-lab port-forward service/products 8094:80

Keep the last command open. In another terminal, run curl http://127.0.0.1:8094/. You should receive the product response. Port forwarding gives your machine a temporary path to the Service; it is not a production public endpoint.

Find a broken service

If a Pod is not ready, describe the Pod and read its logs. An image pull error means the runtime cannot get the image. For this local lab, load the image into the named kind cluster. If a Service has no endpoints, check its selector and the Pod's readiness. If the wrong data appears, check APP_ROLE.

kubectl --context kind-lecturre-lab -n lecturre-lab logs deployment/products
kubectl --context kind-lecturre-lab -n lecturre-lab describe deployment/products
kubectl --context kind-lecturre-lab -n lecturre-lab get endpointslices

When should you split an app?

Separate services can help teams release and scale parts independently. They also add network failures, more deployments and harder debugging. A small team can start with one well-organised app. Split it when a clear need is worth the extra work.

Before adding real orders, decide who owns each piece of data. Add timeouts and bounded retries for network calls. Use an idempotency key when a repeated request must not create a second order. Trace requests across services, and protect private data. Kubernetes does not make these decisions for you.

Practice and clean up

Change the product name in the source, build image 1.1, and update only the products Deployment to that image. Load the new image into kind first. Confirm that orders still works. This shows separate deployment settings, even though our teaching services share one code base.

docker compose -f compose.microservices.yaml down
kubectl --context kind-lecturre-lab -n lecturre-lab delete -f microservices.yaml

Stop any port-forward processes too. Only run these commands against the named local lab. Keep real cluster work separate.

Official references and further reading

Reviewed on 25 October 2026. Tools change, so check the official documentation for the version you use.

DevOps learning roadmap · All DevOps & Cloud tutorials

Comments (0)

No comments yet. Be the first to comment!

LecturreAI Assistant

Hello! I'm LecturreAI

What would you like to learn today?