Add a Caddy Gateway to OrderFlow

Route private OrderFlow APIs through a local Caddy gateway.

Introduction

30 Second Summary

A system becomes harder to control when every part has its own door to your computer. Different paths make failures harder to trace.

In this project, you will add Caddy as the single local API gateway for Order, Inventory, and Catalog. You will prove that a Catalog outage leaves Order available through the same entry point.

What You'll Build

Your finished OrderFlow stack serves Catalog, Order, and Inventory traffic through http://127.0.0.1:8080 while keeping each backend private.

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

  • A single gateway address at http://127.0.0.1:8080 for Catalog, Order, and Inventory requests. You can trace those requests in JSON access logs.
  • A gateway checkout flow that returns HTTP 202 for a submitted order. You can find the same client reference in /processed-orders.
  • Private backend APIs whose direct host ports no longer accept connections. The same paths stay available through Caddy.
  • Secret Mission: Isolate a Catalog outage while Order remains available through the gateway. Restore the product route after the test.

Are there any prerequisites?

You need the completed OrderFlow Projects 1 through 6 persistent-checkout workspace on Linux. The first step verifies each required local tool before you change the stack.

Before We Start

Before the hands-on work begins, lock in the boundary you are about to create. You are adding a loopback-only Caddy gateway at http://127.0.0.1:8080 while preserving the inherited FastAPI routes and Project 6 readiness checks.

Restore the Hardened Baseline

A Caddy gateway adds a new network boundary to OrderFlow. An inherited application fault could look like a gateway routing fault once that boundary exists.

This baseline check gives you a known-good starting point. You will verify the local tools before proving that the completed Project 6 stack is healthy.

In this step, get ready to:
  • Verify the required Linux tool versions.
  • Confirm the completed Project 6 workspace artifacts.
  • Start the inherited stack to prove its readiness.
Verify the Linux tools

The gateway workflow depends on Docker Engine. It also uses Docker Compose to manage the inherited stack.

You will edit the workspace in Visual Studio Code. You will test each gateway route with the curl CLI.

  • Check every required tool by running these commands:
docker version
docker compose version
code --version
curl --version

What do these checks prove?

  • The docker version command confirms that the Docker CLI can reach the local daemon. Its output must include both Client and Server sections.
  • The docker compose version command confirms that the Compose plugin is available.
  • The code --version command confirms that the Visual Studio Code CLI is available.
  • The curl --version command confirms that curl can report its linked libraries and supported features.

✔️ Every tool reports version information

Your toolchain is ready for the inherited stack. The expected baseline uses Docker Engine 29.8.0. It uses Docker Compose v5.5.0 and Visual Studio Code 1.136.

Good work. You have also proved that your terminal can communicate with the local Docker daemon.

ⓧ One tool reports an older version

An older tool can interpret the Compose model differently. Update each affected product through the package method for your Linux distribution.

ⓧ One or more commands are missing

Linux package commands differ across distributions. Use the official installation page for each missing product.

Docker cannot reach the daemon?

Confirm that Docker Engine is running through the method documented for your Linux distribution. Check that your current user has permission to access the local daemon.

Run the version check again after correcting the daemon access. The result must include both Client and Server information.

Get help diagnosing the local Docker connection.

Inspect the completed Project 6 workspace

The gateway must extend the completed Project 6 workspace. The inherited Compose files contain the readiness behavior that later steps preserve.

What should the workspace contain?

  • The top-level compose.yaml file.
  • The top-level compose.override.yaml file.
  • The top-level compose.images.yaml file.
  • The order-service folder.
  • The inventory-service folder.
  • The catalog-service folder.
  • The worker at inventory-service/worker.py.
  • A hardened Dockerfile inside each API service folder.
  • Start Visual Studio Code through your Linux application launcher.
  • Use its folder picker to select the completed persistent-checkout folder.
  • Expand the service folders in the file tree.
  • Compare the visible workspace with the baseline checklist above.
  • Switch to the integrated terminal inside persistent-checkout.

Your completed Project 6 workspace is now open. Every gateway change in the remaining steps starts from this hardened baseline.

Is a baseline artifact missing?

Confirm that you selected the completed persistent-checkout folder from Project 6. A workspace from an earlier OrderFlow project may not contain the hardened Dockerfiles or the worker.

Get help identifying the missing Project 6 artifact.

Validate and start the inherited model

Docker Compose merges the three inherited files into one runtime model. Listing its services first catches an incomplete workspace before any containers start.

  • Resolve the inherited service list by running this command:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml config --services

What does this command confirm?

The command merges the base model with both Project 6 overlays. A complete inherited baseline prints nine service names.

  • Count the service names in the output.

You should see nine inherited services. This proves that all three Compose files resolve as one model.

Do you see fewer than nine services?

Check that the integrated terminal is inside persistent-checkout. Confirm that each Compose filename matches the baseline checklist exactly.

