Your colleague needs the next result, not a tour of your containers. Perhaps you prepare a weekly digest of public notices, turn approved notes into a reviewable brief, or run a local assistant behind a tool you already use. Yesterday the result arrived. Today the application says running, but the model is missing. Which promise did the green indicator actually make?

A container can make an application repeatable. It does not automatically make that application persistent, reachable only by the people who should reach it, ready for its task, recoverable after a fault, or safe to delete. Those are separate properties. This book gives you a way to see those properties rather than infer them from a reassuring word such as running. The point is a useful workflow that another person can depend on, not a collection of successful terminal commands.

Keep the project you chose. The worked example is deliberately small: an Ollama service provides a local API, and a probe service checks that API from inside a Compose network. Ollama is reachable through an explicit loopback-only host port. Its downloaded model files live in a named volume. The probe has no published port, no host mount, and no model data. You will read the declaration, observe the objects it creates, introduce one controlled address failure, recover it, stop and restart the service, replace its container, and distinguish what each result proves. Use this example as a safe practice bench or apply its reasoning to an approved isolated copy of your own stack. You do not have to turn your project into a literature-note or supplier-message exercise.

Nothing in this book claims that you have run the example. Commands and outputs are anticipated examples using synthetic data. Use an approved non-production computer, reviewed images, and an approved model if you choose to practise later. A container stack may be local while still having access to downloads, mounts, logs, and administrative users that you allow.

Learning objectives

  • Explain the separate roles of an image, container, named volume, project network, and published port in the workflow you are building.
  • Read a small Compose declaration field by field and predict the runtime objects and boundaries it creates.
  • Distinguish a host address from an in-network service address, and a loopback publication from network-wide exposure.
  • Separate process state, API readiness, model presence, and task correctness when assessing a local AI service.
  • Diagnose an expected failure with state, logs, and inspection before changing configuration.
  • Explain why restart, recreation, deletion, persistence, and backup are different claims.
  • Give a colleague a repeatable route to the useful result, a clear unavailable state, and a non-destructive recovery procedure without pretending that a prepared exercise is an observed run.

Start with the result someone needs

Choose one recurring result your existing project should produce. Make it small enough that you can say what a good result contains and who checks it. A public-notice digest might need the original links, accurate dates, and a visible distinction between quoted facts and suggested next steps. An approved documentation helper might need to answer from a specific document set and admit when that set has no answer. These are optional examples, not new assignments. If your existing tool already has a useful task and acceptance check, keep them.

Now identify the dependency. Does a colleague read a reviewed output you deliver, or operate the workflow on an approved local computer? Both are valid. Neither requires you to publish an unauthenticated AI endpoint to the office network. In the first arrangement, you own the local run and the colleague depends on its result. In the second, the colleague also needs the selected engine, configuration, model identifier, start command, task input and recovery instructions. Agree which arrangement you are building before changing a port.

In your project's existing README or operating note, add a short entry in ordinary language: “When this approved input is ready, this operator runs this tool; the result appears here; this person reviews it; if generation fails, this is the manual fallback.” Name real locations when you implement it. Do not invent team roles or claim that a colleague has accepted the handoff before they have used it. A useful agreement might be that a failed run leaves the last reviewed digest untouched and marks it with its original date, rather than replacing it with an empty file or silently presenting it as fresh.

A local model your tools can call established how a tool reaches a local endpoint. Here you make the service behind that endpoint understandable and replaceable. You are not rebuilding the prompt, changing the purpose of the tool, or taking on public server operations. Your own AI server is the next boundary when approved requirements genuinely call for remote access, TLS, staging and server ownership. A colleague's dependence on your result is a reason to explain recovery now, not a reason to expose the laptop now.

Core concepts

Begin with the interface you will operate

You normally operate this example from a terminal in one new, empty project directory. A text editor creates two files in that directory: .env contains public configuration values, and compose.yaml declares the services. The container engine then creates the runtime objects described by the declaration. Docker users invoke the Compose provider with docker compose; Podman users may invoke podman compose after confirming which approved external Compose provider that command selects. These command shapes look similar, but similarity is not a compatibility guarantee. Choose one engine and one provider for the project, record that decision, and do not alternate engines midway through evidence gathering.

The worked commands retain the source manuscript's Docker Compose spelling. That is an example interface, not a requirement to install Docker Desktop or abandon an approved Podman setup. Podman users should first inspect the provider reported by podman compose version and its supported options with podman compose --help. Compose operations then go through that selected provider; engine operations such as inspect and volume inspect go through the selected engine. Do not perform a global text substitution and call it a compatibility test.

On Windows and macOS, Linux containers commonly run in a Linux environment managed by the desktop product or a virtual machine. Your terminal, loopback port, and files begin on the host operating system; the container process runs in the guest Linux environment. Docker Desktop often uses WSL 2 on Windows, while Podman Desktop commonly uses a Podman machine. On macOS, both commonly use a Linux virtual machine. The exact backend and its settings are local facts to inspect, not assumptions this book can make for your computer. On Linux, the engine may run directly on the host; access to a rootful Docker daemon remains highly privileged even if the commands feel routine.

This host-and-guest distinction explains several beginner surprises. A path that exists in your home directory is not automatically visible to a container. A process listening inside a container is not automatically reachable through a host port. A hostname your host can resolve is not necessarily meaningful inside the container network. Conversely, a host application's model stored in a host directory is not automatically available to a new Ollama container. Explicit mounts, networks, and published ports bridge boundaries. Each bridge should be narrow and intentional.

A declaration is a desired shape, not a completed system

Compose is a way to describe related runtime objects in YAML. It is useful because the relationships are written down: which image should become which service, which storage attaches where, which services share a network, and which container ports are published to the host. The declaration is not itself the running system. It can be valid YAML yet refer to an unavailable image. It can create containers that start but whose APIs are not ready. It can describe durable storage while the application writes to a different path. It can be rendered differently when a shell environment overrides a value.

Read Compose in two directions. First, read from top to bottom and ask what each field requests. Then read from the runtime back to the declaration: when you see a container, mount, network, or port, identify the exact field that caused it. That second direction is essential during diagnosis. It replaces guessing with a small set of inspectable relationships.

The companion diagram, prepared as /img/canvases/t11-l03-overview.svg, maps this example. It is an explanatory diagram, not a screenshot and not evidence of any running stack. The host contains the engine and Compose project. Inside the project's network are probe and ollama. The probe uses the service name ollama; it does not need the host port. Ollama has a named volume for its model directory and is the only service with a published route to the host, explicitly limited to 127.0.0.1.

Read “private” in the diagram as the service-to-service route, not an air-gap guarantee. The declaration ai-private: {} names an ordinary Compose network; it does not set internal: true or prohibit outbound traffic. Model and image downloads still require their approved routes. A network name is not a security control, and the diagram does not show every possible engine or host access path.

Image: packaged content with an identity question

An image is a packaged filesystem and configuration used to create containers. It is not a running process and not a durable copy of an application's changing data. An image reference often includes a registry, repository, and tag. For example, an image might be described by a fully qualified registry path and a release tag. A tag communicates intended release selection, but tags can be changed by a publisher. A content digest identifies specific image content more precisely. Review processes commonly require a version and a resolved digest because they answer different questions: what release did we intend, and what content arrived?

Do not turn that principle into an invented pin. A manuscript cannot establish that a digest is current, that an image remains available, or that an organisation has approved it. Before a practice run, use current official provider material and your local approval process. Avoid a floating latest reference for a stack that you expect to repeat. Also avoid short, ambiguous image names when a reviewed fully qualified reference is available; the registry from which an image is resolved matters.

An image is reusable. One image may create zero, one, or many containers. Removing a container does not normally remove the image. Pulling an image does not prove that a service can run it on your machine, that it exposes the expected API, or that it contains the model you plan to use. Treat image identity, process startup, endpoint readiness, model availability, and task output as separate checks.

For the colleague who operates your project, preserve the reviewed reference with the declaration. “Use the same image I used last week” is not an instruction unless the identity can be found. Do not combine an image upgrade with a persistence exercise: changing software and replacing a container at the same time makes a failure harder to explain. First prove replacement with the same reviewed content. A later upgrade deserves its own compatibility and recovery decision.

Container: one replaceable runtime instance

A container is one runtime instance of an image plus environment, mount, network, command and port settings. It has an identifier and a writable layer. Stopping and restarting retain that object; removing and recreating replace it. A restart therefore cannot prove that important data lives outside the container, and it does not apply newly edited creation settings. The lifecycle walkthrough below follows those differences through actual observations.

Not every write needs preserving. The probe's retry state is disposable; downloaded models are intended to survive replacement. Find each application's actual write path before deciding what storage it needs.

Volume: a separate lifecycle for data

