Module 2 · Published

Module 2: Service Boundaries, Runtime Isolation, and Containers

Examine what makes services independently deployable, how containers create repeatable runtime boundaries, and how partial failure appears when services run separately.

Learning Objectives

  1. Explain the difference between a process boundary and a service boundary.
  2. Describe why independently deployable services improve isolation and operational flexibility.
  3. Distinguish container images from running containers.
  4. Containerize two independently running services.
  5. Configure services using environment variables rather than hardcoded values.
  6. Explain container networking, ports, host mappings, and service names.
  7. Demonstrate partial failure by stopping one service while another remains available.
  8. Explain health checks, restart policies, and basic resource limits.
  9. Identify common container security risks.
  10. Review AI-generated Dockerfiles and container configuration critically.

Module Overview

Module 1 introduced partial failure and network uncertainty. Module 2 makes those ideas concrete by separating two small services into independent runtime units.

The central question is: what makes two services truly independent rather than merely two pieces of code in the same project? Containers give us a practical way to explore that question, but the course focus is not Docker trivia. The focus is distributed-systems reasoning: boundaries, configuration, deployment independence, failure isolation, observability, and operational judgment.

Why This Topic Matters

Enterprise systems depend on services that can be deployed, restarted, scaled, and debugged independently. If Service A and Service B always ship as one unit, share hidden configuration, and fail together, they are not operationally independent even if their source code lives in different folders.

Containers help teams make boundaries explicit. They package runtime dependencies, define entry points, expose ports, attach networks, set environment variables, and create a repeatable way to start each service. That repeatability is what lets us test partial failure in class instead of only talking about it.

Required Preparation

  • Review Module 1’s discussion of partial failure and lost responses.
  • Install Docker Desktop, Docker Engine, Podman, or another instructor-approved OCI-compatible runtime.
  • Confirm that you can run docker --version or an equivalent runtime command.
  • Bring your Module 1 Service A and Service B notes or starter code.
  • Skim the Dockerfile and networking references listed at the end.

Core Concepts

Process Boundary

A runtime separation between operating system processes. Processes have separate memory and can fail independently, but they may still be deployed and operated as one unit.

Service Boundary

A design and operational boundary where a component has clear ownership, configuration, deployment, health, and failure behavior.

Deployment Unit

The artifact that is built, released, rolled back, monitored, and operated independently.

Container Image

An immutable package containing application code, runtime dependencies, metadata, and a default command.

Running Container

A live process created from an image with runtime configuration, networking, storage, limits, and environment variables.

Container Network

A network namespace where containers can reach each other by service name instead of assuming localhost.

Runtime Configuration

Values supplied outside the image, commonly through environment variables, so the same image can run in different environments.

Failure Isolation

The ability for one service to stop, restart, or degrade while other services remain observable and available.

Process Boundaries and Service Boundaries

A process boundary means the operating system can run two programs separately. A service boundary means the system can treat a component as an independently owned, configured, deployed, observed, and failed unit.

Two processes on one host can still be tightly coupled if they share a deploy step, assume local filesystem paths, depend on hardcoded ports, or cannot be restarted independently. A useful service boundary makes those assumptions visible and controllable.

Two Services Running as Processes on One Host

flowchart LR
  host[Host machine]
  procA[Service A process]
  procB[Service B process]
  shared[Shared runtime config]
  host --> procA
  host --> procB
  procA --> shared
  procB --> shared

Service A and Service B run as separate processes on the same host. They have process isolation, but they may still share a deployment script, filesystem, environment, and failure domain. This is not automatically a clean service boundary.

Runtime Isolation with Containers

A container is not a tiny virtual machine. It is a process with controlled runtime isolation. The container runtime gives it a filesystem view, environment, network namespace, user settings, and resource constraints. That makes it easier to reproduce and inspect service behavior.

Two Services Running in Separate Containers

flowchart LR
  runtime[Container runtime]
  net[Shared container network]
  containerA[Container for Service A]
  containerB[Container for Service B]
  imageA[Image A]
  imageB[Image B]
  runtime --> containerA
  runtime --> containerB
  imageA --> containerA
  imageB --> containerB
  containerA --> net
  containerB --> net

Service A and Service B run in separate containers with their own runtime dependencies, environment variables, ports, and filesystem views. The host still provides the container runtime, but each service can be built, started, stopped, and inspected independently.

Image Versus Container

An image is the artifact you build. A container is what runs. The same image can produce many containers with different environment variables, ports, networks, names, or storage attachments.

Image Build to Container Runtime Lifecycle

