Deploy a Telemetry Gateway with Docker
Deploy, break, and recover a two-container telemetry API with Docker Compose.
Introduction
30 Second Summary
When an important update stops arriving, guessing wastes time. A clear recovery path turns the outage into a repeatable response.
In this project, you will build a telemetry lab across two containers using Docker Compose and FastAPI. The finished lab turns a broken delivery path into a documented recovery drill.
What You'll Build
You will send a fictional sensor report, watch it reach the gateway, deliberately cut the connection, and bring the system back by following your own runbook.
By the end of this project, you'll have:
- A live telemetry gateway where you can check service health, inspect interactive API documentation, and view the latest fictional sensor report.
- A repeatable network recovery drill that exposes why localhost fails between containers before restoring delivery through service-name DNS.
- An operator runbook you can follow to inspect service status, read logs, restart the gateway, and verify fresh telemetry.
- A Secret Mission: add a malformed-payload mode that proves the API rejects telemetry missing battery_percent with HTTP 422.
Are there any prerequisites?
You need a Mac running a Docker-supported macOS release with at least 4 GB RAM. You do not need local Python or a cloud account.
Before We Start
Before the hands-on work begins, lock in the mission-support scenario you are about to build. This gives every later step a clear operational purpose.
Set Up the Mac Workspace
A recovery lab only works when every later result comes from a known runtime. This step removes machine setup as a source of confusion before you deploy either service.
Docker Desktop provides the container engine. Docker Compose manages the two-service application later.
Visual Studio Code gives the lab one visible workspace. You will connect it to Terminal through the code command.
In this step, get ready to:
- Check macOS compatibility before installing Docker Desktop 4.89.0.
- Install Visual Studio Code with Terminal access through the code command.
- Create the mission-telemetry-lab workspace with gateway and simulator folders.
Check compatibility and install Docker Desktop
Docker Desktop needs a supported macOS release with at least 4 GB of memory. Your Mac's processor type determines which installer you download.
- Click the Apple menu in the upper-left corner.
- Select System Settings.
- Select General in the sidebar.
- Select About.
- Confirm that the Memory value is at least 4 GB.
- Compare the macOS value with the support window on the official Docker Desktop Mac installation page.
- Read the Chip or Processor value to identify an Apple silicon or Intel Mac.
Licensing is the one part here that can affect cost, so pause before the first launch. Personal educational use is eligible for free Docker Desktop use.
Government entities require a paid subscription. Ineligible professional use also requires a paid subscription.
- Confirm that this lab is personal educational use or covered by an appropriate Docker subscription.
- Check whether your Applications folder contains Docker Desktop version 4.89.0.
Which Docker Desktop state matches your Mac?
✔️ I have the required version
- Press Cmd+Space to open Spotlight.
- Type Docker into Spotlight.
- Press Enter to launch Docker Desktop.
- Keep Docker Desktop 4.89.0 running.
ⓧ I have an older version
- Open the Docker Desktop release notes.
- Select the Docker Desktop 4.89.0 download that matches your Apple silicon or Intel Mac.
- Open the downloaded Docker.dmg file.
- Drag the Docker icon to the Applications folder.
- Confirm the macOS replacement prompt if it appears.
Docker Desktop is now installed at /Applications/Docker.app. The first launch completes its configuration.
- Press Cmd+Space to open Spotlight.
- Type Docker into Spotlight.
- Press Enter to launch Docker Desktop.
- Accept the Docker Subscription Service Agreement for your eligible use.
The recommended setup may request your Mac password for required configuration.
- Select Use recommended settings.
- Wait until Docker Desktop reports that its engine is running.
ⓧ Docker Desktop is not installed
- Open the Docker Desktop release notes.
- Select the Docker Desktop 4.89.0 download that matches your Apple silicon or Intel Mac.
- Open the downloaded Docker.dmg file.
- Drag the Docker icon to the Applications folder.
Docker Desktop is now installed at /Applications/Docker.app. The first launch completes its configuration.
- Press Cmd+Space to open Spotlight.
- Type Docker into Spotlight.
- Press Enter to launch Docker Desktop.
- Accept the Docker Subscription Service Agreement for your eligible use.
The recommended setup may request your Mac password for required configuration.
- Select Use recommended settings.
- Wait until Docker Desktop reports that its engine is running.
That clears the largest setup hurdle. Docker Desktop now supplies the Docker Engine client, Docker Engine server, and Docker Compose from one installation.
Install and configure Visual Studio Code
The code command lets Terminal hand its current folder to Visual Studio Code. Adding it to PATH keeps your editor aligned with the workspace shown in Terminal.
- Press Cmd+Space to open Spotlight.
- Type Visual Studio Code into Spotlight.
Choose the tab that matches the search result.
✔️ Visual Studio Code appears
- Press Enter to launch Visual Studio Code.
ⓧ Visual Studio Code does not appear
- Download the macOS installer from the official Visual Studio Code page.
- Open the downloaded .dmg file.
- Drag Visual Studio Code.app to the Applications folder.
- Press Cmd+Space to open Spotlight.
- Type Visual Studio Code into Spotlight.
- Press Enter to launch Visual Studio Code.
Visual Studio Code is installed. The remaining configuration connects its command-line launcher to Terminal.
- Press Cmd+Shift+P to open the Command Palette.
- Type shell command into the Command Palette.
- Select Shell Command: Install 'code' command in PATH.
- Quit Terminal if it is already open.
- Press Cmd+Space to open Spotlight.
- Type Terminal into Spotlight.
- Press Enter to open a fresh Terminal window.
The fresh Terminal session reads the updated PATH. It is ready to launch Visual Studio Code from a folder.
Create and verify the workspace
The workspace separates the gateway from the simulator before either service has source code. This directory structure becomes the boundary between the two container images.
- Move the fresh Terminal session to your Desktop by running this command:
cd ~/Desktop
What does this command do?
The cd command changes Terminal's current directory. The ~/Desktop path points to the Desktop folder inside your Mac user account.
Before you run the workspace commands, what evidence do you expect each tool to show when the setup is complete?
- Create the workspace, check both Docker tools, and launch the editor by running these commands:
mkdir -p mission-telemetry-lab/gateway mission-telemetry-lab/simulator
cd mission-telemetry-lab
docker version
docker compose version
code .
What do these commands do?
- The mkdir -p command creates mission-telemetry-lab with empty gateway and simulator directories.
- The cd command moves Terminal into mission-telemetry-lab.
- The two Docker checks inspect the Engine client, Engine server, and Compose installation without creating images or containers.
- The code command sends the current workspace to Visual Studio Code.
You'll see Docker client and server sections in Terminal. You'll also see Docker Compose version v5.5.0.
Visual Studio Code opens the mission-telemetry-lab folder. In the Explorer sidebar, you'll see the empty gateway and simulator directories.
Missing a version or workspace folder?
- Start Docker Desktop if the Docker output does not include a server section.
- Install Docker Desktop 4.89.0 again if Compose does not report v5.5.0.
- Run Shell Command: Install 'code' command in PATH again if Visual Studio Code does not open.
Still stuck? Help me diagnose my Docker, Compose, or Visual Studio Code workspace verification.
Your Mac now has a verified container runtime plus a workspace for both services. Next, you will turn the gateway directory into a running FastAPI deployment.
Deploy the Telemetry Gateway
Your Mac workspace is ready. The next piece is a gateway that can receive fictional sensor reports through a REST API.
A working gateway gives you an early deployment win before you introduce the cross-container networking problem. You will package the API as a Docker image and run it on your Mac.
In this step, get ready to:
- Create the gateway API with a typed telemetry contract.
- Package the gateway with pinned dependencies.
- Build the image and verify the running API in your browser.
Create the API and pin dependencies
The gateway needs a clear contract for every telemetry report. FastAPI uses a typed Pydantic model to validate the incoming JSON.
Why FastAPI for this gateway?
FastAPI turns the same typed model into request validation and browser-based API documentation. This keeps the contract close to the code that receives each report.
A framework such as Flask would need extra validation and documentation tooling. FastAPI keeps this lab focused on deployment and integration.
- Create main.py inside the gateway directory by using the file controls in Visual Studio Code from earlier.
- Define the application and telemetry contract by copying this code into gateway/main.py:
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Telemetry(BaseModel):
sensor_id: str
x_coordinate: float
y_coordinate: float
battery_percent: int
status: str
latest_telemetry: Telemetry | None = None
What does this code do?
- The imports provide the web application class and the typed model base.
- The Telemetry model defines the five fields that every valid sensor report must contain.
- The field annotations tell Pydantic which values must be strings and which values must be numbers.
- The latest_telemetry variable gives the lab an in-memory slot for the newest accepted report.
- Save gateway/main.py.
- Confirm that the saved Telemetry model contains five typed fields.
Does the model look incomplete?
- Check that Telemetry is indented beneath its class declaration.
- Check that all five field names match the code block exactly.
- Check that latest_telemetry starts at the left edge of the file.
Still stuck? Help me check my Telemetry model.
A health endpoint gives operators a quick way to confirm that the gateway process is ready to receive requests.
- Add the health path operation below latest_telemetry in gateway/main.py by copying this code:
@app.get("/health")
def health():
return {"status": "ready"}
What does this endpoint do?
- The @app.get("/health") decorator connects an HTTP GET request to health().
- The function returns a small JSON response that identifies the gateway as ready.
- Save gateway/main.py.
- Confirm that health() returns the status value ready.
Is the health route misplaced?
- Place the decorator at the left edge of the file.
- Keep the return statement indented inside health().
- Check that the route is written as /health.
Need another pair of eyes? Help me debug my health endpoint.
The ingestion operation accepts a validated report. It also updates the in-memory slot so another operation can retrieve the latest telemetry.
- Add the telemetry ingestion path operation below health() by copying this code:
@app.post("/telemetry")
def receive_telemetry(report: Telemetry):
global latest_telemetry
latest_telemetry = report
return {"accepted": True, "sensor_id": report.sensor_id}
What does this endpoint do?
- The typed report parameter tells FastAPI to validate the request body against Telemetry.
- The global declaration lets the function update the module-level telemetry slot.
- The response confirms acceptance and echoes the submitted sensor identifier.
- Save gateway/main.py.
- Confirm that receive_telemetry() accepts a Telemetry report.
Is the ingestion route incomplete?
- Check that the route uses @app.post("/telemetry").
- Check that the function parameter is named report.
- Check that the returned sensor identifier comes from report.sensor_id.
Need help comparing the route? Help me inspect my telemetry ingestion endpoint.
The final operation exposes the gateway's current in-memory state. Before the first report arrives, it returns a waiting status.
- Add the latest-telemetry path operation below receive_telemetry() by copying this code:
@app.get("/telemetry/latest")
def get_latest_telemetry():
if latest_telemetry is None:
return {"status": "waiting"}
return latest_telemetry
What does this endpoint do?
- The route handles GET requests to /telemetry/latest.
- The condition checks whether the gateway has accepted any telemetry.
- The final return sends the newest validated report after one arrives.
- Save gateway/main.py.
- Confirm that the final route returns waiting when latest_telemetry is empty.
Is the latest route returning too early?
- Indent the waiting response beneath the if condition.
- Keep the final return latest_telemetry aligned with the condition.
- Check that the route includes /telemetry/latest.
If the return paths are confusing, help me check the latest-telemetry endpoint.
A dependency manifest makes each image build use the same library releases. The three pins cover the API framework, its validation model, and the server process.
- Create requirements.txt inside the gateway directory by using the file controls in Visual Studio Code.
- Pin the gateway dependencies by copying this code into gateway/requirements.txt:
fastapi==0.141.1
pydantic==2.13.5
uvicorn==0.52.4
What are these dependencies?
- FastAPI 0.141.1 provides the path operations and generated documentation.
- Pydantic 2.13.5 validates each telemetry request against the typed model.
- Uvicorn 0.52.4 runs the FastAPI application inside the container.
- Save gateway/requirements.txt.
- Confirm that the file contains exactly three dependency pins.
Do the dependency pins differ?
- Check that each package name uses lowercase letters.
- Check that every version uses two equals signs.
- Remove any spaces around the version pins.
Need help finding a mismatch? Help me check my dependency file.
Package the gateway
A Dockerfile turns the source files into a repeatable container image. Its layer order installs dependencies before copying the application code.
Why copy dependencies first?
Docker can reuse the dependency layer when only main.py changes. Later rebuilds can skip reinstalling the packages.
- Create Dockerfile inside the gateway directory by using the file controls in Visual Studio Code.
- Define the gateway image by copying this code into gateway/Dockerfile:
FROM python:3.14.7-slim
WORKDIR /code
COPY requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY main.py /code/main.py
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
What does this Dockerfile do?
- The python:3.14.7-slim image supplies Python without requiring a local Python installation.
- The WORKDIR instruction gives the container a predictable application directory.
- The first copy and run instructions install the pinned dependencies into a reusable layer.
- The final command starts Uvicorn on every container interface at port 8000.
- Save gateway/Dockerfile.
- Confirm that gateway now contains main.py, requirements.txt, and Dockerfile.
Is the Dockerfile in the wrong place?
- Move Dockerfile into the same gateway directory as the other two files.
- Check that the filename has no extension.
- Check that the first line uses python:3.14.7-slim.
Still unsure about the package layout? Help me check my gateway files.
✔️ Awesome, I've got everything!
Your three gateway files are saved. You are ready to build the image.
ⓧ I'd like to double check the full code
Compare each saved file with the complete versions below.
from fastapi import FastAPI
from pydantic import BaseModel
app = FastAPI()
class Telemetry(BaseModel):
sensor_id: str
x_coordinate: float
y_coordinate: float
battery_percent: int
status: str
latest_telemetry: Telemetry | None = None
@app.get("/health")
def health():
return {"status": "ready"}
@app.post("/telemetry")
def receive_telemetry(report: Telemetry):
global latest_telemetry
latest_telemetry = report
return {"accepted": True, "sensor_id": report.sensor_id}
@app.get("/telemetry/latest")
def get_latest_telemetry():
if latest_telemetry is None:
return {"status": "waiting"}
return latest_telemetry
What should you compare?
Check the model fields, route order, indentation, and returned values. Every identifier should match exactly.
fastapi==0.141.1
pydantic==2.13.5
uvicorn==0.52.4
What should you compare?
Check the three package names and their complete version pins. The file should not contain extra packages.
FROM python:3.14.7-slim
WORKDIR /code
COPY requirements.txt /code/requirements.txt
RUN pip install --no-cache-dir --upgrade -r /code/requirements.txt
COPY main.py /code/main.py
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
What should you compare?
Check the instruction order and the paths beneath /code. Check that the final command binds port 8000.
Build and run the gateway
The source files now describe the gateway. Building turns them into the local mission-gateway image that Docker can run.
- Switch back to the Terminal from earlier.
- Build the gateway image from the mission-telemetry-lab workspace by running:
docker build -t mission-gateway ./gateway
What does this command do?
- The build reads gateway/Dockerfile from the supplied directory.
- The tag gives the finished image the local name mission-gateway.
- The dependency installation becomes part of the image, so the Mac does not need a local Python environment.
The first build downloads the Python image and installs the pinned packages. Later builds can reuse unchanged layers.
Did the image build fail?
- Confirm that Terminal is still in the mission-telemetry-lab workspace.
- Confirm that Docker Desktop is still running.
- Compare the three gateway files with the full-code tab above.
If the build still stops, help me diagnose my gateway image build.
A container needs a host port mapping before your browser can reach the API. The mapping connects port 8000 on your Mac to port 8000 inside the container.
- Start the gateway container in the background by running:
docker run -d --rm --name mission-gateway -p 8000:8000 mission-gateway
What does this command do?
- Detached mode keeps the gateway running after Terminal returns to the prompt.
- The container name gives you a stable reference for later operational commands.
- Automatic removal cleans up the standalone container after it stops.
- The port mapping exposes the containerized API at localhost:8000 on your Mac.
Docker prints a long container identifier. That confirms the gateway process started in the background.
Before you check the gateway, what response do you expect from a process that is ready to receive telemetry?
- Open http://localhost:8000/health in your browser.
You should see {"status":"ready"}. That response proves the container is running and reachable through the host port.
- Open http://localhost:8000/docs in your browser.
You should see Swagger UI with operations for GET /health, POST /telemetry, and GET /telemetry/latest.
Does the gateway page fail to load?
- Confirm that Docker Desktop is still running.
- Check the Terminal output from the container start for a port conflict.
- Compare every route and the Uvicorn command with the full-code tab.
If the health response or documentation is missing, help me troubleshoot the running gateway.
Your gateway image now runs as a reachable container with generated API documentation. Next, you will launch a separate simulator and expose the networking limitation hidden behind localhost.
Trigger the Container Networking Failure
Your telemetry gateway is live in a Docker container. Your Mac can reach it through port 8000.
A second container has its own network namespace. You will build the simulator with a familiar localhost target. The container will test whether that address still reaches the gateway.
In this step, get ready to:
- Create a synthetic sensor simulator with retry behavior.
- Package the simulator as a separate Docker image.
- Run the simulator container to reveal the networking failure.
Create the sensor simulator
The simulator acts as a one-shot sensor client. It creates fictional telemetry as JSON. It sends that report through an HTTP request.
Its target comes from GATEWAY_URL. The default points to http://localhost:8000/telemetry. This gives the container a concrete address to test.
- Select the simulator folder in the Visual Studio Code file sidebar.
- Use the new-file control in the file sidebar to create simulator.py inside the simulator folder.
- Set up the target and fictional report by copying this code into simulator.py:
import json
import os
import time
from urllib.error import URLError
from urllib.request import Request, urlopen
gateway_url = os.getenv(
"GATEWAY_URL",
"http://localhost:8000/telemetry",
)
payload = {
"sensor_id": "SIM-ALPHA-01",
"x_coordinate": 120.5,
"y_coordinate": 88.0,
"battery_percent": 74,
"status": "nominal",
}
request = Request(
gateway_url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
What does this code do?
- The imports provide JSON serialization and environment lookup. They also provide timing and standard-library HTTP tools.
- The gateway_url value lets another environment override the destination. Its default deliberately targets the simulator container's loopback address.
- The payload dictionary represents one fictional report from SIM-ALPHA-01.
- The request object converts the report to JSON bytes. It prepares a POST request for the telemetry endpoint.
- Save simulator.py.
- Check the Visual Studio Code file sidebar to confirm simulator.py appears inside simulator.
Seeing a syntax indicator?
Compare the closing braces and parentheses around payload and Request(). A missing delimiter can make the editor flag several later lines.
Still stuck? Help me check the first part of my telemetry simulator.
- Add the delivery loop below the request block in simulator.py by copying this code:
for attempt in range(1, 6):
try:
with urlopen(request, timeout=2) as response:
print(f"DELIVERY CONFIRMED: HTTP {response.status}")
print(response.read().decode("utf-8"))
break
except URLError as error:
print(f"DELIVERY FAILED ({attempt}/5): {error.reason}")
if attempt == 5:
raise SystemExit(1)
time.sleep(1)
How does the retry loop work?
- The loop allows up to five delivery attempts. A successful response prints its HTTP status before ending the loop.
- The URLError handler prints the failed attempt number. The fifth failure exits the simulator with an unsuccessful status.
- The one-second pause spaces out the retries. This makes the sequence easy to follow in the terminal.
- Save simulator.py.
- Check the bottom of simulator.py to confirm the final line is time.sleep(1).
Does the loop look misaligned?
Python uses indentation to group the retry logic. Keep except URLError as error: aligned with try:.
Need another pair of eyes? Help me check the indentation in my simulator retry loop.
✔️ Awesome, I've got everything!
Strong start. Your simulator can form one fictional report and retry its delivery up to five times.
ⓧ I'd like to double check the full code
import json
import os
import time
from urllib.error import URLError
from urllib.request import Request, urlopen
gateway_url = os.getenv(
"GATEWAY_URL",
"http://localhost:8000/telemetry",
)
payload = {
"sensor_id": "SIM-ALPHA-01",
"x_coordinate": 120.5,
"y_coordinate": 88.0,
"battery_percent": 74,
"status": "nominal",
}
request = Request(
gateway_url,
data=json.dumps(payload).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
for attempt in range(1, 6):
try:
with urlopen(request, timeout=2) as response:
print(f"DELIVERY CONFIRMED: HTTP {response.status}")
print(response.read().decode("utf-8"))
break
except URLError as error:
print(f"DELIVERY FAILED ({attempt}/5): {error.reason}")
if attempt == 5:
raise SystemExit(1)
time.sleep(1)
How to use this reference
Compare the imports and request setup first. Then compare the retry loop. The complete file creates one report and attempts to deliver it five times.
Package the simulator
The simulator needs its own image so it runs inside an isolated environment. Its Dockerfile uses the same Python image as the gateway.
- Select the simulator folder in the Visual Studio Code file sidebar.
- Use the new-file control in the file sidebar to create Dockerfile inside the simulator folder.
- Define the simulator image by copying this code into simulator/Dockerfile:
FROM python:3.14.7-slim
WORKDIR /code
COPY simulator.py /code/simulator.py
CMD ["python", "simulator.py"]
What does this Dockerfile do?
- The base image supplies Python 3.14.7 inside the container.
- The working directory places the simulator at /code/simulator.py.
- The command starts the simulator whenever a container launches from this image.
- Save simulator/Dockerfile.
- Switch back to the Terminal window from earlier.
- Build the simulator image from the mission-telemetry-lab folder by running:
docker build -t mission-simulator ./simulator
What does this command do?
The build uses the simulator folder as its context. The tag stores the finished image under the name mission-simulator.
Good progress. You should see the build complete with an image named mission-simulator.
Did the image build fail?
- Confirm Terminal is still inside the mission-telemetry-lab folder.
- Confirm the simulator folder contains both simulator.py and Dockerfile.
- Confirm Docker Desktop is still running before repeating the build.
Still blocked? Help me troubleshoot the mission-simulator image build.
✔️ Awesome, I've got everything!
Your mission-simulator image is ready to run as a separate container.
ⓧ I'd like to double check the full code
FROM python:3.14.7-slim
WORKDIR /code
COPY simulator.py /code/simulator.py
CMD ["python", "simulator.py"]
How to use this reference
Compare the base image and working directory first. Then confirm the copy instruction and startup command match this reference.
Observe the networking failure
The gateway responds at localhost from your Mac. The simulator now tests what that same name reaches from inside a separate container.
Before you run this, do you think the simulator will confirm delivery or exhaust all five attempts?
- Run the isolated simulator container by entering:
docker run --rm mission-simulator
Why this failure is useful
You should see five retry messages beginning with DELIVERY FAILED. The process exits after the fifth attempt. DELIVERY CONFIRMED never appears.
Inside the simulator container, localhost resolves to that container's own loopback interface. The gateway and Mac host sit outside that interface.
Host port 8000 supports browser access from your Mac. It does not connect two separately launched containers.
Seeing a different result?
- Repeat the previous image build if Terminal reports that the simulator image is unavailable.
- Confirm you ran the simulator with the exact command shown above if the output uses an unexpected target.
- Count the retry lines if the process appears to stop early. The expected run prints five failed attempts.
Need help reading the result? Help me diagnose my simulator container output.
You have reproduced the network boundary that breaks the integration. Next, you will connect both services through service-name DNS and prove delivery succeeds.
Restore Integration with Docker Compose
The simulator's failed run exposed the boundary around each container's network namespace. Its localhost address points back to itself.
Docker Compose can put both services on one managed network. Its internal DNS gives the simulator a stable gateway host name.
This step replaces the broken loopback route. You will prove the fix by inspecting service state plus delivery logs.
In this step, get ready to:
- Define the gateway and simulator as one Docker Compose application.
- Replace the standalone gateway with the integrated application.
- Confirm that synthetic telemetry reaches the gateway through service-name DNS.
Define the integrated application
A Compose configuration describes how the two existing images run together. Both services join the same default bridge network.
The simulator keeps its localhost fallback in simulator/simulator.py. Compose overrides GATEWAY_URL with the gateway service address.
Why Docker Compose?
Compose exposes the service discovery problem directly. It keeps this lab focused on deploying two connected services.
A local cluster would add administration work before you could test the integration. Compose provides the networking model this recovery needs.
- In the Visual Studio Code window from earlier, create compose.yaml inside the mission-telemetry-lab folder using the file sidebar.
- Copy this configuration into compose.yaml:
services:
gateway:
image: mission-gateway
build: ./gateway
ports:
- "8000:8000"
simulator:
image: mission-simulator
build: ./simulator
environment:
GATEWAY_URL: http://gateway:8000/telemetry
What does this configuration do?
- The gateway service uses mission-gateway as its image name.
- The build: ./gateway setting points Compose at the gateway build context.
- The 8000:8000 port mapping keeps the gateway reachable from your Mac.
- The simulator service uses mission-simulator as its image name.
- The GATEWAY_URL value sends telemetry to http://gateway:8000/telemetry.
- The host name gateway resolves to the gateway container through Docker's internal DNS.
- Save compose.yaml.
- Confirm that the file sidebar lists compose.yaml directly inside mission-telemetry-lab.
Can't find the Compose file?
- Check that compose.yaml sits beside the gateway folder inside mission-telemetry-lab.
- Check that the file name ends with .yaml.
- Match the indentation shown in the configuration because each nested setting must remain under its service.
Still stuck? Help me check the location and structure of my Compose configuration.
✔️ Awesome, I've got everything!
Your compose.yaml now defines both services plus the internal gateway address.
ⓧ I'd like to double check the full code
- Compare your entire compose.yaml file with this reference:
services:
gateway:
image: mission-gateway
build: ./gateway
ports:
- "8000:8000"
simulator:
image: mission-simulator
build: ./simulator
environment:
GATEWAY_URL: http://gateway:8000/telemetry
Replace the standalone deployment
The standalone gateway currently owns host port 8000. Compose needs that port for its gateway service.
Stopping the standalone container removes only that temporary container because it was launched with --rm. Your images plus source files remain available.
- Switch back to the Terminal window from earlier.
- Replace the standalone container with the Compose application by running these commands:
docker stop mission-gateway
docker compose up --build -d
What do these commands do?
- The first command stops the standalone gateway that owns host port 8000.
- The second command builds the service images before starting the Compose application.
- The -d flag leaves the gateway running in the background.
- Compose creates the default network that connects both services.
- The simulator uses its five delivery attempts to absorb a short gateway startup delay.
You should see Compose finish without an error. The gateway continues serving host port 8000 through the new application.
- Visit http://localhost:8000/health in your browser.
You should see {"status":"ready"}. This confirms that the Compose gateway is reachable from your Mac.
Compose didn't start?
- If the terminal reports that port 8000 is unavailable, confirm that the first command stopped mission-gateway.
- If the build fails to find a file, confirm that Terminal still points to the mission-telemetry-lab folder.
- If the health page does not load, confirm that Docker Desktop is still running.
Need another set of eyes? Help me diagnose why my Compose gateway did not start on port 8000.
Verify end-to-end delivery
A healthy gateway proves that the server is available from your Mac. The simulator logs prove that container-to-container delivery now works.
Before you run the check, predict the simulator's result on the Compose network.
- Inspect service state plus simulator logs by running these commands:
docker compose ps -a
docker compose logs simulator
What do these checks prove?
- The first command includes stopped containers so you can see the simulator's final state.
- The second command reads the simulator's output after its one-time run.
- An exited simulator is expected after one successful delivery.
You should see DELIVERY CONFIRMED: HTTP 200 in the simulator logs. The simulator should appear exited after completing its one-time delivery.
That closes the loop. The failed container now delivers through the Compose network.
- Visit http://localhost:8000/telemetry/latest in your browser.
You should see the latest report with sensor_id set to SIM-ALPHA-01 plus battery_percent set to 74.
Strong recovery. Your gateway now receives synthetic telemetry through service-name DNS.
Don't see a confirmed delivery?
- Check that GATEWAY_URL in compose.yaml is exactly http://gateway:8000/telemetry.
- Check that the service name remains gateway because Docker DNS uses that name.
- Check the gateway health endpoint before repeating the Compose launch commands.
Still seeing failed attempts? Help me trace why the simulator cannot reach the gateway service.
Your two containers now share a working network path. Next, you'll use status checks plus logs to complete a documented recovery drill.
Run an Operator Recovery Drill
Your Docker Compose application now delivers telemetry through service-name DNS. A successful deployment alone does not prove you can support it during an outage.
This drill turns recovery into a repeatable operator runbook. You will use service state plus logs to move from a controlled outage to verified delivery.
In this step, get ready to:
- Document a repeatable recovery procedure in RUNBOOK.md.
- Diagnose a controlled gateway outage with service state plus logs.
- Produce recovery evidence suitable for a concise operator handoff.
Create the operator runbook
A runbook gives an operator a known baseline before an outage occurs. It also preserves the diagnostic sequence when the pressure is on.
- Switch back to Visual Studio Code from earlier.
- Create RUNBOOK.md at the top level of mission-telemetry-lab using the Explorer sidebar.
- Replace its blank contents with the runbook below:
# Mission Telemetry Gateway Runbook
## Expected state
- `gateway` is running and publishes port `8000` to the Mac.
- `simulator` exits after one successful delivery. This is normal.
- `http://localhost:8000/health` returns `{"status":"ready"}`.
## Diagnose
1. Run `docker compose ps -a` to inspect service state.
2. Run `docker compose logs gateway` to inspect gateway activity.
3. Run `docker compose logs simulator` to inspect the last delivery attempt.
## Recover
1. Run `docker compose start gateway`.
2. Run `docker compose run --rm simulator`.
3. Confirm `DELIVERY CONFIRMED: HTTP 200`.
4. Open `http://localhost:8000/telemetry/latest` and confirm the latest report.
## Escalate
If the gateway will not start, record the service state and relevant log lines before changing configuration or rebuilding images.
How does this runbook help?
- The Expected state section defines the healthy baseline for both services.
- The Diagnose section turns service state plus logs into evidence.
- The Recover section restarts the gateway. It then sends fresh telemetry before checking the browser.
- The Escalate section protects useful evidence before anyone changes the system.
- Save RUNBOOK.md.
- Confirm the Explorer sidebar lists RUNBOOK.md alongside compose.yaml.
Runbook missing or incomplete?
- Check that RUNBOOK.md sits directly inside mission-telemetry-lab.
- Compare each heading with the full reference below if a section is missing.
Still stuck? Help me check why my operator runbook is missing or does not match the required recovery procedure.
✔️ Awesome, I've got everything!
Your saved RUNBOOK.md now covers the healthy baseline, diagnosis, recovery, verification, and escalation.
ⓧ I'd like to double check the full code
# Mission Telemetry Gateway Runbook
## Expected state
- `gateway` is running and publishes port `8000` to the Mac.
- `simulator` exits after one successful delivery. This is normal.
- `http://localhost:8000/health` returns `{"status":"ready"}`.
## Diagnose
1. Run `docker compose ps -a` to inspect service state.
2. Run `docker compose logs gateway` to inspect gateway activity.
3. Run `docker compose logs simulator` to inspect the last delivery attempt.
## Recover
1. Run `docker compose start gateway`.
2. Run `docker compose run --rm simulator`.
3. Confirm `DELIVERY CONFIRMED: HTTP 200`.
4. Open `http://localhost:8000/telemetry/latest` and confirm the latest report.
## Escalate
If the gateway will not start, record the service state and relevant log lines before changing configuration or rebuilding images.
Induce and diagnose an outage
A recovery procedure becomes useful when it survives a controlled failure. This drill stops only the gateway container.
The application network remains available. Your source files also remain unchanged.
- Switch back to Terminal from earlier.
- Stop the gateway service by running this command:
docker compose stop gateway
What does this command do?
Compose stops the existing gateway container without removing it. The service keeps its existing network attachment.
Its port configuration also remains available for recovery.
Before you run the simulator, consider whether it can deliver while the gateway process is stopped.
- Test delivery against the stopped gateway by running:
docker compose run --rm simulator
This failure is expected
You will see five attempts beginning with DELIVERY FAILED. This shortfall is intentional.
The simulator has no running gateway process to accept its request. The failed delivery proves the outage is visible from the client side.
- Inspect the service states by running:
docker compose ps -a
What does the service state show?
The gateway service appears stopped. That status matches the failed delivery.
The original simulator service appears exited after its earlier one-time delivery. That exited state is normal after success.
- Inspect the gateway's last recorded activity by running:
docker compose logs gateway
What do the gateway logs prove?
The logs preserve the gateway's last known server activity. They give you evidence from before the service stopped.
No successful telemetry request appears for the test you ran during the outage.
Outage evidence does not match?
- Check that Terminal is still inside mission-telemetry-lab.
- Confirm the stop command completed before you run the simulator test.
- Use the status output to confirm you are inspecting the services from compose.yaml.
Need another pair of eyes? Help me interpret my gateway service state and logs during this recovery drill.
Recover and hand off
Starting the existing service keeps the Compose configuration intact. The gateway's latest report lived in memory, so recovery needs a fresh simulator delivery.
- Start the existing gateway container by running:
docker compose start gateway
What does this command restore?
Compose starts the stopped gateway container with its existing service configuration. The published port becomes available again.
The gateway also returns to the Compose network under the stable gateway service name.
Before you run the recovery test, consider whether service-name DNS still works after the gateway restarts.
- Send a fresh telemetry report by running:
docker compose run --rm simulator
What does the confirmation prove?
You will see DELIVERY CONFIRMED: HTTP 200. The simulator reached the recovered gateway through service-name DNS.
The successful response proves the gateway accepted fresh telemetry after the outage.
- Refresh the browser tab from earlier at http://localhost:8000/telemetry/latest.
You should see SIM-ALPHA-01 in the returned telemetry. The fresh report completes the recovery check.
Delivery still failing?
- Run the status check from your runbook again if the gateway is still unavailable.
- Repeat the simulator command shown above if the browser returns a waiting response.
- Check the gateway logs for activity from the recovery attempt.
Still blocked? Help me diagnose why telemetry delivery still fails after restarting the gateway.
Recovery complete. Fresh telemetry from SIM-ALPHA-01 reaches the gateway again.
Shape the operator handoff
A concise handoff gives the next operator enough evidence to trust the recovery. Use these five points in order.
- Symptom: Telemetry delivery failed while the gateway was unavailable.
- Evidence: Service state showed the stopped gateway. Logs preserved its last activity.
- Root cause: The gateway service had been stopped.
- Recovery action: The existing gateway service was started again.
- Final verification: A new delivery returned HTTP 200. The latest endpoint displayed the report.
- Rehearse the operator handoff in 30 seconds using the five points above.
Your lab now has a repeatable recovery procedure backed by fresh telemetry evidence.
Secret mission
Add Contract-Failure Detection
Add a malformed-payload mode to the simulator. You will prove the gateway is reachable while FastAPI rejects telemetry that breaks the API contract.
Clean Up Your Resources
Clean Up Your Resources
This lab leaves only local Docker resources on your Mac. Choose whether to keep the lab running, pause it for later, or delete it entirely.
Cost warning
Licensing is easy to overlook when everything runs on your own Mac. The $0 estimate assumes eligible personal or educational use of Docker Desktop.
Government entities require a paid subscription. Ineligible professional use also requires a paid subscription.
Resources you used:
- One running gateway container publishing port 8000 to your Mac.
- A default bridge network created for the Docker Compose application.
- Two local container images: mission-gateway plus mission-simulator.
- The local mission-telemetry-lab source folder, including RUNBOOK.md.
Keep everything running
No action is needed. Choose this if you are still demonstrating the telemetry integration or practising recovery drills.
- Leave the gateway active at http://localhost:8000.
- Keep the default network available for service-name DNS.
- Keep both service images available for future container runs.
- Keep the mission-telemetry-lab folder available in VS Code.
Pause - I'll come back to this later
Stop the running service to free its port while preserving the containers, network, images, and source files.
- Stop the existing Compose containers by running this command:
docker compose stop
What happens when you stop?
- The running gateway stops.
- The gateway container remains available.
- The default network remains available.
- Both service images remain available.
- Resume the existing containers from the same mission-telemetry-lab folder by running this command:
docker compose start
What happens when you start?
- Docker Compose starts the existing gateway container.
- Your source files stay unchanged.
- Return to http://localhost:8000/health in your browser.
You'll see {"status":"ready"}. Your paused lab is ready for another demonstration.
Delete - I don't want to use this again
This option removes every Docker resource created for the lab. Deleting the source folder completes the permanent reset.
- Remove the Compose containers, network, and service images by running this command:
docker compose down --rmi all
What does this cleanup remove?
- Docker Compose removes the application containers.
- Docker Compose removes the default network.
- Docker Compose removes both service images.
- Delete the local mission-telemetry-lab source folder by running these commands:
cd ..
rm -rf mission-telemetry-lab
ls
What does this folder cleanup do?
- The first line moves Terminal to the folder above the lab.
- The second line permanently removes the lab folder.
- The final line lists the remaining folders. You should no longer see mission-telemetry-lab.
Nice Work!
Nice Work!
You did it! You built a Docker Compose telemetry lab that demonstrates deployment, diagnosis, recovery, and contract validation.
You've learned how to:
- Build a typed telemetry system where a containerized FastAPI gateway accepts reports from a separate synthetic sensor client.
- Reproduce a container localhost failure caused by network namespace isolation. Restore telemetry delivery with service-name DNS.
- Use service status plus logs to diagnose an outage. Follow an operator runbook to restart the gateway and verify a fresh report.
- Complete the optional Secret Mission by adding a malformed-payload mode that distinguishes an HTTP 422 validation failure from a connectivity failure.
Ready to quiz yourself?