A named volume is engine-managed storage with a lifecycle separate from its containers. Here, t11-l03-ollama-models mounts at /root/.ollama. Replacement should produce a different container ID, reattach that same volume at that destination, and leave the exact approved model available. All three observations matter: a volume listed by the engine is not proof that Ollama is using it.

Persistence is not backup. A retained volume can still be deleted, corrupted or lost with its host. docker compose down normally retains declared named volumes; docker compose down --volumes requests their deletion. Inspect ownership and users before deliberate deletion, and never make the deletion flag habitual.

The model volume is not an output archive either. Keep the reviewed digest, brief or other result in your project's approved destination with its source and review status. An Ollama model surviving replacement says nothing about unique records written by another service. Those need their own storage and recovery decision.

Network: private service discovery is not publication

Compose gives participating services a shared network and service-name discovery. The probe uses http://ollama:11434, not a host DNS name or a copied container IP. That stable name can resolve to a replacement container whose IP has changed.

The service-to-service route needs no published host port. Host publication and administrative access are separate paths. When ollama-typo fails, restore the declared service name rather than guessing an IP or broadening access. The request traces below show why those apparent shortcuts target the wrong boundary.

Published port: a deliberate host boundary

The Compose mapping 127.0.0.1:11434:11434 has three parts. The first 11434 is the host port. The final 11434 is the container port. The leading 127.0.0.1 is the host interface restriction: the intended publication accepts connections through host loopback. A host client can use an address such as http://127.0.0.1:11434; the probe should still use http://ollama:11434 inside the project network.

Binding to 0.0.0.0, or omitting a host address where the provider publishes broadly by default, can make the service listen on all IPv4 interfaces. That may make it reachable from a LAN or another network path. Do not treat a firewall as a substitute for declaring the narrow endpoint you intend. For this local exercise, keep the explicit loopback binding. Authentication, TLS, firewall policy, remote access, and team access are separate deployment concerns, not repairs for a local stack accidentally published too broadly.

Check the engine version as part of that boundary. Docker's port-publishing documentation warns that releases older than 28.0.0 could expose localhost-published ports to hosts on the same layer-2 segment. It also describes direct-routing configurations that change reachability. A printed loopback mapping is evidence of the configured publication, not a universal proof of isolation. Do not use an affected or unreviewed environment for a sensitive local workflow or weaken the binding to work around a reachability problem.

Before starting, identify whether host port 11434 already has an owner you control. On Windows, Get-NetTCPConnection -LocalPort 11434 -ErrorAction SilentlyContinue can help find a listener. On macOS, lsof -nP -iTCP:11434 -sTCP:LISTEN can do so. On Linux, ss -lnt can show listening TCP ports. An occupied port is evidence to investigate, not a reason to stop an unknown process or broaden the binding. If an approved service needs the port, choose a deliberate alternative loopback port and update the documented host clients together.

Four readiness questions, not one green light

Local AI examples often fail because four distinct states are collapsed into “it is up.” Keep them separate.

QuestionEvidence in this exampleWhat a pass does not prove
Is the container process running?compose ps --all shows a running service.That the API accepts requests.
Is the API ready?The probe reaches /api/tags and prints PROBE_READY.That the selected model exists, or that the API remains available later.
Is the model present?ollama show succeeds for the exact approved identifier.That a generation is correct.
Is the task result correct?A human checks the result against its approved input and acceptance rule.That the port, volume, or recovery boundary is correct.

The short depends_on form shown here is useful for creation order but does not establish application readiness. The probe therefore uses bounded retries against the actual endpoint it needs. If the request fails, it exits and leaves an error in its logs. An exited probe is more honest than a dashboard that implies success simply because the Ollama process began.

Likewise, an Ollama health check such as ollama list can provide a useful process/application signal, but it is not a proof that a particular model is installed or that a prompt result meets its requirements. An endpoint can respond while a model pull is incomplete. A model can generate a fluent response that changes a synthetic record value. The reviewer must check the output shape and values, because container health cannot judge task correctness.

The example probe makes one startup request, then sleeps for 7,200 seconds so its successful state is available to inspect. It is not a continuous monitor and it intentionally ends after that interval. An old PROBE_READY line is historical evidence. Before relying on a later run or claiming recovery, make the fresh request shown in the lifecycle checks. A colleague needs to know whether the result is usable now, not merely whether the endpoint once answered.

Inspect before improvising

When a declared service does not behave as expected, collect evidence in an order that reduces changes.

state -> logs -> inspect -> smallest justified correction -> recreate -> repeat checks

State asks what exists and whether it is running, exited, restarting, or unhealthy. docker compose ps --all is scoped to the project directory and is a good first observation. Logs ask what the main process reported during startup or failure. Use docker compose logs --tail 50 SERVICE so that an old, huge history does not become the only thing you read. Inspection asks what configuration the engine actually applied: selected image, mounts, networks, port bindings, environment, and state. docker inspect can contain more metadata than an evidence artifact needs, so read it locally and record only necessary conclusions.

This order matters because a port allocation error can happen before a container starts; service logs alone may be empty. A DNS failure in the probe logs points to its address and project network, not to a reason to alter Ollama's host port. A model not found after recreation may point to the volume mount or the actual application data path, not to an inference error. Change one evidenced cause, recreate only the affected service when configuration changed, then repeat the relevant checks. Multiple simultaneous YAML edits create a story, not a diagnosis.

Safety belongs at the boundary it protects

Containers are configured boundaries, not miniature secure computers. Never use privileged: true, an engine socket mount, a broad host directory mount, or a home-directory mount as a casual repair. Those choices can grant a container access far beyond the two-service task. The probe example deliberately has no volume, no published port, no secret, a read-only root filesystem, a temporary filesystem for the location it needs, dropped capabilities, and a no-new-privileges option. These settings narrow its role; they do not establish universal security.

The Ollama service needs one named volume for its model data and a loopback-only port for a host client. Do not add credentials to the public .env example. Do not source arbitrary .env content into a shell merely because you need MODEL_ID; read the one expected value with a small, explicit command. A project that holds secrets needs a different review posture, and commands that print resolved environment values can expose those secrets. Only inspect compose config --environment in the isolated, non-secret practice project described here.

Use synthetic prompts for the practice bench. Do not enter participant, patient, customer, supplier, contract, credential, unpublished, or confidential data because the endpoint feels local. Local processes may write logs, use mounted storage, communicate through allowed networks, or be administered by others. The safe example is intentionally bounded: a fictional literature note or fictional supplier message, a JSON request, and a human reviewer who does not send or route anything. In your chosen project, use public, synthetic or explicitly approved inputs and the existing review boundary; “local” is not a replacement for approval.

Choose one engine and locate the Linux boundary

Choose which engine and Compose provider own this project, then keep every observation in that environment. Creating a volume with Docker and inspecting a similarly named Podman container mixes different object stores. Record the selected local connection as well as the product name. A command affects the engine it reaches, not necessarily the computer on which you typed it.

Imagine creating compose.yaml in C:\Users\Example\t11-l03-local-ai. The file lives on Windows; the provider reads it and asks the selected Linux engine to create Ollama. /root/.ollama is a path inside that Linux container, not the user's Windows model directory. A named volume supplies storage at that path. Similar filenames on the host do not create a relationship; an explicit mount does.

On macOS, the same distinctions apply across its managed Linux boundary. Port forwarding can make host loopback work without making the service name ollama resolve in the host browser. On native Linux the engine may run directly on the host, but container filesystem and network views remain distinct. Inspect the actual backend rather than assuming that one desktop diagram describes every installation.

Provider selection is separate from engine selection. podman compose delegates to an external provider such as docker-compose or podman-compose; docker compose version identifies Docker's installed integration. Confirm parsing, interpolation and lifecycle options in the selected provider. Record any required syntax adjustment before gathering evidence, not halfway through a comparison.

Place four locations in your preflight note: the host project directory, host client address http://127.0.0.1:11434, peer-container address http://ollama:11434, and container model path /root/.ollama. Add the engine, provider, active local connection and volume name. If one is unknown, inspect it before creating anything. This is enough to prevent confusing host-native Ollama, containerized Ollama and another engine's objects without building a paperwork exercise.

Follow an image into a container object

Start before anything runs. Pulling OLLAMA_IMAGE resolves packaged content into the selected engine's image store. It does not create a working API or install your chosen model. Creation then combines that image with the declaration: a new ID, writable layer, network attachment, volume mount, host publication and health check. An image digest identifies content; a container ID identifies an instance; a volume name identifies storage. They answer different questions.

Starting launches the created object's process. It can exit immediately because of an invalid argument, architecture mismatch or application error. Creation succeeded; operation did not. Alternatively, the process can remain running while its API initializes. compose ps --all keeps exited objects visible so you can distinguish those cases rather than inspecting only the survivors.