flowchart LR
  source[Source code]
  deps[Dependency file]
  dockerfile[Container definition]
  image[Container image]
  runtime[Container runtime]
  c1[Container instance one]
  c2[Container instance two]
  source --> image
  deps --> image
  dockerfile --> image
  image --> runtime
  runtime --> c1
  runtime --> c2

Source code, dependency files, and a container definition produce an image. The runtime starts one or more containers from that image with environment variables, port mappings, networks, and storage configuration.

Ports, Networks, and Service Names

Published ports make a container reachable from the host. Container networks let containers call each other. The distinction matters because localhost changes meaning depending on where the code runs.

Host Port to Container Port Mapping

flowchart LR
  browser[Browser]
  host[Host port 8080]
  container[Service A container]
  app[App listens on port 5000]
  browser --> host
  host --> container
  container --> app

The host receives traffic on port 8080 and forwards it to port 5000 inside the Service A container. Inside the container, the application still listens on port 5000.

Service A Calls Service B Through a Container Network

flowchart LR
  net[Shared container network]
  serviceA[Service A container]
  serviceB[Service B container]
  endpoint[service-b port 5001]
  serviceA --> net
  net --> endpoint
  endpoint --> serviceB

Service A reaches Service B using the name service-b on a shared container network. It does not call localhost, because localhost inside Service A points back to Service A itself.

Configuration and Secret Boundaries

Hardcoded endpoint URLs make a service brittle. A service should learn the location of its dependencies through runtime configuration. In this module, simple environment variables are enough. In production systems, secrets should come from a protected secret-management path rather than source code, image layers, or copied local files.

Configuration and Secret Boundaries

flowchart TB
  image[Image with code and dependencies]
  env[Environment variables]
  secrets[Secret source]
  container[Running container]
  risk[Do not copy secrets into image]
  image --> container
  env --> container
  secrets --> container
  secrets -.-> risk

A container image contains code and dependencies. Runtime configuration supplies endpoint names and ports. Secrets should come from a secret source or protected runtime mechanism, not from committed code or the image build context.

Partial Failure, Health, and State

When Service B stops, Service A may still be healthy. That does not mean the whole workflow is healthy. Service A must decide whether to return a controlled error, retry, degrade, or queue work for later. The logs should tell that story clearly.

Service B Fails While Service A Remains Running

flowchart LR
  client[Client]
  serviceA[Service A running]
  serviceB[Service B stopped]
  logs[Logs]
  client --> serviceA
  serviceA -. request fails .-> serviceB
  serviceA --> logs

Service A stays available after Service B stops. Requests that require Service B fail or degrade, while health checks and logs show that Service A itself is still running. This is partial failure in a small containerized system.

Health Check Failure Followed by Container Restart

flowchart LR
  runtime[Container runtime]
  check[Health check]
  unhealthy[Marked unhealthy]
  restart[Restart container]
  healthy[Healthy again or still failing]
  runtime --> check
  check --> unhealthy
  unhealthy --> restart
  restart --> healthy

The runtime checks a container health endpoint. If health checks fail, the runtime marks the container unhealthy and may restart it according to policy. The restarted process may still need dependencies and state to be healthy.

Persistent Volume Versus Ephemeral Container Storage

flowchart LR
  c1[Running container]
  temp[Ephemeral container storage]
  volume[Persistent volume]
  c2[Replacement container]
  c1 --> temp
  c1 --> volume
  temp -. lost on remove .-> c2
  volume --> c2

Ephemeral container storage is lost when the container is removed. A persistent volume stores selected data outside the container lifecycle so it can be reused by a replacement container.

Enterprise Case Study

Checkout Services in Separate Containers

Context
A retailer separates cart, pricing, payment, and fulfillment services so each team can deploy and operate its own component.
System tension
The payment service restarts during a promotion. The cart service is still running, but checkout requests that need payment authorization must fail gracefully and produce useful telemetry.
Lesson
Runtime isolation helps reduce blast radius, but service boundaries still need timeout behavior, clear configuration, health checks, persistent state decisions, logs, and operational ownership.

Worked Example

Suppose Service A exposes a public endpoint on host port 8080 and calls Service B on the shared container network.

  1. Build an image for Service A.
  2. Build an image for Service B.
  3. Create a shared network.
  4. Run Service B with name service-b.
  5. Run Service A with SERVICE_B_URL=http://service-b:5001.
  6. Publish Service A from container port 5000 to host port 8080.
  7. Stop Service B and call Service A again.
  8. Inspect Service A logs to explain the partial failure.

The important idea is not the exact command syntax. The important idea is that Service A now depends on a network-visible service name and a configured endpoint, not a hardcoded localhost assumption.