Get help comparing the resolved service list with the Project 6 workspace.

The first build can take a few minutes while Docker processes the hardened images. A quiet stretch during the build is normal.

  • Build the inherited images and wait for service readiness by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml up --build --wait

What does this startup command do?

The command builds the inherited API images before starting the stack. The wait option keeps the command active until services are running or healthy.

The terminal returns to its prompt once the readiness gate completes. That gives the final status check a stable stack to inspect.

Did the readiness gate fail?

Treat this as an inherited Project 6 problem. Resolve the unhealthy service before adding any Caddy configuration.

Review the service named by the startup output. Check its Project 6 Dockerfile or dependency health before repeating the same startup command.

Get help isolating the inherited service failure.

Before you check, predict which APIs should show health states and which worker should show only a running state.

  • Inspect the inherited service states by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml ps

How to read the baseline status

  • The order-service API reports healthy.
  • The inventory-service API reports healthy.
  • The catalog-service API reports healthy.
  • The inventory-worker service reports running.
  • The inherited databases and Redis services report running or healthy.
  • The inherited RabbitMQ service reports running or healthy.

That is the baseline restored. Your hardened OrderFlow stack is healthy before the gateway changes begin.

Does a service show an unhealthy state?

Use the service name in the status output to trace the failure back to Project 6. Repeat the startup command after correcting that inherited issue.

Get help interpreting the unhealthy service state.

Your local tools and inherited services are ready. Next, you will put Catalog behind Caddy to create the first gateway route.

Put Catalog Behind Caddy

The inherited OrderFlow stack is healthy. Its APIs still use separate host addresses, so there is no single place to control request routing.

This step adds Caddy as a reverse proxy for Catalog. The first route gives you a visible gateway win before a second path reveals what the current routing table can reach.

In this step, get ready to:
  • Create a Caddy route that forwards Catalog requests by service name.
  • Extend the Compose model with the gateway and Catalog network boundary.
  • Compare a routed Catalog request with an unmapped Order request.
Create the first Caddyfile

The Caddyfile is the gateway's routing table. Path matchers select the incoming requests that each handler receives.

  • In the VS Code Explorer sidebar, use the new-folder control to create gateway inside persistent-checkout.
  • Use the new-file control beside gateway to create Caddyfile.
  • Add the first gateway route by pasting this configuration into gateway/Caddyfile:
:8080 {
	log {
		output stdout
		format json
	}

	@health path /gateway-health
	handle @health {
		respond 200
	}

	@catalog path /products /products/*
	handle @catalog {
		reverse_proxy catalog-service:8000
	}

	handle {
		respond "route not found" 404
	}
}

What does this Caddyfile do?

  • The :8080 site listens for local HTTP requests on port 8080.
  • The log block sends JSON access logs to the container output.
  • The @health matcher gives Compose a lightweight readiness endpoint.
  • The @catalog matcher preserves product paths while forwarding them to catalog-service:8000.
  • The final handle returns the custom fallback for every unmatched path.
  • Save gateway/Caddyfile.
  • Validate the saved Caddy configuration by running:
docker run --rm -v "$PWD/gateway:/etc/caddy:ro" caddy:2.11.4 caddy validate --config /etc/caddy/Caddyfile

What does this validation do?

Docker mounts your local gateway directory into a temporary Caddy container as read-only. Caddy then parses /etc/caddy/Caddyfile without changing the running OrderFlow stack.

You should see confirmation that the configuration is valid. That clears the first gateway checkpoint before the service joins Compose.

Caddyfile validation failed?

  • Confirm gateway/Caddyfile is saved inside the open persistent-checkout workspace.
  • Compare the braces around each handle block with the configuration above.
  • Check that the temporary container can read the mounted gateway directory.

Help me diagnose my Caddyfile validation failure.

Add Caddy to Docker Compose

Docker Compose lets containers use service-name discovery on a shared network. Catalog needs one network for its data dependencies and another for gateway traffic.

The database and cache stay on catalog-data. The Catalog API also joins checkout-api so Caddy can reach it.

  • Switch to compose.override.yaml from the open workspace.
  • Keep the existing Catalog image, environment, health check, dependency, build, and port settings.
  • Apply the Catalog network memberships and private network declaration by matching this YAML:
services:
  catalog-db:
    networks:
      - catalog-data

  catalog-cache:
    networks:
      - catalog-data

  catalog-service:
    networks:
      - catalog-data
      - checkout-api

networks:
  catalog-data:
    internal: true

Why split the Catalog networks?

The catalog-data network connects Catalog to PostgreSQL and Redis. Marking it internal: true creates an isolated data boundary.

The Catalog API also joins checkout-api. Caddy can reach the API without sharing a network with its database or cache.

  • Save compose.override.yaml.
  • Check the updated network configuration by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml config --quiet

What does this check prove?

Compose merges the three inherited files before validating the result. A successful check confirms that the network declarations fit the existing Project 6 model.

A valid model returns you to the terminal prompt without reporting a Compose configuration error.

Network configuration rejected?

  • Confirm each networks key is nested inside the correct Catalog service.
  • Confirm the top-level networks section starts at the left edge of the file.
  • Check that catalog-data uses the same spelling in every location.

Help me fix my Compose network structure.

Caddy's official image provides /data and /config mount points. Named volumes make their lifecycle explicit in the OrderFlow model.

  • Update the top-level volumes section in compose.override.yaml to match this YAML:
volumes:
  catalog-data:
    name: orderflow-catalog-data
  caddy-data:
    name: orderflow-caddy-data
  caddy-config:
    name: orderflow-caddy-config

What do these volumes store?

The inherited catalog-data volume keeps the Catalog database files. The two Caddy volumes reserve its official storage locations without creating anonymous volumes.

  • Save compose.override.yaml.
  • Validate the named volume declarations by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml config --quiet

What does this volume check prove?

Compose resolves each explicit volume name across the merged model. A successful check confirms that the new declarations are valid YAML.

You should return to the terminal prompt without a configuration error.

Volume declarations invalid?

  • Confirm the top-level volumes key starts at the left edge of compose.override.yaml.
  • Check that each volume name is indented beneath volumes.
  • Compare every hyphen in orderflow-caddy-data and orderflow-caddy-config with the YAML above.

Help me fix my Compose volume declarations.

The gateway container waits for all three APIs to become healthy. It joins only checkout-api because its job is to reach the API layer.

  • Add the caddy service beneath services in compose.override.yaml by matching this YAML:
  caddy:
    image: caddy:2.11.4
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      order-service:
        condition: service_healthy
      inventory-service:
        condition: service_healthy
      catalog-service:
        condition: service_healthy
    volumes:
      - ./gateway:/etc/caddy:ro
      - caddy-data:/data
      - caddy-config:/config
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://127.0.0.1:8080/gateway-health"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 5s
    networks:
      - checkout-api

How is the gateway constrained?

  • The quoted 127.0.0.1:8080:8080 mapping publishes the gateway only on host loopback.
  • The depends_on conditions wait for the three inherited APIs to report healthy.
  • The ./gateway:/etc/caddy:ro mount gives Caddy read-only access to your configuration directory.
  • The health check calls /gateway-health inside the Caddy container.
  • The checkout-api membership lets Caddy resolve each API by service name.
  • Save compose.override.yaml.
  • Validate the complete three-file model by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml config --quiet

What does the final model check prove?

Compose now validates the Catalog network boundary together with the Caddy service. A successful result proves the three files resolve into one usable model.

You should return to the terminal prompt without a configuration error.

Complete model still invalid?

  • Confirm caddy is indented beneath the top-level services key.
  • Check that caddy-data and caddy-config exist in the top-level volume declarations.
  • Confirm checkout-api already exists in the inherited Compose model.

Help me diagnose my merged Compose model.

✔️ Awesome, I've got everything!

Good progress. Both gateway files are saved and the complete Compose model passes validation.

ⓧ I'd like to double check the full code

These references show the exact cumulative contents required before starting the gateway.

services:
  catalog-db:
    image: postgres:18.6
    environment:
      POSTGRES_USER: catalog
      POSTGRES_PASSWORD: catalog
      POSTGRES_DB: catalog
    volumes:
      - catalog-data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      retries: 5
      start_period: 30s
      timeout: 10s
    networks:
      - catalog-data

  catalog-cache:
    image: redis:8.10.1
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - catalog-data

  catalog-service:
    build: ./catalog-service
    ports:
      - "127.0.0.1:8002:8000"
    depends_on:
      catalog-db:
        condition: service_healthy
      catalog-cache:
        condition: service_healthy
    networks:
      - catalog-data
      - checkout-api

  caddy:
    image: caddy:2.11.4
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      order-service:
        condition: service_healthy
      inventory-service:
        condition: service_healthy
      catalog-service:
        condition: service_healthy
    volumes:
      - ./gateway:/etc/caddy:ro
      - caddy-data:/data
      - caddy-config:/config
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://127.0.0.1:8080/gateway-health"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 5s
    networks:
      - checkout-api

volumes:
  catalog-data:
    name: orderflow-catalog-data
  caddy-data:
    name: orderflow-caddy-data
  caddy-config:
    name: orderflow-caddy-config

networks:
  catalog-data:
    internal: true

What should match?

Compare the service indentation, Catalog network memberships, Caddy mounts, health check, named volumes, and private network with your saved file.

:8080 {
	log {
		output stdout
		format json
	}

	@health path /gateway-health
	handle @health {
		respond 200
	}

	@catalog path /products /products/*
	handle @catalog {
		reverse_proxy catalog-service:8000
	}

	handle {
		respond "route not found" 404
	}
}

What should the gateway contain?

Your file should contain JSON logging, the health route, the Catalog route, and the unmatched-path fallback in this order.

Test the first gateway route

The merged model is valid, so Compose can apply the new service and network configuration while preserving mounted data. The build may sit quietly while containers become healthy, so that pause is expected.

  • Start the updated stack and wait for its readiness checks by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml up --build --wait

What does this startup do?

Compose rebuilds where needed and recreates services whose configuration changed. The --wait option keeps the command active until services are running or healthy.

  • Inspect the running service states by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml ps

What should the status prove?

The status table should show caddy as healthy. The inherited APIs should remain healthy while the worker and supporting services remain running or healthy.

The gateway is live beside the inherited stack. Your existing OrderFlow services have kept their healthy baseline.

Caddy not healthy?

  • Confirm the standalone Caddy validation succeeded before startup.
  • Check that gateway/Caddyfile is saved before Compose mounts the directory.
  • Confirm all three API dependencies still report healthy in the status table.

Help me troubleshoot the unhealthy Caddy container.

The first end-to-end request checks whether Caddy can receive a product path and reach Catalog through catalog-service:8000.

  • Request the seeded product through the gateway by running:
curl --include http://127.0.0.1:8080/products/TEE-001

What does this request test?

The request reaches Caddy on host port 8080. The Catalog matcher forwards the unchanged product path to the private service-name upstream.

You should see a successful product response for TEE-001. That is your first working request through the new gateway.

Product request unsuccessful?

  • Confirm the requested path starts with /products so it matches @catalog.
  • Check that Caddy and catalog-service both join checkout-api.
  • Confirm the Catalog service remains healthy in the Compose status table.

Help me trace the failed Catalog gateway request.

The direct Order endpoint provides a control request for this test. Its inherited host mapping remains available during this step.

  • Confirm the Order API still responds directly on port 8000 by running:
curl --include http://127.0.0.1:8000/orders

What does the control request prove?

This request bypasses Caddy and reaches the inherited Order host port. A successful response proves the Order API is healthy before you test the same path at the gateway.

You should see a successful response from the inherited Order API.

Direct Order request failed?

  • Confirm order-service reports healthy in the Compose status table.
  • Check that the request uses host port 8000.
  • Resolve the inherited Project 6 failure before continuing with the gateway comparison.

Help me diagnose the inherited Order endpoint failure.

Before you run this check, do you think a healthy Order backend is enough for Caddy to forward the request?

  • Send the same Order path to the gateway by running:
curl --include http://127.0.0.1:8080/orders

What does this boundary check test?

This request reaches Caddy on port 8080 and asks its current routing table to select a handler for /orders.

You will see HTTP 404 with the body route not found. The fallback handled the request because the Caddyfile has no Order matcher.

Why is this fallback useful?

The direct request proved the Order API is healthy. The gateway result proves that backend health alone cannot create a route.

Every public path family needs an explicit mapping to the correct private service name.

Seeing a different gateway result?

  • Confirm your first Caddyfile contains only the health and Catalog matchers before the final fallback.
  • Check that the request uses gateway port 8080 instead of the direct Order port.
  • Confirm the final fallback responds with route not found and status 404.

Help me compare my gateway response with the intended fallback.

Catalog now works through one loopback gateway while the unmapped Order path exposes the routing boundary. Next, you will add the Order and Inventory path families to the same entry point.

Route Every API Through Caddy

The Catalog request from the previous step proved that Caddy can reach a private API by its Docker Compose service name. The /orders request reached the fallback because the gateway had no matching route.

This step completes the gateway's explicit routing table. Each reverse proxy route preserves the incoming path while sending the request to the correct private service.

In this step, get ready to:
  • Map the Order path family to order-service:8000.
  • Map the Inventory path families to inventory-service:8000.
  • Apply the validated routing table to the running gateway.
Add the Order route family

A path matcher groups the exact root path with its descendants. The pair /orders and /orders/* covers the inherited Order route family without catching lookalike paths.

  • In gateway/Caddyfile, find the closing brace of the @catalog handle.
  • Add the Order route immediately below it by copying this code:
	@orders path /orders /orders/*
	handle @orders {
		reverse_proxy order-service:8000
	}

What Does the Order Route Do?

  • The @orders matcher groups the Order collection path with every path below it.
  • The handle block selects this route while preserving the incoming URI.
  • The reverse_proxy target sends matching requests to order-service:8000 on the shared network.
  • Save gateway/Caddyfile.
  • Check the new Order route by running the Caddy validation command:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml exec -w /etc/caddy caddy caddy validate --config Caddyfile

What Does This Validation Check?

This command asks the running Caddy container to parse the mounted Caddyfile. It catches matcher or brace problems before the active gateway configuration changes.

You should see confirmation that the configuration is valid. The Order route is ready to join the active routing table.

Configuration Not Valid?

  • Confirm that the @orders block sits inside the :8080 site.
  • Check that the new block appears after the closing brace of the @catalog handle.
  • Compare every opening brace with its closing brace.

Help me fix my Order route validation.

Add the Inventory route families

Inventory exposes the /inventory family plus the /processed-orders family. One matcher can send all four path patterns to the same private API.

  • In gateway/Caddyfile, find the closing brace of the @orders handle.
  • Add the Inventory routes immediately below it by copying this code:
	@inventory path /inventory /inventory/* /processed-orders /processed-orders/*
	handle @inventory {
		reverse_proxy inventory-service:8000
	}

What Does the Inventory Route Do?

  • The @inventory matcher covers both Inventory route families.
  • The handle block preserves each incoming URI.
  • The reverse_proxy target sends every matching request to inventory-service:8000.
  • The unmatched handle remains last so unknown paths still receive the gateway fallback.
  • Save gateway/Caddyfile.

✔️ Awesome, I've got everything!

Your routing table now covers Catalog, Order, and Inventory. The unmatched fallback remains last.

ⓧ I'd like to double check the full code

:8080 {
	log {
		output stdout
		format json
	}

	@health path /gateway-health
	handle @health {
		respond 200
	}

	@catalog path /products /products/*
	handle @catalog {
		reverse_proxy catalog-service:8000
	}

	@orders path /orders /orders/*
	handle @orders {
		reverse_proxy order-service:8000
	}

	@inventory path /inventory /inventory/* /processed-orders /processed-orders/*
	handle @inventory {
		reverse_proxy inventory-service:8000
	}

	handle {
		respond "route not found" 404
	}
}

What Should Match?

Use this complete file to check the order of your route blocks. The health route comes first while the unmatched fallback stays last.

Validate and reload Caddy

Validation checks the complete saved routing table. A graceful reload applies that file to the running gateway.

  • Validate the finished gateway/Caddyfile by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml exec -w /etc/caddy caddy caddy validate --config Caddyfile

What Does the Final Validation Prove?

Caddy parses the complete file from /etc/caddy. A successful result proves the Catalog, Order, Inventory, health, and fallback routes form one valid configuration.

You should see confirmation that the complete configuration is valid.

  • Apply the saved routing table to the running gateway by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml exec -w /etc/caddy caddy caddy reload

What Does the Reload Do?

The reload replaces the active routing table with the validated file. The existing Caddy container stays in place while the new routes become available.

Before you test both routes, do you expect either request to reach the fallback now?

  • Verify the Order and processed-order routes through the gateway by running these requests:
curl --include http://127.0.0.1:8080/orders
curl --include http://127.0.0.1:8080/processed-orders

What Should These Requests Prove?

Both requests use the single gateway address on port 8080. Caddy selects a private service-name upstream from each incoming path.

You'll see successful API responses for both paths. The text route not found no longer appears.

Why Does the Path Stay Intact?

Each route uses handle to select an upstream while preserving the original URI. The inherited FastAPI routes therefore receive /orders and /processed-orders exactly as the client sent them.

Still Seeing the Gateway Fallback?

  • Confirm that you saved gateway/Caddyfile before running the reload command.
  • Check that @orders and @inventory appear before the final unmatched handle.
  • Repeat the validation command above before reloading Caddy again.

Help me troubleshoot a gateway route that still reaches the fallback.

You closed the routing gap that produced the earlier 404. Next, you'll remove the direct API host ports so Caddy becomes the only application entry point.

Remove Direct API Bypasses

Your routing table now carries Order, Inventory, and Catalog requests through Caddy. The inherited host ports still give local clients a shortcut around that routing.

An API gateway controls a boundary only when the backend services stay private. This step removes the three shortcuts while preserving service-name traffic on the shared network.

In this step, get ready to:
  • Reset the inherited Order and Inventory host mappings in the Compose override.
  • Remove Catalog's direct host mapping while preserving its private networks.
  • Recreate the sealed stack with Caddy as its only application entry point.
Clear the inherited Order and Inventory ports

The compose.override.yaml file contributes to a merged Docker Compose model. Port lists from inherited files can survive an ordinary override.

The !reset tag clears those inherited sequence values. Container traffic still uses port 8000 across checkout-api.

  • Switch back to Visual Studio Code.
  • Select compose.override.yaml in the file sidebar.
  • Place this block immediately below services: by pasting:
services:
  order-service:
    ports: !reset []

  inventory-service:
    ports: !reset []

What does !reset do?

Compose merges matching service definitions from the three files. The !reset [] value replaces each inherited port sequence with an empty sequence.

Order and Inventory remain reachable through their container ports. Only their host-published mappings are removed.

  • Save compose.override.yaml.
  • Switch back to the terminal in persistent-checkout.
  • Validate the partial merged model by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml config --quiet

What does this check prove?

A successful check returns control to the shell without an error. That confirms Compose accepts the reset entries in the merged model.

Does the configuration check fail?

  • Check that order-service aligns directly beneath services:.
  • Check that inventory-service uses the same indentation as order-service.
  • Confirm each ports value is exactly !reset [].

Help me fix my Compose reset entries.

Remove Catalog's host mapping

Catalog's direct port lives in the override file. Deleting that block removes its host bypass while the two network entries preserve its private connections.

  • Find catalog-service in compose.override.yaml.
  • Delete the ports block containing 127.0.0.1:8002:8000.
  • Compare the edited service with this reference:
  catalog-service:
    build: ./catalog-service
    depends_on:
      catalog-db:
        condition: service_healthy
      catalog-cache:
        condition: service_healthy
    networks:
      - catalog-data
      - checkout-api

Why keep both networks?

  • The catalog-data network keeps Catalog connected to PostgreSQL and Redis.
  • The checkout-api network keeps Catalog discoverable to Caddy.
  • The service definition now contains no host port mapping.
  • Save compose.override.yaml.

The complete override is ready for a final comparison.

✔️ Awesome, I've got everything!

Good work. Your saved override now removes all three direct API host mappings.

ⓧ I'd like to double check the full code

services:
  order-service:
    ports: !reset []

  inventory-service:
    ports: !reset []

  catalog-db:
    image: postgres:18.6
    environment:
      POSTGRES_USER: catalog
      POSTGRES_PASSWORD: catalog
      POSTGRES_DB: catalog
    volumes:
      - catalog-data:/var/lib/postgresql
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U $${POSTGRES_USER} -d $${POSTGRES_DB}"]
      interval: 10s
      retries: 5
      start_period: 30s
      timeout: 10s
    networks:
      - catalog-data

  catalog-cache:
    image: redis:8.10.1
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 5s
      timeout: 3s
      retries: 5
    networks:
      - catalog-data

  catalog-service:
    build: ./catalog-service
    depends_on:
      catalog-db:
        condition: service_healthy
      catalog-cache:
        condition: service_healthy
    networks:
      - catalog-data
      - checkout-api

  caddy:
    image: caddy:2.11.4
    ports:
      - "127.0.0.1:8080:8080"
    depends_on:
      order-service:
        condition: service_healthy
      inventory-service:
        condition: service_healthy
      catalog-service:
        condition: service_healthy
    volumes:
      - ./gateway:/etc/caddy:ro
      - caddy-data:/data
      - caddy-config:/config
    healthcheck:
      test: ["CMD", "curl", "--fail", "http://127.0.0.1:8080/gateway-health"]
      interval: 5s
      timeout: 3s
      retries: 5
      start_period: 5s
    networks:
      - checkout-api

volumes:
  catalog-data:
    name: orderflow-catalog-data
  caddy-data:
    name: orderflow-caddy-data
  caddy-config:
    name: orderflow-caddy-config

networks:
  catalog-data:
    internal: true

What should match?

The full file begins with reset entries for Order and Inventory. The Catalog service keeps both networks without a ports block.

Caddy retains the single loopback mapping. The gateway configuration in gateway/Caddyfile remains unchanged.

Does your override look different?

  • Move the Order and Inventory reset entries directly below services:.
  • Keep the depends_on block beneath catalog-service.
  • Keep both Catalog network entries in their existing order.

Help me compare my override with the target file.

Recreate and inspect the sealed stack

The saved override now defines the sealed network boundary. Compose must apply that model by recreating the services whose port configuration changed.

Private service-name discovery continues across checkout-api. Caddy can still reach each API on container port 8000.

  • Validate the finished three-file model by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml config --quiet

What does the final validation cover?

This check resolves the base file with both override files. A successful result confirms the final merged configuration is valid.

Does the finished model fail validation?

  • Compare the opening reset entries with the full-file reference above.
  • Confirm the Catalog service has no leftover empty ports key.
  • Check the indentation around catalog-service.

Help me diagnose the merged Compose model.

This recreate can take a few minutes while images build and health checks settle. A longer pause here is normal.

  • Recreate the sealed stack by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml up --build --wait

What does this command change?

  • Compose recreates services whose port or network configuration changed.
  • The --wait option keeps the command active until services are running or healthy.
  • Mounted volumes preserve the inherited application data.

Does the stack fail to become healthy?

  • Identify the service that remains unhealthy in the terminal output.
  • Confirm the inherited Project 6 services were healthy before this recreate.
  • Check that Caddy still shares checkout-api with all three API services.

Help me diagnose the unhealthy recreated stack.

The stack is healthy again with its backend host ports sealed. The next checks prove the boundary from the client's point of view.

Before you inspect the services, consider which application service you expect to retain a host-published port.

  • Inspect the recreated service state by running:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml ps

What should you see?

The Caddy row is the only application row with a host-published address. Order, Inventory, and Catalog have no host address.

The backend containers remain available on their Compose networks. Their internal ports do not create host bypasses.

Do you still see a direct API port?

  • Check that Order and Inventory each use ports: !reset [].
  • Check that Catalog contains no ports block.
  • Run the recreate command again after saving the override.

Help me find the remaining host port.

Before you test the old direct path, consider whether port 8000 can still accept the request.

  • Confirm the old direct request fails by running this intentional failure check:
curl --max-time 2 http://127.0.0.1:8000/orders

This failure is expected

curl cannot complete the connection because the host no longer publishes port 8000. The failed request proves the direct Order bypass is closed.

Before the final request, consider whether removing the host mapping also removed Caddy's private route to Order.

  • Repeat the Order request through the gateway by running:
curl --include http://127.0.0.1:8080/orders

What does the successful response prove?

You should see response headers followed by Order API data. Caddy accepted the request on port 8080 and reached order-service:8000 across the private network.

That is the boundary you wanted. Clients use one gateway address while the backend APIs stay private.

Does the gateway request fail?

  • Confirm Caddy reports healthy in the service status output.
  • Confirm order-service remains attached to checkout-api in the inherited Compose model.
  • Confirm gateway/Caddyfile still proxies the Order route to order-service:8000.

Help me restore the private gateway route.

You have sealed the application boundary. Caddy is now the only host-facing application service while every backend remains reachable through Compose networking.

Your gateway is now the only route into OrderFlow. Next, you will send a real checkout through that entry point and trace its Inventory outcome.

Prove the Gateway Checkout Flow

Your sealed stack now accepts application traffic only through Caddy on loopback. The direct API ports are closed.

A real checkout tests more than routing. It moves from Order into RabbitMQ before inventory-worker stores a durable Inventory outcome.

In this step, get ready to:
  • Submit an order through Caddy.
  • Retrieve the durable Inventory outcome through the same gateway address.
  • Inspect Caddy's JSON access logs for both gateway requests.
Submit an order through Caddy

Submitting an order tests the gateway's write path. The seeded SCALE-001 payload gives the inherited Order API a valid checkout request.

  • Return to the terminal in your persistent-checkout folder from earlier.
  • Replace the numeric suffix in gateway-flow-001 if that client reference has already been used.

Before you send the request, predict the response status you expect from a checkout accepted through the sealed gateway.

  • Submit the checkout through Caddy by running this command:
curl --include --header "Content-Type: application/json" --data '{"client_reference":"gateway-flow-001","sku":"SCALE-001","quantity":1}' http://127.0.0.1:8080/orders

What does this request prove?

  • The Content-Type: application/json header labels the request body as JSON.
  • The --data option sends the checkout fields to the inherited /orders route.
  • The 127.0.0.1:8080 address keeps client traffic on Caddy's published port.

You should see an HTTP 202 status in the response headers. That confirms Caddy carried a write request into Order.

  • Record the exact client reference from the successful request here: gateway-flow-001.

No HTTP 202 Response?

  • Confirm the sealed stack from earlier still reports Caddy plus the three APIs as healthy.
  • Replace the numeric suffix if the response indicates that the client reference was already used.
  • Check that the payload still uses SCALE-001 with a quantity of 1.

Help me troubleshoot the missing HTTP 202 response.

Observe the asynchronous result through Caddy

Order acceptance happens before the Inventory worker finishes processing the event. The durable result appears after the worker receives the RabbitMQ message.

Before you query the result, predict whether the submitted order is already visible or still being processed.

  • Retrieve the processed orders through Caddy by running this command:
curl --include http://127.0.0.1:8080/processed-orders

How does this complete the flow?

The request reaches inventory-service:8000 through Caddy's /processed-orders handle. It reads durable outcomes written by inventory-worker after RabbitMQ delivery.

  • Look for gateway-flow-001 in the response.
  • Wait briefly if the submitted client reference is absent.
  • Repeat the same processed-order request after the pause.
  • Confirm the submitted client reference has an Inventory outcome.

You should see the same client reference with a durable Inventory outcome. That is the full checkout loop proven through Caddy.

Outcome Still Missing?

  • Repeat the processed-order request after another short pause.
  • Return to the stack status from earlier to confirm inventory-worker is running.
  • Check that you are searching for the exact client reference used in the successful request.

Help me trace an order that is missing from the processed-order response.

Inspect the gateway evidence

Caddy writes one access record for each gateway request. The JSON output on stdout lets you connect the Order request to the later processed-order lookup.

Before you inspect the logs, predict which two route families should appear in the latest records.

  • Inspect the latest Caddy access records by running this command:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml logs --tail 20 caddy

What do the logs confirm?

  • The --tail 20 option limits the output to the latest Caddy log lines.
  • The caddy service writes access records to standard output because logging is enabled in the Caddyfile.
  • The two route records show that both client requests crossed the gateway boundary.
  • Find the JSON access record for the Order request.
  • Find the JSON access record for the processed-order request.
  • Confirm both requests targeted the gateway on port 8080.

You should see JSON access records for both requests. Together with the routes in gateway/Caddyfile, this shows that clients used port 8080 while Caddy selected private service-name upstreams.

You have now proved the complete checkout path through one local entry point. The sealed stack stayed healthy while the worker stored the durable result.

Missing a Gateway Record?

  • Run the processed-order request again if its record fell outside the latest 20 log lines.
  • Confirm you are inspecting logs for the caddy service in the inherited three-file Compose model.
  • Check that both request URLs use 127.0.0.1:8080.

Help me find the missing Caddy access record.

Secret mission

Isolate a Catalog Failure

Take one private backend offline while the gateway stays live. Prove the product route fails in isolation while Order remains available. Inspect Caddy's evidence before restoring Catalog.

Clean Up Your Resources

Clean Up Your Resources

Choose whether to keep your Docker Compose stack running, pause it for later, or delete its runtime resources. The local stack creates no cloud charges while consuming your computer's CPU, memory, storage, and network bandwidth.

Resources you used:

  • Running stack: Ten local service containers. This includes one Caddy gateway container.
  • Private network: The catalog-data project network.
  • Caddy storage: The orderflow-caddy-data volume plus the orderflow-caddy-config volume.

Your gateway/Caddyfile file stays inside persistent-checkout under every option below. Your updated compose.override.yaml file also stays there.

Keep everything running

No action is needed. Choose this if you are still testing gateway routes or failure isolation.

  • Keep using http://127.0.0.1:8080 as the stack's only host entry point.
  • Leave the service containers running for your next OrderFlow session.

The stack remains ready for requests. Its containers continue using local computing resources.

Pause - I'll come back to this later

Shut down the running processes to free local resources. The containers plus every named volume remain available for your return.

  • Stop the OrderFlow service containers by running this command from the persistent-checkout folder:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml stop

What Does This Command Do?

This command stops the containers defined by the three-file OrderFlow model. It keeps the containers available for a later restart.

Your named volumes remain intact. The durable processed order remains available.

  • Confirm the terminal reports that the service containers have stopped.

Good call. The stack is quiet while your workspace stays ready for another session.

Delete - I don't want to use this again

Remove the runtime resources from this project while preserving the inherited OrderFlow data. Your workspace files also remain available.

Protect Your Inherited Data

Deleting the runtime resources looks final. The cleanup below preserves the inherited PostgreSQL data plus the inherited RabbitMQ data.

  • Keep --volumes out of the Compose teardown command.
  • Remove the service containers plus project networks by running this command from the persistent-checkout folder:
docker compose -f compose.yaml -f compose.override.yaml -f compose.images.yaml down

What Does This Cleanup Remove?

This command removes the OrderFlow containers plus the project networks. That includes the private catalog-data network.

Named volumes remain on your computer. Your inherited application data stays available.

  • Remove the two added Caddy storage volumes by running:
docker volume rm orderflow-caddy-data orderflow-caddy-config

What Does This Volume Cleanup Remove?

This command removes only orderflow-caddy-data plus orderflow-caddy-config.

The inherited PostgreSQL volumes remain untouched. The inherited RabbitMQ volume also remains untouched.

  • Confirm the terminal prints both Caddy volume names.

Are the Caddy Volumes Still in Use?

A volume can remain attached if the Compose teardown did not finish. Run the teardown command again before retrying the volume removal.

Ask for help with the remaining attachment: Why are my Caddy volumes still attached after Docker Compose down?

That clears the Caddy runtime storage. Your inherited OrderFlow data remains available.

Nice Work!

Nice Work!

Excellent work! Your hardened OrderFlow stack now uses Caddy as its only loopback gateway for three private APIs. You also proved that a Catalog outage stays isolated while healthy Order traffic keeps moving.

You've learned how to:

  • Map four API path families with reverse proxy routing. Preserve the inherited FastAPI request paths through private service-name upstreams.
  • Remove every direct API host-port bypass with Docker Compose reset behavior. Keep Caddy as the only loopback application entry point at http://127.0.0.1:8080.
  • Submit an asynchronous checkout through Caddy. Trace the request through RabbitMQ to the Inventory worker. Retrieve the durable outcome from /processed-orders.
  • Secret Mission: Prove failure isolation by stopping only Catalog. Observe HTTP 502 through Caddy while the Order route remains available. Use JSON access logs to identify the failed upstream. Restore Catalog without disrupting Order.

Ready to quiz yourself?