Next follow a write. Beneath the active /root/.ollama mount it goes to the named volume; elsewhere it ordinarily goes to the container's writable layer unless another mount applies. A stop/start retains both. Model availability after restart therefore cannot tell you which location held the data. That is the trap the replacement test removes.

A restart also retains creation settings. Editing a bad OLLAMA_URL in YAML does not rewrite the existing probe's environment. Recreation builds a new object from the effective declaration. The corrected settings and new ID should belong to the same instance whose logs you inspect.

Remove Ollama's container and its writable layer disappears, while the image and named volume normally remain. Creating its replacement should give a different ID and reattach the intended volume. If the model is then available without a new pull, the observations support persistence outside the old container. Equal IDs fail the replacement claim; different IDs with a missing model establish replacement but not persistence.

Image and volume deletion remain independent. Removing an unused image may require another pull; removing a volume deletes data even after every container has gone. Read cleanup flags as specific object-deletion requests, not as a generic tidying command.

The probe deliberately has no durable data. Its image supplies curl and a shell, its environment supplies the address, and its process requests the API before sleeping. Recreating it should discard the bad settings and temporary state. Both services are replaceable computation; only Ollama needs persistent model storage. This asymmetry is the design, not an omission.

Predict declared, rendered, and applied configuration

Compose configuration exists in at least three useful forms. The declared form is the text you wrote in compose.yaml, .env, and any explicit override file. The rendered form is the configuration the provider produces after interpolation, file merging, defaults, and validation. The applied form is the configuration attached to runtime objects by the engine. Treating these forms as identical creates avoidable surprises. They often agree, but the purpose of inspection is to discover when they do not. A disciplined run predicts the rendered form before creation and then compares selected applied facts after creation.

Begin with the base declaration and predict one runtime consequence per field. ${OLLAMA_IMAGE:?...} should resolve to the reviewed Ollama image reference or stop configuration when no value is supplied. The port entry should retain the explicit host address 127.0.0.1, host port 11434, and container port 11434. The volume entry should refer to the top-level named volume and target /root/.ollama. Both services should join ai-private. The probe environment should contain an internal URL using the service name, while the escaped dollar in its command should remain for the container shell to expand later. These predictions form a compact checklist for rendered output.

Interpolation explains why reading YAML alone is insufficient. Compose obtains values from defined sources with provider-documented precedence. A stale OLLAMA_URL in the current shell can override the intended value in the project's .env, even though the file still visibly says http://ollama:11434. A missing image variable can fail early because the declaration explicitly requires it. $$ has another role: it prevents Compose from replacing the probe shell's ${OLLAMA_URL} during rendering. The rendered command should preserve the shell reference, while the rendered environment should contain the actual internal URL that the shell will read inside the container.

Override files add another transformation. In the controlled failure, the base declaration remains correct and compose.break.yaml changes only probe.environment.OLLAMA_URL. Rendering with both files should predict http://ollama-typo:11434 for the probe while preserving Ollama's image, volume, network, and loopback port. This matters because it bounds the expected failure. If the rendered output also changed the published port or removed the network, the test would no longer isolate one cause. Always validate the same ordered file set that you intend to use for creation; rendering only the base file says nothing about an override that will be supplied later.

Validation is not evidence that objects exist. compose config --quiet can establish that the provider accepts the composed configuration without creating a container, pulling an image, allocating a port, or checking application behavior. Full rendered output can show what the provider intends to apply, but it cannot prove that port 11434 is available or that an image can run on your machine. This is a useful limitation. Configuration errors should be removed before runtime, while runtime errors remain visible as runtime evidence rather than being confused with a YAML or interpolation problem.

After creation, move from prediction to selected applied facts. compose ps identifies project services and their current state. compose port ollama 11434 reports the effective host publication and should begin with loopback for this exercise. Container inspection can reveal the selected image identity, attached network, mount source and destination, environment, and engine state. Volume inspection establishes that the named storage object exists. Do not collect all inspection output merely because it is available. Compare the handful of facts that correspond to your earlier predictions, and preserve only conclusions needed for the exercise's evidence.

Suppose the declaration shows loopback, the rendered output shows loopback, but the observed port appears broad. That mismatch is a stop condition. Do not rationalize it with assumptions about a firewall or desktop implementation. Verify that the command targeted the intended project and engine, inspect the applied binding, and correct the cause before continuing. Suppose instead that the rendered probe URL is a host address. That is already enough to stop before creation because the probe is supposed to use service discovery on its own network. Fixing it at the rendered stage avoids turning a predictable configuration defect into noisy runtime diagnosis.

Applied configuration also has a time dimension. Editing compose.yaml changes the declared form immediately and may change future rendered output, but it does not rewrite an existing container object in place. A restart acts on the existing applied form. Recreation constructs a new applied form from the current rendering. Therefore, after correcting the bad probe URL, the evidence sequence is render the base declaration, force recreation of the probe, and inspect its new behavior. The successful log then belongs to a container created from the corrected declaration, rather than to an old object whose provenance is ambiguous.

This three-form model turns Compose from a magic command into a chain of testable transformations. Declared text answers what you authored. Rendered configuration answers what the selected provider understood for this invocation. Applied inspection answers what the engine attached to a concrete object. Runtime requests answer whether those applied relationships function. When a result differs from expectation, compare adjacent links instead of editing at random: declaration to rendering for precedence or merge errors, rendering to applied objects for creation or stale-container problems, and applied objects to runtime behavior for startup, networking, storage, or application faults.

Trace a request inside the project network

Follow the probe's successful request one boundary at a time. Compose creates probe and ollama containers and attaches both to ai-private. The probe process receives OLLAMA_URL=http://ollama:11434. When its shell runs curl, the client parses ollama as the hostname and 11434 as the destination port. It does not consult the host's browser settings, host loopback publication, or a copied container IP. It asks for name resolution from within its own container network context, where Compose service discovery associates the service name ollama with a reachable endpoint on the shared project network.

The resulting connection travels from the probe's network namespace across the Compose network to the Ollama container's port 11434. No host port is needed for this path. In fact, the service-to-service request could still work if the host publication were removed, provided both services remained attached to the same network and Ollama listened appropriately inside its container. The published 127.0.0.1:11434 entry solves a different requirement: allowing a client on the host to reach Ollama. Keeping these routes separate lets you test internal service communication without accidentally making the probe depend on host forwarding.

The service name is preferable to a container IP because it expresses the stable relationship in the declaration. A recreated Ollama container receives a new container ID and may receive a different network address. Compose can continue to make the ollama service name meaningful to peers on the network. Hard-coding an inspected IP would couple the probe to one disposable instance and could fail after legitimate replacement. The name does not mean that any process anywhere can resolve ollama; its usefulness is scoped to the participating network and the provider's service discovery.

Now trace the deliberate typo. Rendering the override gives the probe http://ollama-typo:11434. curl asks the same network resolver for ollama-typo, but no declared service or approved alias has that name. The request fails before it can establish a TCP connection to Ollama. Ollama can be healthy, its model volume can be mounted, and the host loopback route can work at the same time. The probe still fails because its first required relationship, name resolution, is wrong. This is why a probe log reporting name resolution should direct attention to its URL and network membership rather than to the host port.

Changing the typo to a random container IP would be a poor repair even if it produced one successful request. It would bypass the stable service relationship and encode transient runtime detail. Adding host networking would be worse because it would replace a narrow private route with a different network boundary. Publishing the probe or broadening Ollama to 0.0.0.0 would not fix the misspelled internal name at all. The smallest justified correction is to restore http://ollama:11434, recreate only the probe so its environment is updated, and repeat the same request and log observation.

Other internal failures occur at later points in the trace and therefore look different. Successful name resolution followed by connection refusal can mean that the target process has not begun listening, is restarting, or is stopped. A connection that succeeds but returns an HTTP error means DNS and TCP routing worked, while the endpoint or application rejected the request. A successful /api/tags response means that particular API path answered; it does not prove that the chosen model exists. Reading the error at the correct layer prevents one failure from becoming an excuse to modify every network and application setting at once.

The probe's retries account for a normal timing difference between container startup and API readiness. The short depends_on form arranges creation order, but the Ollama process may need additional time before accepting requests. The shown curl options retry connection refusal and supported transient failures; they do not promise to retry every error. A misspelled hostname may fail immediately. Explicit connection, transfer and retry time limits keep the request from waiting indefinitely. On failure, the shell must exit before printing readiness. Otherwise the failed request would be followed by a successful echo and a sleeping process, producing exactly the false green state the book warns against.

You can narrate an internal success precisely: the probe container resolved the Compose service name on ai-private, connected to Ollama's container port, received a successful response from /api/tags, printed PROBE_READY, and remained observable for its intended lifetime. That statement does not mention host loopback because the route did not use it. It does not claim a model or task pass because the endpoint check did not test either. Precision is valuable here: if later host access fails while a fresh probe request succeeds, investigation can focus on publication and the host boundary.