In-Class Discussion Prompts

  • When does a code module become a service?
  • What should Service A return when Service B is stopped?
  • What does a health check need to prove?
  • What state should survive a container restart?
  • Which Dockerfile choices change the security boundary?

Lab: Containerizing Independent Services

This lab builds on the Module 1 Service A and Service B exercise. You will containerize both services, run them independently, connect them through a shared container network, observe partial failure, and explain what recovered and what did not.

Lab Objectives

  • Containerize Service A.
  • Containerize Service B.
  • Run each service independently.
  • Connect services through a shared container network.
  • Configure the Service B endpoint through an environment variable.
  • Stop one service and observe partial failure.
  • Restart the service and observe recovery.
  • Inspect logs.
  • Explain what data or state was lost.
  • Document one improvement for graceful failure handling.

Prerequisites

  • Docker, Podman, or another approved OCI-compatible runtime.
  • A terminal and text editor.
  • Two small HTTP services from the Module 1 activity, or the baseline Flask structure below.
  • Basic comfort running commands from a project folder.

Expected Folder Structure

module-02-lab/
+-- service-a/
|   +-- app.py
|   +-- requirements.txt
|   +-- Dockerfile
|   +-- .dockerignore
+-- service-b/
|   +-- app.py
|   +-- requirements.txt
|   +-- Dockerfile
|   +-- .dockerignore
+-- compose.yaml

Baseline Service A

import os
import requests
from flask import Flask, jsonify

app = Flask(__name__)
service_b_url = os.environ.get("SERVICE_B_URL", "http://service-b:5001")

@app.get("/health")
def health():
    return jsonify(status="ok", service="a")

@app.get("/call-b")
def call_b():
    try:
        response = requests.get(f"{service_b_url}/health", timeout=2)
        return jsonify(service="a", dependency=response.json())
    except requests.RequestException as exc:
        app.logger.warning("dependency failure: %s", exc)
        return jsonify(service="a", dependency="unavailable"), 503

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Baseline Service B

from flask import Flask, jsonify

app = Flask(__name__)

@app.get("/health")
def health():
    return jsonify(status="ok", service="b")

if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5001)

Requirements Files

Use explicit dependency versions where reasonable.

Flask==3.0.3
requests==2.32.3

Service B does not need requests unless your version of Service B calls another dependency.

Sample Dockerfile

Use one Dockerfile per service. Adjust the port and dependency list for each service.

FROM python:3.12-slim

WORKDIR /app

RUN useradd --create-home --shell /usr/sbin/nologin appuser

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY app.py .

USER appuser

EXPOSE 5000

HEALTHCHECK --interval=10s --timeout=3s --retries=3 \
  CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:5000/health', timeout=2)"

CMD ["python", "app.py"]

For Service B, expose and check port 5001.

Sample .dockerignore

__pycache__/
.venv/
.env
.git/
*.pyc

Build Images

docker build -t cmpe273-service-a:week2 ./service-a
docker build -t cmpe273-service-b:week2 ./service-b

Create a Network

docker network create cmpe273-week2

Run Service B

docker run --rm \
  --name service-b \
  --network cmpe273-week2 \
  -p 5001:5001 \
  cmpe273-service-b:week2

Open another terminal before starting Service A.

Run Service A

docker run --rm \
  --name service-a \
  --network cmpe273-week2 \
  -e SERVICE_B_URL=http://service-b:5001 \
  -p 8080:5000 \
  cmpe273-service-a:week2

Call Service A from the host:

curl http://localhost:8080/health
curl http://localhost:8080/call-b

Demonstrate Partial Failure

Stop Service B:

docker stop service-b

Call Service A again:

curl -i http://localhost:8080/call-b

Expected observation: Service A still responds, but the dependency call fails with a controlled 503 response. Inspect logs:

docker logs service-a

Restart Service B and observe recovery:

docker run --rm \
  --name service-b \
  --network cmpe273-week2 \
  -p 5001:5001 \
  cmpe273-service-b:week2

Optional Compose Configuration

services:
  service-a:
    build: ./service-a
    ports:
      - '8080:5000'
    environment:
      SERVICE_B_URL: http://service-b:5001
    depends_on:
      - service-b
  service-b:
    build: ./service-b
    ports:
      - '5001:5001'

Run with:

docker compose up --build

Checkpoints

  • Service A and Service B images build successfully.
  • Each service runs in its own container.
  • Service A reaches Service B by service name on a shared network.
  • Service A remains reachable when Service B stops.
  • Service A logs the dependency failure.
  • You can explain whether any state was lost.