Trace a request from the host

The host request starts in a different naming and routing domain. A browser, shell, or approved client on the same computer uses http://127.0.0.1:11434. Here 127.0.0.1 means the host's own IPv4 loopback interface. The provider's port publication maps host loopback port 11434 to port 11434 in the Ollama container. On Windows or macOS, the desktop environment may forward that traffic through its managed Linux boundary before it reaches the container. Those implementation details can vary, but the declared security intent remains narrow: accept this route from the same host, not from every network interface.

The host should not use http://ollama:11434. The name ollama is supplied by service discovery on ai-private; it is not automatically entered into host DNS. A host failure to resolve ollama therefore does not show that the project network is broken. It shows that the request began outside the scope where that service name is defined. The correct host address is the explicit loopback publication. Likewise, the probe should not use 127.0.0.1:11434, because loopback inside the probe refers to the probe container itself, not to the host and not to Ollama.

This double meaning of loopback is a frequent source of accidental complexity. Every network namespace has its own idea of “self.” On the host, 127.0.0.1 selects the host loopback route and can reach a published port. Inside the probe container, 127.0.0.1 selects the probe container. Unless the probe itself serves port 11434, that request fails. Replacing the service name with host loopback would therefore move the request to the wrong endpoint. Use addresses according to the caller's location: service name for a peer container, published loopback for a host client.

Read the publication from right to left when predicting traffic. The last 11434 is the destination port in the container. The middle 11434 is the port selected by the host client. The leading 127.0.0.1 restricts the host interface on which the publication listens. Omitting that address can allow a provider to bind more broadly. Writing 0.0.0.0 explicitly requests all IPv4 interfaces. Neither is a harmless convenience in this local exercise. A broad bind can create a route from a LAN or another attached network, subject to platform forwarding and firewall behavior, where no remote access was intended.

The loopback bind is a narrow route, not a complete security system. It does not add application authentication, TLS, authorization, or protection from other processes and users on the host. It does not prove that Ollama has no other route created outside this project. It does establish an inspectable intention for this Compose publication, subject to the version and routing caveats above. That is why the exercise checks compose port and expects a result beginning with 127.0.0.1. If the effective output is broad or ambiguous, stop and resolve the mismatch rather than making a stronger security claim than the observation supports.

Port ownership is checked before startup because only one listener can normally own the same address and port combination. If an approved host-native Ollama is already listening on loopback port 11434, Compose may fail to allocate the publication before the container starts. Ollama service logs may then be empty because the failure occurred in the engine's setup path, not in the application. Preserve the error and identify the listener. Do not terminate an unknown process. Either stop an approved duplicate deliberately or select another documented host loopback port while leaving the container port and internal probe URL unchanged.

That alternative illustrates the independence of the two traces. Suppose the approved host port becomes 127.0.0.1:11435:11434. A host client would use http://127.0.0.1:11435, but the probe would still use http://ollama:11434. Only the host-side route changed. Editing OLLAMA_URL to use 11435 would be incorrect because the service-to-service request does not traverse the host publication. Conversely, changing the internal application port would require coordinated changes to the destination and health or probe assumptions, not merely a different host-side number. Keeping a written trace prevents these unrelated ports from drifting together.

Compare two simultaneous observations to localize a fault. If a fresh probe request succeeds but a host loopback request fails, the API and private route have evidence of success; inspect the effective port publication, host listener, and selected engine. If the host loopback request succeeds but the probe reports name resolution failure, publication and the API have evidence of success; inspect the probe's rendered internal URL and network attachment. If both fail, begin with container state and application logs rather than assuming one network explanation. The two paths are deliberately redundant enough to teach boundaries, not interchangeable enough to substitute for one another.

Separate running, API, model, and task readiness

Use the four readiness questions to localize a fault, not merely to collect four green lights. An exited process calls for logs and applied-configuration inspection. A running process with a failed API request calls for checks of listening readiness, URL, DNS and network attachment. An answering API with a failed ollama show calls for the exact identifier, completed pull, mount and application data path. A generated but incorrect answer calls for task or model review, not new ports or volumes.

Model lookup is narrower than it may sound. It establishes that this application can find the named model in its current storage view. It does not establish license suitability, provenance beyond available provider information, sufficient memory for generation or answer quality. Likewise, receiving a non-error completion is not a task pass. The Lab example can return the requested JSON keys while altering LAB-C-17; the Company example can wrongly label a no-reply message ACTION. A person must compare the output with the supplied facts.

After a lifecycle action, repeat the relevant observations. A fresh /api/tags request proves API recovery from that caller, and ollama show proves model presence. Task recovery requires rerunning and reviewing the selected task. Keep the time scope clear: ps is a snapshot, logs describe past events, and a request proves reachability at that moment. None grants permanent certainty.

The final language can stay simple: process pass, API pass from probe, model presence pass for this identifier, and task pass under human review. Retain individual passes and failures rather than collapsing them into “the stack works.” A task failure blocks delivery even when infrastructure passes. Keep a failed candidate separate from the reviewed result where retention is approved; do not retry until the model happens to say the expected thing or change the acceptance rule after seeing its answer. Tell the colleague whether this run produced a reviewed result, an explicitly stale previous result, or no result.

Diagnose from state, logs, and inspection

Suppose docker compose up -d returns but readiness is missing. compose ps --all shows Ollama running and probe exited. Both objects exist; only one main process remains running. State has selected the next evidence source, not yet supplied a diagnosis.

Read the probe's recent logs. If ollama-typo could not be resolved, compare that literal hostname with the rendered environment and service name. The controlled override supplies a complete causal chain: wrong declared address, wrong applied environment, no matching service. No request reached Ollama, so its logs are not the first priority. If the base file is already corrected, inspect the existing probe: it may still carry the old environment. Recreate only that object from the base declaration.

Now consider a port-allocation error before Ollama starts. Empty application logs are unsurprising; the engine could not establish publication. Identify the listener and its owner before choosing another approved loopback port or stopping an approved duplicate. Never terminate an unknown process for a tutorial.

Repeated connection refusals point further along the request path. Check Ollama's state and recent logs for delayed startup or crashes, then confirm the URL and network attachment. Preserve evidence if startup exceeded the bounded window before considering a timing adjustment. “Connection refused” is not a universal instruction to increase retries.

For a missing model after replacement, compare IDs, inspect the new mount, then look up the exact model. If the mount is correct, check whether the initial pull completed and whether the reviewed image still uses /root/.ollama. Do not repull first and hide a persistence failure. Inspect only the needed facts locally: full inspection output may expose environment values, paths and irrelevant metadata.

After one correction, repeat the failed observation and adjacent prerequisites. A URL correction needs a new probe and fresh request; a publication correction needs port inspection and a host request; a storage correction needs the full replacement chain. Avoid unrelated image, timing, hardening and network edits in the same retry. Your diagnosis should be narratable: observed state, reported failure, applied value, one correction, recreated object, repeated check. Success without that chain may be accidental.

Prove lifecycle and data claims without hiding failures

Before a lifecycle command, predict which objects keep their identity, which are replaced and which data remains. stop/start retain the object; rm and subsequent up replace it; down removes project containers and network, with volume deletion a separate request. That prediction makes the exercise a test rather than a ritual.

The stop/start check must detect unavailability, not merely eventual recovery. An unexpected successful request while Ollama is stopped fails the test. So does an unrelated exec error mistaken for endpoint failure. Inspect the error as well as the exit status. PowerShell uses $LASTEXITCODE; Bash uses an explicit conditional. A later successful show must not erase either mistake.

Replacement needs different non-empty IDs, the expected mount, a fresh API request and the exact model lookup without a new pull. Stop on a failed required step and print the pass marker only after its conditions succeed. Shells generally report the last command's status, not a summary of the whole transcript: a failed assertion followed by a successful service list can otherwise look like a pass. The guarded runnable blocks below keep those failures visible.

Choose cleanup deliberately. To resume later, retain the approved model volume with an owner and cleanup date. To remove data, verify the project and exact volume, ensure no container uses it, then authorize that deletion. If ownership or retention is unclear, ask. Neither indefinite retention nor irreversible deletion should happen by habit.

Backup is a different operation. Copying arbitrary files from an engine's internal volume directory is not automatically valid, especially inside a desktop guest or while an application writes. A real plan needs supported capture procedures, an approved destination, integrity protection, retention and a restore test. Downloadable model files and unique user records may need different treatment. This exercise establishes replacement persistence, not disaster recovery.

Finally, match the result to all its evidence: changed IDs, intended mount, model lookup, fresh API request, loopback publication, running services and human-reviewed task output. No single observation covers the whole claim. That is the honest operational meaning of the exercise.

Runnable exercise

This is optional preparation for a later hands-on journey, not evidence that it has been performed. The original two-service exercise and its single screenshot artifact remain available below. If you bring your chosen project instead, use an approved isolated copy and identify the equivalent service, durable data, client route and task check before any lifecycle action. Do not remove a colleague's working container as a classroom experiment.

Prerequisites and selection

Use an approved non-production computer with one selected local engine and Compose provider. For Docker, docker context show, docker context inspect, docker version, and docker compose version identify the current context, its endpoint and the installed provider. A context name alone is not proof that the endpoint is local. For Podman, inspect podman system connection list, podman version, and podman compose version. Confirm the selected connection and its intended local Linux boundary. A remote server is outside this exercise because it changes the ownership, network, and deletion boundary.

Choose the provider before writing files. Docker Compose and a Podman Compose provider may differ in supported details, image handling, desktop integration, and current behavior. Do not infer universal compatibility from a YAML file that resembles another tutorial. The declaration below shows the intended object model. Before running it with Podman, confirm the provider's current documented behavior and the organisation's approved image references. Do not switch from docker compose to podman compose in the same project directory and then compare results as if they came from one engine.

You need permission to pull two reviewed images, create local containers and a named volume, and bind loopback port 11434. You also need one approved Ollama model identifier, adequate disk and memory, and an approved network window if images or models are not already available. The model installed by a host-native Ollama application is not automatically available to the container; storage is separate unless you deliberately configure a connection, which this exercise does not do.

In your host file manager, create a new empty directory named t11-l03-local-ai. Do not place it inside a real project. It must not contain private documents, another Compose file, credentials, or a .env inherited from unrelated work. Open that directory in your text editor, then open a terminal at that directory. Confirm the displayed working path before running project commands. The directory is an operational boundary: compose down, ps, and logs must refer to the intended stack.

The named volume is a second boundary. The explicit name t11-l03-ollama-models is not made unique by creating another directory. Before the first run, use the selected engine's volume inspect t11-l03-ollama-models. A confirmed “not found” means the example can create it; an engine connection or permission error does not establish absence. If it already exists, inspect ownership and attached containers before proceeding. Use it only if it belongs to this exercise and you are authorized to reuse it. Otherwise choose a distinct approved name and update the declaration and every mount assertion together. Never delete someone else's volume to make the example's name available.

Write the configuration and read it first

In the editor, create a new plain-text file named exactly .env in the empty directory. Replace the placeholders with reviewed public values before use. This manuscript intentionally supplies no fresh image pins or model claims. A release tag and digest must be checked at the time of an approved run. OLLAMA_URL is an internal address, not a host address. Use one unquoted MODEL_ID line with no inline comment; the small loaders below read that deliberately narrow format.

OLLAMA_IMAGE=REVIEWED_OLLAMA_IMAGE_REFERENCE
PROBE_IMAGE=REVIEWED_CURL_IMAGE_REFERENCE
MODEL_ID=APPROVED_MODEL_IDENTIFIER
OLLAMA_URL=http://ollama:11434

Create a new compose.yaml in the same directory, not a replacement for another project's file:

services:
  ollama:
    image: ${OLLAMA_IMAGE:?Set OLLAMA_IMAGE in .env}
    ports:
      - "127.0.0.1:11434:11434"
    volumes:
      - ollama-models:/root/.ollama
    networks:
      - ai-private
    healthcheck:
      test: ["CMD", "ollama", "list"]
      interval: 5s
      timeout: 5s
      retries: 12

  probe:
    image: ${PROBE_IMAGE:?Set PROBE_IMAGE in .env}
    depends_on:
      - ollama
    environment:
      OLLAMA_URL: ${OLLAMA_URL:?Set OLLAMA_URL in .env}
    entrypoint: ["/bin/sh", "-c"]
    command:
      - |
        curl --fail --show-error --connect-timeout 3 --max-time 5 \
          --retry 12 --retry-connrefused --retry-delay 2 --retry-max-time 60 \
          "$${OLLAMA_URL}/api/tags" >/dev/null || exit 1
        echo PROBE_READY
        exec sleep 7200
    read_only: true
    tmpfs:
      - /tmp
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    networks:
      - ai-private

networks:
  ai-private: {}

volumes:
  ollama-models:
    name: t11-l03-ollama-models

Do not copy this as an opaque spell. Read every declaration and predict its outcome.

DeclarationRuntime meaningWhy it exists here
services.ollama.imageCreates the Ollama container from the reviewed image reference.Separates selected software from runtime data.
ports on OllamaCreates a host route from loopback port 11434 to the container's API port.Lets a host client test locally without intentionally publishing to the LAN.
ollama-models:/root/.ollamaMounts the named volume where Ollama is expected to keep model data.Lets model data survive container replacement.
ai-private on both servicesJoins both containers to one project network.Lets the probe resolve ollama by service name; does not prohibit egress.
healthcheckRepeats a command inside the Ollama container.Adds a process/application signal, not task proof.
short depends_onOrders service creation.It does not wait for the API to be ready.
probe OLLAMA_URLGives the probe the internal service address.Keeps the probe independent of the host port.
$${OLLAMA_URL}Leaves one dollar sign for the shell inside the probe.Prevents Compose from consuming the shell variable too early.
bounded curl, followed by `exit 1`
sleep 7200Keeps a successful one-shot probe observable for two hours.Supports inspection; is not ongoing monitoring.
probe hardening fieldsReduces writable storage and privileges available to the probe.Matches the probe's narrow task.
named network and volumeNames relationships outside individual containers.Makes inspection and deliberate cleanup clearer.

The command is not an application model test. It performs an HTTP request to /api/tags, writes no response body to the terminal, prints PROBE_READY only after a successful request, then sleeps so the successful probe remains observable. If the request fails, || exit 1 ends the shell before the success marker. depends_on may have created Ollama first, but the request is what tests the function the probe needs.

The connection limit is three seconds and each transfer is limited to five seconds. Retries have a sixty-second retry window, but that is not an exact sixty-second overall deadline: an attempt already started can finish under its own transfer limit. The chosen image must supply a curl version supporting these options, /bin/sh, and sleep. Confirm those requirements in the image's documentation rather than stripping error handling to make an unsuitable image start.

The double dollar sign deserves close attention. Compose interprets $ for its own interpolation. $$ escapes it so the shell that starts inside the probe receives ${OLLAMA_URL}. The shell can then read the environment value passed to that container. If you changed it to a single dollar sign, Compose might try to interpolate the host-side value early, leading to a configuration that looks plausible but points somewhere unintended.

Validate rendered configuration before creating objects

Environment values in your shell can override .env values. Use a dedicated terminal session for this project and clear stale overrides before testing configuration. On macOS or Linux, the following commands remove the three relevant shell values, then show the resolved values:

unset OLLAMA_IMAGE PROBE_IMAGE OLLAMA_URL
docker compose config --environment | grep -E '^(OLLAMA_IMAGE|PROBE_IMAGE|OLLAMA_URL)='
docker compose config --quiet
docker compose config

On Windows PowerShell, use the equivalent targeted removal and display:

Remove-Item Env:OLLAMA_IMAGE, Env:PROBE_IMAGE, Env:OLLAMA_URL -ErrorAction SilentlyContinue
docker compose config --environment | Select-String '^(OLLAMA_IMAGE|PROBE_IMAGE|OLLAMA_URL)='
docker compose config --quiet
docker compose config

For a Podman run, substitute the selected approved provider command after confirming its syntax and output. Do not run an environment-display command in a real project that may contain credentials. This isolated example deliberately has no secret values, but the habit should still be narrow: inspect only what you need.

Run these inspection commands one at a time. The quiet configuration check should finish without a configuration error. In PowerShell, check $LASTEXITCODE immediately after an external command when its success is required; an error printed by a native tool does not always stop the shell. Do not paste a failed configuration check followed by up and assume the first command blocked the second. The later lifecycle blocks make their required exit checks explicit.

The rendered configuration should show the intended reviewed image references, OLLAMA_URL: http://ollama:11434, the named volume, and the explicit 127.0.0.1 publication. Read the output as a prediction. If it instead contains a host URL, a blank image, a broad port, or a different mount target, stop before pulling or starting. Fix the declaration or the known override, then render again.

Create and observe the initial runtime state

When the rendered configuration is correct and image references are approved, pull and start the project from its directory. Run each command after the preceding command succeeds:

docker compose pull
docker compose up -d
docker compose ps --all
docker compose logs --tail 50 probe

The anticipated successful result is not merely two container names. ps --all should show both services, with Ollama eventually reporting its configured health state and the probe running after its readiness request. The probe log should contain PROBE_READY. If the probe exits, read its log before changing YAML. It may show a connection-refused message while Ollama starts, a DNS error for a wrong service name, or an image/application failure. Each implies a different next question. If your session lasts beyond the probe's two-hour sleep, recreate only the probe from the base declaration before using exec probe again.