Common Errors

  • Using localhost from Service A to reach Service B.
  • Publishing the wrong host or container port.
  • Forgetting to attach both containers to the same network.
  • Copying a local .env file into the image.
  • Running as root without a reason.
  • Writing state only to the container filesystem and expecting it to survive removal.
  • Adding a health check that tests the wrong port.

Troubleshooting Guidance

  • Use docker ps to confirm containers and published ports.
  • Use docker logs service-a and docker logs service-b to inspect behavior.
  • Use docker network inspect cmpe273-week2 to confirm network membership.
  • Rebuild after Dockerfile or dependency changes.
  • Check that Flask binds to 0.0.0.0, not only 127.0.0.1.
  • Confirm that Service A uses SERVICE_B_URL=http://service-b:5001.

Cleanup Commands

docker stop service-a service-b
docker network rm cmpe273-week2
docker image rm cmpe273-service-a:week2 cmpe273-service-b:week2

If you used Compose:

docker compose down

Reflection Questions

  • What changed when Service A stopped assuming localhost?
  • What evidence showed that Service A remained available during Service B failure?
  • Did restart equal recovery? Why or why not?
  • What state would be lost if a container were removed?
  • What would you change before running this design in production?
  • What AI-generated Dockerfile issue would you check first?

Submission Guidance

Submit the required lab artifacts in Canvas when the assignment is released. Do not submit through this portal.

Connection to the Semester Project

Every final project must include at least three independently deployable components. Module 2 starts that path by making independence visible at runtime. Your project should eventually explain each component’s deployment unit, configuration boundary, persistent state, health behavior, failure mode, and security or trust boundary.

Connection to Agentic Systems

Agentic systems often coordinate multiple tools, APIs, model endpoints, memory stores, approval gates, and observability services. Container boundaries help teams test whether an agent can degrade safely when a tool is unavailable, whether risky actions require human approval, and whether logs can reconstruct a multi-step workflow.

Knowledge Check

Service A calls http://localhost:5001 from inside its own container and cannot reach Service B. Why?

Inside a container, localhost means the current container. Service A should call Service B by its service name on the shared container network, such as http://service-b:5001.

What is the difference between an image and a container?

An image is the packaged artifact. A container is a running instance created from that image with runtime configuration, networking, storage, and limits.

A command maps host port 8080 to container port 5000. Which port does the app listen on inside the container?

The app still listens on container port 5000. The host receives traffic on 8080 and forwards it to 5000 inside the container.

Service B crashes while Service A remains available. What distributed-systems concept are you observing?

This is partial failure. One component failed while another component stayed alive and must decide how to respond.

Does a restart policy guarantee recovery?

No. Restarting can bring a process back, but recovery also depends on dependencies, persistent state, configuration, and whether the failure cause has been resolved.

A student writes temporary data inside the container filesystem and then removes the container. What happens to that data?

Ephemeral container storage is lost when the container is removed. Data that must survive should be stored in a volume or external service-owned data store.

Why should endpoint URLs be supplied through environment variables instead of hardcoded into the source?

Runtime configuration lets the same image run in different environments and makes dependencies visible at deployment time.

An AI-generated Dockerfile uses a huge base image, runs as root, copies the whole home directory, and exposes several unused ports. What is wrong?

It increases attack surface, may copy secrets, grants unnecessary privileges, bloats the image, and exposes network surfaces the service does not need.

Why might a health check that only verifies that a process is running be misleading?

The process may be alive but unable to serve useful requests because dependencies are unavailable, configuration is wrong, or the service is stuck internally.

Key Takeaways

  • Two code folders are not enough to make two services independent; runtime, deployment, configuration, ownership, and failure boundaries matter.
  • A container image is a reusable artifact, while a container is a running instance configured for a specific environment.
  • Localhost inside a container means that same container, not another service or the host.
  • Environment variables separate runtime configuration from code and images, but secrets still require careful handling.
  • Health checks and restart policies can restore a process, but recovery also depends on state, dependencies, and correctness.
  • Container security starts with small images, non-root users, clean build context, minimal ports, and critical review of generated configuration.

Additional Resources

  • Dockerfile Reference documentation

    Official reference for Dockerfile instructions and image build behavior.

  • Docker Networking Overview documentation

    Official documentation for bridge networks, published ports, and container name resolution.

  • Open Container Initiative documentation

    Standards body for portable container image and runtime specifications used by Docker, Podman, and other runtimes.

  • OWASP Docker Security Cheat Sheet article

    Practical security checklist for container images, runtime settings, secrets, and host exposure.