Inspect the host route and named volume independently:

docker compose port ollama 11434
docker volume inspect t11-l03-ollama-models
docker compose ps -q ollama

The last command prints the Ollama container ID. In the same terminal, run docker inspect CONTAINER_ID, replacing CONTAINER_ID with that exact non-empty ID. For a Podman environment, use its engine's inspection commands and the ID returned by its selected provider. Do not inspect a container from a different engine because its name looks familiar.

The port result must begin with 127.0.0.1 for this exercise. A result without that loopback host restriction changes the exposure claim and should be corrected before proceeding. In the container inspection, find the selected image, the mount destination /root/.ollama, the named volume, the project network, and the running state. Do not paste a complete inspection record into shared evidence: it can include irrelevant metadata. The point is to connect declaration to runtime state, not to collect maximal output.

At this stage, make a precise statement. You may be able to say: “the engine created two services; the probe reached the Ollama API through the service name; Ollama has this loopback publication; and the declared named volume is mounted.” You cannot yet say that the approved model is present or that it performs your chosen task correctly.

Pull and identify one model

Read only MODEL_ID from .env; do not source the entire file. In a non-secret example on macOS or Linux, run this block in Bash in the project directory. It expects exactly one unquoted setting, as shown above:

test "$(grep -c '^MODEL_ID=' .env)" = 1 || { echo 'Expected one MODEL_ID line' >&2; exit 1; }
MODEL_ID="$(sed -n 's/^MODEL_ID=//p' .env)"
test -n "$MODEL_ID" && test "$MODEL_ID" != APPROVED_MODEL_IDENTIFIER || { echo 'Set an approved MODEL_ID' >&2; exit 1; }
docker compose exec ollama ollama pull "$MODEL_ID" || exit 1
docker compose exec ollama ollama show "$MODEL_ID" >/dev/null || exit 1
docker compose exec ollama ollama list

In PowerShell, select exactly one expected line and reject ambiguity:

$modelLine = Get-Content .env | Where-Object { $_ -match '^MODEL_ID=' }
if (($modelLine | Measure-Object).Count -ne 1) { throw 'Expected one MODEL_ID line' }
$env:MODEL_ID = $modelLine -replace '^MODEL_ID=', ''
if ([string]::IsNullOrWhiteSpace($env:MODEL_ID) -or $env:MODEL_ID -eq 'APPROVED_MODEL_IDENTIFIER') { throw 'Set an approved MODEL_ID' }
docker compose exec ollama ollama pull $env:MODEL_ID
if ($LASTEXITCODE -ne 0) { throw 'Model pull failed' }
docker compose exec ollama ollama show $env:MODEL_ID
if ($LASTEXITCODE -ne 0) { throw 'Model lookup failed' }
docker compose exec ollama ollama list

These examples read the one intended setting. They do not execute arbitrary lines from .env or implement a general dotenv parser. Keep the documented simple format; do not add quoting, duplicate settings or inline comments and expect the loader to interpret them. The expected observation is that ollama show succeeds for the exact approved identifier and ollama list displays it. Record a resolved model identity if the tool supplies one, but do not invent a value when a provider does not show it. A host-native model installation does not establish anything about the model data in this container volume.

Perform a bounded task check

First use your chosen project's approved representative input and existing acceptance rule, if you have them. Open the same host tool you connected in T11-L02, check that its endpoint is the intended host loopback address and its selected model is the approved identifier, then run one bounded input. Inspect the result where your colleague would actually find it. A successful direct CLI prompt does not prove that your application's saved endpoint, parsing or output destination is correct.

The two synthetic framings below are optional worked examples. Choose either if you need a small practice task; neither is a requirement to replace your own project. They reveal a subtle but important point: a model can answer in valid-looking JSON while still violating the requested facts.

For the Lab framing, open the approved model in the Ollama container from the project terminal. In Bash, use:

docker compose exec ollama ollama run "$MODEL_ID"

In PowerShell, use:

docker compose exec ollama ollama run $env:MODEL_ID

You are now at Ollama's interactive prompt, not the host shell. For a multiline prompt, type """, paste the following text, then close with """ on its own line and submit. This keeps the instruction and note in one prompt rather than relying on terminal paste behavior:

Return JSON only with keys record_id, material, temperature_c, and result.
Use only the supplied synthetic note and do not infer missing values.

Synthetic note: Record LAB-C-17 tested a polymer film at 18 C.
Result: no visible change.

The accountable Lab reviewer checks four things. The response must be valid JSON. It must have exactly the requested four keys. It must preserve LAB-C-17, polymer film, 18, and no visible change. It must not add a scientific conclusion, such as claiming a material is safe, stable under all temperatures, or suitable for a product. The JSON numbers 18 and 18.0 have the same numeric value; if your downstream consumer requires an exact textual representation, agree that additional rule before judging. A response that changes the material or adds an ungrounded claim fails the task check even if the API is healthy.

For the Company framing, use the same model command and multiline input method with this fictional prompt:

Return JSON only with keys record_id, label, and basis.
Label must be ACTION or INFORMATION. Use only the supplied synthetic message.

Synthetic message: Record CO-C-17 says delivery remains scheduled for Tuesday.
No reply or approval is requested.

The reviewer checks for valid JSON, exactly three keys, CO-C-17, the label INFORMATION, and a basis grounded in the absence of a requested reply or approval. The expected classification is not permission to send, route, or act on a supplier message. It is a bounded output comparison. An answer labelled ACTION because the word “delivery” appears would demonstrate why semantic-looking output requires a human check.

Return to the host shell before running Compose commands; use Ollama's interactive help if you need the exit command for the selected version. Keep the project directory and model variable in the host session. A line beginning docker compose belongs in that shell, not in the model's prompt input.

For either framing, separate the conclusions in your notes. Endpoint pass means the network and service are functioning for that HTTP call. Model pass means the selected identifier is available in the container. Generation pass means a non-error completion occurred. Task pass means the accountable person compared the output against a bounded source. None substitutes for the other. For your own project, deliver only the reviewed result through its approved destination, not a raw model answer merely because this practice prompt passed.

Controlled failure: break one address, retain the evidence

Now test the diagnostic path without touching model data. Keep .env unchanged. In the editor, create a new compose.break.yaml in the same directory:

services:
  probe:
    environment:
      OLLAMA_URL: http://ollama-typo:11434

This override is intentional and deterministic in the isolated example, where no such service or alias exists. It changes only the probe's internal address. It does not change Ollama, its volume, the project network, or the host loopback publication. Use both files to validate and recreate only the probe. Run each command separately, stopping if validation or recreation fails:

docker compose -f compose.yaml -f compose.break.yaml config --quiet
docker compose -f compose.yaml -f compose.break.yaml config
docker compose -f compose.yaml -f compose.break.yaml up -d --no-deps --force-recreate probe
docker compose -f compose.yaml -f compose.break.yaml ps --all
docker compose -f compose.yaml -f compose.break.yaml logs --tail 50 probe

Read the rendered output before up: only the intended probe address should differ. --no-deps keeps this targeted operation from starting or recreating dependencies; Ollama must already be running from the initial checks. The anticipated failure is an exited probe with a log explaining that the host cannot be resolved. DNS failure may be immediate rather than retried. There must be no PROBE_READY from this new failed probe. Detached up itself may succeed because it created the container; that is not a successful probe request.

That failure is useful evidence because it is specific. The correct diagnosis is not “containers are unreliable.” It is “the probe was given a name that does not match the declared service on the project network.” Check state first, then logs, then compare OLLAMA_URL with the ollama service name. If the probe unexpectedly reaches something, stop and inspect the network and environment instead of declaring the controlled failure passed.

Do not repair this by publishing the probe, adding host networking, setting administrator privileges, choosing a random container IP, or exposing Ollama broadly. Return to the base declaration and recreate only the probe:

docker compose -f compose.yaml config --quiet
docker compose -f compose.yaml up -d --no-deps --force-recreate probe
docker compose -f compose.yaml ps --all
docker compose -f compose.yaml logs --tail 20 probe

A plain restart is insufficient here because the existing probe container was created with the bad environment setting. --force-recreate makes a new probe container from the base declaration, and the explicit -f compose.yaml leaves the failure override out. The pass condition is a running new probe whose log contains PROBE_READY. This sequence demonstrates a disciplined recovery: one changed input, one visible failure, one corrected declaration, one re-created affected service, and one repeated readiness observation.

Stop, recover, and interpret the result

Next prove that an ordinary service stop is visible to a dependent service. In the same project, confirm that the recovered probe is still running. Then stop Ollama, make an internal request from the probe, inspect state and logs, and start Ollama again. The following is a Bash block; do not paste Bash variables, backslash continuations or if ...; then into PowerShell:

docker compose stop ollama || exit 1
if docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 http://ollama:11434/api/tags; then
  echo 'Unexpected success while Ollama was stopped' >&2
  exit 1
fi
docker compose ps --all
docker compose logs --tail 50 ollama
docker compose start ollama || exit 1
docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 \
  --retry 12 --retry-connrefused --retry-delay 2 --retry-max-time 60 \
  http://ollama:11434/api/tags >/dev/null || exit 1
docker compose exec ollama ollama show "$MODEL_ID" >/dev/null || exit 1
echo RECOVERY_PASS

PowerShell uses the same sequence with explicit exit-code checks:

docker compose stop ollama
if ($LASTEXITCODE -ne 0) { throw 'Ollama stop failed' }
docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 http://ollama:11434/api/tags
if ($LASTEXITCODE -eq 0) { throw 'Unexpected success while Ollama was stopped' }
docker compose ps --all
docker compose logs --tail 50 ollama
docker compose start ollama
if ($LASTEXITCODE -ne 0) { throw 'Ollama start failed' }
docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 --retry 12 --retry-connrefused --retry-delay 2 --retry-max-time 60 http://ollama:11434/api/tags
if ($LASTEXITCODE -ne 0) { throw 'API recovery failed' }
docker compose exec ollama ollama show $env:MODEL_ID
if ($LASTEXITCODE -ne 0) { throw 'Model lookup failed after recovery' }
'RECOVERY_PASS'

The first curl should fail while Ollama is stopped. Read the error: it must describe the intended endpoint becoming unavailable, not an unrelated exec failure because the probe ended, a missing executable, or a lost engine connection. A nonzero exit code alone cannot tell those causes apart. If the request succeeds while the target service is stopped, verify that you are in the intended project and that you did not reach an unrelated endpoint. If a guarded block stops before the start command, Ollama remains stopped; inspect the cause, then deliberately use docker compose start ollama and repeat the recovery checks. Do not leave a colleague guessing whether the failure exercise is still active.

After start, the retrying request should eventually succeed, then ollama show should confirm that the selected model remains available. RECOVERY_PASS, together with review of the expected failure, means the service was detectable as unavailable, became reachable again inside the project network, and still exposes the named model. Repeat the selected task check before claiming task recovery. This does not prove automatic failover, high availability, restart policy behavior, remote accessibility, or production readiness. It proves a bounded stop-and-start recovery path in one local project.

Restart is not replacement

An ordinary restart is worth testing but has a limited meaning. In Bash:

docker compose restart ollama || exit 1
docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 \
  --retry 12 --retry-connrefused --retry-delay 2 --retry-max-time 60 \
  http://ollama:11434/api/tags >/dev/null || exit 1
docker compose exec ollama ollama show "$MODEL_ID" >/dev/null || exit 1
echo RESTART_CONTINUITY_PASS
docker compose ps

In PowerShell:

docker compose restart ollama
if ($LASTEXITCODE -ne 0) { throw 'Ollama restart failed' }
docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 --retry 12 --retry-connrefused --retry-delay 2 --retry-max-time 60 http://ollama:11434/api/tags
if ($LASTEXITCODE -ne 0) { throw 'API did not recover after restart' }
docker compose exec ollama ollama show $env:MODEL_ID
if ($LASTEXITCODE -ne 0) { throw 'Model lookup failed after restart' }
'RESTART_CONTINUITY_PASS'
docker compose ps

If this produces RESTART_CONTINUITY_PASS, you know the service restarted, the internal API became reachable, and the named model was still available. The result is useful operationally, but it does not isolate the named volume as the reason model data remained. The same container may retain its writable layer across a restart. Do not overstate it as proof of deletion resilience.

The stronger check deliberately removes only the Ollama service container, then creates a replacement using the unchanged reviewed declaration and image. Do not pull a model or change the model identifier between removal and the post-replacement lookup: that would hide the persistence question. Capture the old and new IDs, verify the mount, wait for the internal API, and verify the same model. In Bash:

before="$(docker compose ps -q ollama)" || exit 1
test -n "$before" || { echo 'Missing original container ID' >&2; exit 1; }
docker compose rm --stop --force ollama || exit 1
docker compose up -d ollama || exit 1
after="$(docker compose ps -q ollama)" || exit 1
printf 'before=%s\nafter=%s\n' "$before" "$after"
test -n "$after" && test "$before" != "$after" || { echo 'Container was not replaced' >&2; exit 1; }
mount="$(docker inspect --format '{{range .Mounts}}{{if eq .Destination "/root/.ollama"}}{{.Name}} -> {{.Destination}}{{end}}{{end}}' "$after")" || exit 1
test "$mount" = 't11-l03-ollama-models -> /root/.ollama' || { echo 'Unexpected model mount' >&2; exit 1; }
docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 \
  --retry 12 --retry-connrefused --retry-delay 2 --retry-max-time 60 \
  http://ollama:11434/api/tags >/dev/null || exit 1
docker compose exec ollama ollama show "$MODEL_ID" >/dev/null || exit 1
printf 'REPLACEMENT_PERSISTENCE_PASS model=%s mount=%s\n' "$MODEL_ID" "$mount"
docker compose port ollama 11434
docker compose ps

Read the expected result as a chain of evidence. before and after must both be non-empty and different, otherwise a replacement did not occur. The inspected mount must name t11-l03-ollama-models -> /root/.ollama, otherwise the new container is not using the intended durable location. The fresh API request and ollama show must succeed, otherwise the readiness or data claim fails. The port result must begin with 127.0.0.1, and both services should be running. Finally, rerun the selected task check. The volume relationship, not the old container, carried the model data through replacement.

On Windows PowerShell, use the same reasoning with explicit variables and exit-code checks:

$before = docker compose ps -q ollama
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($before)) { throw 'Missing original container ID' }
docker compose rm --stop --force ollama
if ($LASTEXITCODE -ne 0) { throw 'Ollama removal failed' }
docker compose up -d ollama
if ($LASTEXITCODE -ne 0) { throw 'Replacement creation failed' }
$after = docker compose ps -q ollama
if ($LASTEXITCODE -ne 0 -or [string]::IsNullOrWhiteSpace($after)) { throw 'Missing replacement container ID' }
"before=$before"
"after=$after"
if ($before -eq $after) { throw 'Container was not replaced' }
$mount = docker inspect --format '{{range .Mounts}}{{if eq .Destination "/root/.ollama"}}{{.Name}} -> {{.Destination}}{{end}}{{end}}' $after
if ($LASTEXITCODE -ne 0 -or $mount -ne 't11-l03-ollama-models -> /root/.ollama') { throw "Unexpected model mount: $mount" }
docker compose exec probe curl --fail --show-error --connect-timeout 3 --max-time 5 --retry 12 --retry-connrefused --retry-delay 2 --retry-max-time 60 http://ollama:11434/api/tags
if ($LASTEXITCODE -ne 0) { throw 'Ollama did not become ready' }
docker compose exec ollama ollama show $env:MODEL_ID
if ($LASTEXITCODE -ne 0) { throw 'Model did not survive replacement' }
"REPLACEMENT_PERSISTENCE_PASS model=$env:MODEL_ID mount=$mount"
docker compose port ollama 11434
docker compose ps

The PowerShell version does not prove more than the Bash version. It makes the same assertions visible in the host's usual shell syntax, including the IDs and final service/port observations required by the optional screenshot. If a command differs for a selected Podman Compose provider, consult that provider's current documentation before running it; do not silently translate a command and assume matching lifecycle semantics.

If the replacement cannot find the model, stop the test and retain the failure. Do not repull and photograph a pass as if the volume had worked. Inspect the new mount and the original model identity, then decide the smallest non-destructive correction. The retained named volume may still contain the data even when the application cannot see it. A missing lookup is not permission to delete storage.

Common observations and the smallest justified response

ObservationWhat it may meanFirst evidence to collectSmallest justified response
up cannot bind port 11434Another process owns the host loopback port.Preserve the error and identify the listener with an OS command.Stop only an approved duplicate or choose another documented loopback port.
Probe exits with name-resolution errorOLLAMA_URL does not match the network service name.Probe state, probe logs, rendered override.Restore http://ollama:11434 and recreate only probe.
Ollama is running but probe is not readyAPI startup is incomplete or endpoint configuration is wrong.ps --all, Ollama logs, probe logs.Wait within the bounded retry window or correct one evidenced setting.
An old readiness line exists but exec probe failsThe one-shot probe ended or the wrong project is selected.Current probe state, exit time and selected project.Recreate only the probe from the base declaration after confirming Ollama is available.
ollama show fails after initial startupThe selected model is absent or model storage is not what you expected.Exact model command, mount inspection, volume inspection.Complete the approved initial pull into the intended volume; do not assume a host copy is shared.
Model disappears after replacementData was not mounted at the actual durable path, or the wrong volume was attached.New ID, mount destination, model command.Preserve the failure before any repull; correct the evidenced mount or identity issue, then repeat replacement.
A restart passes but replacement failsThe container writable layer masked missing persistence.Compare lifecycle action and IDs.Use named storage and test replacement, not restart alone.
A port responds from another devicePublication, engine version or another route violates the intended local boundary.Effective mapping, engine version and host/network routing.Stop use, restore the approved local boundary, recreate if needed, and retest.
A permissions issue tempts a privileged settingThe real requirement is unknown.Image documentation, engine boundary, exact path and user.Stop and review; do not add privileged, socket mounts, or broad host mounts.

Deliberate cleanup

When the exercise is no longer needed, remove the project's containers and network with docker compose down from the verified project directory. This normally preserves the named volume. Keep it only while its owner and intended cleanup date are known. To delete course model data later, inspect the exact volume and ensure no container uses it, then deliberately remove that named volume. Do not turn a cleanup instruction into a deletion shortcut. The fact that the data is synthetic does not make accidental deletion educational.

For a colleague-dependent workflow, “exercise finished” and “service no longer needed” are different events. Agree an interruption before stopping a stack that supplies a recurring result. Preserve the reviewed output and its date in the existing destination, tell the colleague what is unavailable, and record whether the next run requires start of retained containers or up to recreate removed ones. Never leave compose.break.yaml active in the normal operating command.

Make the handoff work without you

Return from the practice bench to your chosen result. The local stack is a means of producing it. In your existing project note, replace vague instructions such as “start the AI” with the actual working directory, selected engine/provider, configuration filenames and start command used in your successful run. Name the local client endpoint separately from the service-to-service address. Link the reviewed image and model identities rather than telling the next operator to choose whatever is newest.

Then write the shortest useful operating route: open the approved tool, select or confirm this endpoint and model, use this approved input, inspect the candidate here, apply this acceptance rule, and deliver the reviewed result there. Use the tool's real interface labels from your own setup, not invented buttons. If a colleague only consumes the result, they need its location, source, review status and freshness; they do not need container-admin permissions. If they also operate the stack, give them the authorized local setup and recovery route. Do not solve that difference by sharing a privileged engine socket or an administrator account.

Include the failure route beside the operating route. The first questions are still state, logs and inspection. For the example's stopped service, docker compose start ollama is the normal recovery action while the container exists. If only the container was removed and the correct volume remains, docker compose up -d ollama creates a replacement. Neither instruction includes volume deletion, a model repull disguised as persistence, a broad bind, or a speculative image upgrade. After recovery, a fresh API request, model lookup and the project's task check must pass before the next result is called ready.

Give the colleague one approved input or synthetic rehearsal case and ask them to follow the handoff without live coaching. They should be able to find the expected result and explain what happens if it is absent or stale. An authorized operator should also be able to locate the retained model volume and carry out the bounded recovery in the isolated copy. Do not fault the reader for missing a command you kept in your head; put that missing step into the operating note. Record what actually happened, including any point where you intervened. A prepared handoff is not yet an independent-use result.

This is the dependency you are building: the colleague can use the recurring result, and a recoverable local process sits behind it. It is not a claim of continuous availability. If the laptop is off, the service may be unavailable. If the task result fails review, it must not be delivered as accepted. A clear manual fallback and an honest unavailable state are more useful than an impressive diagram that conceals both limits.

Exit check

For your chosen project, deliver one colleague-usable handoff in the project's existing documentation or approved destination. Link the recurring reviewed result or a clearly labelled rehearsal result, the configuration and selected engine/provider, the acceptance check, replacement evidence for the data you intend to retain, and the bounded non-destructive recovery route. A colleague should be able to find the result and its freshness, know who operates it, and recognize when to stop rather than improvise. If the run or colleague check has not happened, mark that part prepared, not passed. You do not need a new tracker, CSV or mandatory Lab/Company fixture.

The following retains the original reading-pilot contract for learners who choose the optional Ollama/probe exercise. It is a compact infrastructure evidence artifact, not a substitute for the useful-result handoff in your own project.

Deliver exactly one artifact for that optional exercise: one evidence screenshot showing the two-service local AI stack running after the Ollama service container was replaced, with the terminal line REPLACEMENT_PERSISTENCE_PASS visible.

The artifact passes only when all of these visible conditions can be read in the same image:

  • Different, non-empty before and after Ollama container IDs are visible.
  • The pass line names the exact approved model identifier and t11-l03-ollama-models -> /root/.ollama, or the deliberately selected equivalent name used consistently throughout your run.
  • The visible compose port result begins with 127.0.0.1.
  • compose ps identifies both ollama and probe as running.

The controlled probe failure, stop/start recovery, ordinary restart check, and selected task check should also have passed during the same real run. They do not require separate submitted artifacts for this optional exercise. Do not submit extra logs, YAML, notes, or screenshots in place of its single contract artifact. Where visual capture is inaccessible, arrange an approved text-equivalent evidence accommodation before the journey; do not shrink essential text until the screenshot is unreadable. Keep operational instructions as selectable text in the handoff, not only in an image.

Before capturing, crop or recapture to remove credentials, tokens, personal data, private hostnames, broad filesystem paths, real prompts, and unrelated workloads. A screenshot of a chat answer, a running container row without replacement evidence, a volume list without the two services, or an anticipated output copied from this book does not pass. Do not manufacture the pass marker or alter a screenshot to hide a failed required check.

Source notes

  • Original Sol manuscript material supplied the two-service Ollama/probe exercise, named-volume replacement reasoning, loopback binding, controlled bad-address failure, stop/start recovery, synthetic Lab and Company prompts, and the single-artifact exit contract. Its retained references include Docker container and Compose concepts, Docker volumes, Docker logs and security material, Podman documentation, and Ollama documentation.
  • The earlier reading-pilot revision credited Terra with reorganising the material into capability template v1, expanding runtime objects, host versus guest Linux, provider selection, declared/rendered/applied configuration, address domains, readiness, diagnosis and lifecycle reasoning, and creating the original topology SVG. It also credited Sol with final expansion, failure handling and canvas labels. These contributions remain part of this edition; this is not a newly originated Astra book or diagram.
  • This Astra edition revises the opening and learning path around a learner-chosen recurring result, adds the colleague operating and recovery handoff, keeps both synthetic tasks optional, and corrects places where executable examples could imply success after a failed command. It also clarifies that the named network is not an air gap, the probe is one-shot, and loopback publication has version and routing limits.
  • Fresh official-source checks for this edition: Podman compose (opens in a new tab) for external-provider behavior; Docker Compose up (opens in a new tab) for recreation, mounted-volume preservation and --no-deps; Docker port publication (opens in a new tab) for host binding, direct routing and the pre-28.0.0 localhost warning; Compose networks (opens in a new tab) for network declarations; and Ollama CLI (opens in a new tab) for model commands and multiline input. These pages were consulted on 9 September 2026, not exercised as a running stack.
  • Retained public references: Docker container concepts (opens in a new tab), Docker Compose (opens in a new tab), Docker volumes (opens in a new tab), Docker Engine security (opens in a new tab), Podman installation (opens in a new tab), and Ollama Docker guidance (opens in a new tab). These are inherited references for checking current provider behavior and primary concepts, not a claim that all pages or current UI details were freshly verified during this revision.
  • The curl manual (opens in a new tab) sections for --connect-timeout, --max-time, --retry, --retry-connrefused, --retry-delay and --retry-max-time were checked from the saved official response. They support the distinction between connection limits, per-transfer limits and the retry window, and between transient failures and errors that are not retried. Confirm option support against the selected image's curl version before running. No current image pins, model suitability or provider compatibility were established here.
  • All record IDs, prompts, organisations, notes, and expected observations in the worked examples are synthetic. No application runtime was executed while preparing this manuscript.

Unknowns and limitations

This book does not establish which engine, Compose provider, image release, digest, model, hardware profile, license, or desktop backend is suitable for your organisation. Check current official documentation, image provenance, model terms, local connection, and approval requirements before a run. Docker and Podman are related tools, not interchangeable evidence; provider behavior and supported Compose fields must be confirmed in the selected environment.

The exercise does not cover production deployment, Kubernetes, remote access, TLS, authentication, multi-user service access, backups and restore testing, GPU configuration, monitoring, upgrades, incident response, or broad compatibility. A named volume surviving one container replacement is not a backup or availability claim. A loopback binding limits this particular host publication but does not prove every other access path is safe. Container hardening settings shown for the probe are narrow example decisions, not a universal security baseline.

The health check, API probe, model presence check, and output comparison deliberately measure different things. Passing one does not pass the others. The model examples do not establish reliability for real records, scientific inference, supplier decisions, or automated action. Keep human review and data boundaries appropriate to the actual task. The colleague handoff described here is an instruction for a real check, not a reported classroom result.