Executable Deployment Recipes with an Honest Verification Date
A versioned local deployment recipe records its prerequisites, owned resources, observed HTTP results and cleanup, making its verification date a claim about an actual run.
Explore the mechanism ↓
Reading tools Listen or translate
Read aloud
Uses an installed voice on your device.
Translate article
Machine translation. Code and evidence notes stay in English.
Original English text.
START WITH THE MECHANISM
Interactive companion and laboratory update: 12 November 2026. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.
THE FULL INVESTIGATION
Executable Deployment Recipes with an Honest Verification Date
Dates and technology context
This is a retrospective editorial timeline. Topic context: 25 September 2026. First publicly available: 12 November 2026. Current text, visuals and laboratory revisions were prepared on 12 November 2026.
Exact manifest, image digests, runtime10.0.15, preflight/cleanup cases and execution receipt retain12Nov2026.
A deployment tutorial says that it was updated yesterday. Its command sequence may still depend on a file left by an earlier run, an unmentioned tool version or a port that happened to be free on the author's machine. The date tells us that someone edited the page. It does not tell us what was executed.
An executable recipe should make a stronger, narrower promise. Given declared inputs and prerequisites, it creates a finite set of owned resources, observes a specified result and closes its responsibility for those resources. If a prerequisite is missing, it stops before provisioning. If execution fails halfway through, the report distinguishes the original failure from cleanup failure.
This article builds that contract around a real local ASP.NET Core service behind an Nginx TLS proxy. The companion article examines the hosting failures themselves. Here the focus is the recipe that makes those failures reproducible: manifest validation, input identity, negative preflight cases, partial-resource cleanup and a machine-readable execution receipt.
The fixture was executed on 12 November 2026. It passed four recipe-level scenarios and five HTTP configurations. The result includes source hashes, published-artifact hashes, exact image identities and observed cleanup actions. No Azure account or public deployment was used. The local example demonstrates a method that can inform a later cloud recipe, not a claim that the cloud-specific provisioning boundary has already been tested.
Give the recipe one outcome and a finite scope
01 / See the idea
One recipe, finite owned resources
The invocation owns a network, a proxy, one application container at a time and temporary fixture files. It does not own the Docker daemon or unrelated containers.
Interpretation and evidence
- Record exact created identities.
- Keep image caches outside cleanup scope.
- Never replace cleanup with system prune.
The recipe's outcome is not “Docker started” or “the build succeeded.” It is a set of observed HTTP behaviors through a particular request path. The fixed configuration must return HTTPS context, the /shop path base, the expected route and the declared artifact. Known broken configurations must exhibit their intended failures. Untrusted forwarding claims must not become authoritative.
The owned runtime resources are equally concrete: one uniquely named Docker network, one proxy container and one application container at a time. The application container is replaced sequentially for each configuration. Both containers have one CPU and 512 MiB limits, and every published port binds to 127.0.0.1.
The recipe also creates a temporary local directory containing a short-lived TLS certificate, its private key, an Nginx configuration and a synthetic text artifact. That directory belongs only to the invocation. It is removed after the containers stop, and the key is never included in the result report.
The existing Docker daemon is a prerequisite, not a resource the recipe owns. The script does not stop it at the end. Likewise, image layers are dependencies that may remain cached; the cleanup contract concerns containers, the network and temporary fixture files. Calling docker system prune would exceed that scope and risk destroying unrelated work.
This finite ownership model is the first practical difference from a loose list of commands. It tells the reader what the recipe will touch, what a successful result means, and which resources should still exist afterward. Without that boundary, cleanup is easy to describe vaguely and hard to verify safely.
Separate build provenance from runtime verification
The application is built locally with SDK 10.0.400 and targets net10.0 with runtime 10.0.15 pinned. The publish output is framework-dependent, and the recipe starts its DLL inside the selected ASP.NET container. A local global.json selects the SDK without changing the user's global configuration.
The build command is straightforward:
dotnet publish -c Release --no-self-contained -o publish
That command establishes a published artifact. It does not establish that the proxy can reach it, that the forwarded scheme is trusted or that an artifact path exists inside the container. Those are runtime observations made by the recipe.
The manifest records both a human-readable tag used during acquisition and the immutable image reference used during execution. The accepted fixture uses ASP.NET 10.0.15 and Nginx 1.28.0-alpine, with the exact registry digests retained in recipe.json. Official publisher pages establish the image families, while the pull and inspect results establish which artifacts were used in this run. Microsoft ASP.NET image registry, official Nginx image.
The result report also includes the image ID returned by Docker, the runtime reported by the application and the Nginx executable's version string. These are related pieces of evidence, not substitutes for one another. A tag is a convenient name, a digest identifies an artifact, and a running process reveals what actually executed.
The recipe hashes its source files, project file, SDK selector, manifest, Python runner, published DLL and runtime configuration. A future edit can therefore be distinguished from the exact inputs attached to the accepted result. Merely retaining the date would not provide that distinction.
This is still not a reproducible-build proof. The fixture does not compare two independent builds bit for bit or attest a trusted supply chain. It preserves enough identity to make the local execution claim inspectable and to identify which changed inputs would invalidate it.
Make prerequisites observable before provisioning
The first preflight condition is an explicit environment selection. The caller must say local-lab. Omitting it or selecting another value fails before the recipe creates a network or temporary certificate. This is a scope guard, not an authentication mechanism or a general authorization system.
The script then checks that the published application and runtime configuration exist. It reads the runtime manifest and requires the expected framework version. It verifies that the requested loopback port can be bound, confirms that the Docker server responds, and inspects both pinned images locally.
The order matters. A missing application artifact should not lead to a half-created proxy. An occupied port should not trigger an attempt to terminate the process using it. An unavailable image should be a clear prerequisite failure rather than an implicit download hidden inside an otherwise deterministic run.
Image acquisition is a separate preparation step. The recipe executes by digest after the image is present. A user can decide when to fetch dependencies, while the functional run records the artifacts it found. This also keeps a later failure easier to classify: registry access and application behavior are different boundaries.
Preflight is a check at a moment in time. A free port can become occupied before Docker publishes it. The recipe cannot eliminate that race by checking harder. It must also handle a later container-start failure and clean up any resource it already created. That is why preflight and rollback are complementary, not competing approaches.
The actual negative tests cover a missing environment and a port held by an owned test listener. In the occupied-port scenario, the listener remains open after the recipe refuses to proceed. The test does not count a failure as successful if it first kills the thing that made the condition inconvenient.
Allocate resource identity before depending on the resource
Each invocation generates a unique ownership token. Containers receive that token as a label, and their exact IDs are stored as soon as Docker creates them. The script uses docker create followed by docker start, rather than assuming that a failed combined run leaves nothing to track.
This sequencing gives cleanup an identity even if startup fails. A created but unhealthy container still belongs to the invocation and can be removed by ID after its label is checked. Selecting resources only by a broad name prefix would be weaker, especially in a workspace where several tasks run concurrently.
The network is also uniquely named and its returned ID is retained. The recipe does not enumerate every network on the machine and decide which ones look disposable. It removes the network it created after its owned containers are gone.
The proxy gets its address from Docker's actual network state. The first implementation attempted to assign static addresses within an automatically chosen subnet and Docker rejected that combination. The correction was to stop guessing the address and inspect the proxy's allocated address instead. That address becomes the application's explicit trusted-proxy input for this invocation.
This is an example of a setup failure that belongs in the development record without being confused with the intended HTTP failure cases. The script had not yet tested forwarded headers. A network-allocation rejection cannot serve as evidence that the application correctly rejected an untrusted proxy.
The final network is a dedicated bridge. It is not claimed to block all outbound communication. An internal-network attempt on this host prevented the loopback-published endpoint from becoming reachable, so the working recipe uses ordinary bridge connectivity and explicit loopback publication. The distinction is recorded rather than hidden behind a generic word such as isolated.
Verify exposure and resource limits from the created container
Passing the desired flags to Docker is an action. Inspecting the created container is an observation. The recipe checks that NanoCpus corresponds to one CPU, memory is 536,870,912 bytes, and every published binding names 127.0.0.1.
Those checks do not prove a container's workload is cheap. They establish the configured containment of this small fixture. The application handles synthetic requests and the suite runs sequentially; it does not intentionally create load or measure a capacity limit.
The loopback binding is especially important because Docker's default publication can expose a port on all host interfaces. The recipe does not rely on a reader remembering to add the host address manually. It both supplies the address and verifies it in the created configuration. Docker port-publishing behavior.
The recipe also keeps the application data and published output mounted read-only. This makes their role clear: they are inputs, not state that the running service is expected to modify. A future variant requiring writable storage would need a named owner and cleanup or preservation rule for that output.
No certificate is installed into the system trust store. The client creates an SSL context that trusts only the generated local certificate for this test. The certificate includes localhost and the loopback address, and it expires after one day. This lets the HTTP checks exercise real TLS without disabling certificate validation globally or treating every server certificate as acceptable.
The private key exists only in the owned temporary directory and the proxy mount. It is not a user credential and is not exported as an article asset. The report contains versions and outcomes, not secret material.
Readiness is not the same as acceptance
02 / Watch the mechanism
Preparation, observation and cleanup are different steps
Static view. Play to see how the mechanism unfolds.
The recipe checks prerequisites before provisioning, waits for a reachable upstream, verifies scenario behavior and then removes its own resources. An expected HTTP failure cannot be replaced by a service that never started.
Interpretation and evidence
- Readiness is not acceptance.
- Failure still triggers cleanup.
- Preserve the stage where execution stopped.
The recipe starts the proxy and then the application configuration. It polls a bounded readiness path until the application becomes reachable through the proxy. A connection refusal or transient 502 during startup does not yet represent the intended behavior of the scenario.
The readiness loop has a finite deadline and per-request timeout. If the deadline expires, the recipe captures the owned containers' logs before cleanup and fails. It does not wait indefinitely or declare success because the containers are listed as running.
Once the path responds beyond the proxy's unavailable-upstream state, the scenario's own assertion runs. In the no-forwarding and wrong-trust modes, 307 is expected. In the no-pathbase mode, 404 is expected. In the fixed mode, 200 is expected together with exact scheme, path-base, path and runtime fields.
This separation prevents a common testing mistake. A test that expects a failure can accidentally pass because the service never started. Here an unreachable application does not count as a successful redirect test, and a missing proxy does not count as an application denial.
The file-dependent endpoint is checked separately. The missing-file configuration deliberately reaches the correct route but returns a controlled 500 because its required artifact is absent. The fixed configuration returns the expected artifact. That makes the failure diagnostic rather than a vague assertion that something somewhere went wrong.
Acceptance is therefore a relation between a specific input configuration and an observed application result. The same status code can mean different things at readiness and at acceptance, so the report retains both context and response evidence.
Preserve the failure while still finishing cleanup
Cleanup runs in a finally block. It removes owned containers in reverse order, checks their ownership labels before removal, then removes the owned network and temporary directory. A successful removal is recorded in the proof object.
If cleanup itself fails, the script reports that separately. It must not return a clean success while leaving an unknown resource behind. Conversely, a successful cleanup does not erase the original test failure. The command-line entry point writes a failed result with the captured evidence and returns a failing process outcome.
The partial-provisioning test injects an exception immediately after network creation. The test then verifies that cleanup has no errors before continuing to the full recipe. This is a real exercise of an interrupted path, not an assertion that the same code would probably work because the happy path removed its resources.
The fixture does not claim recovery after forcible termination of the entire Python process or host power loss. A finally block cannot run when the process is no longer executing. Durable ownership recovery after such a failure would require persisting resource IDs before every mutation and providing a separate recovery command. That is a useful future extension, but it is not silently counted as part of this accepted test.
The temporary directory is created by the recipe itself with a unique name. Cleanup resolves only that owned directory; it does not accept an arbitrary user directory as a deletion target. The Docker IDs likewise come from successful creation calls in the same invocation. This makes the cleanup authority smaller and easier to inspect.
An occupied port is different: the process using it is not owned by the recipe. The correct cleanup action there is no action against that process. Refusing to start is a completed preflight outcome, not a reason to expand authority until the desired port becomes free.
Store evidence in a form another person can check
04 / See the idea
The receipt identifies what actually ran
A verification date is useful when attached to source hashes, published artifacts, image identities and observed outcomes. Editing prose later does not mean these runtime checks were repeated.
Interpretation and evidence
- Separate preparation from execution date.
- Keep hashes with the result.
- Do not infer a rerun from a fresh page date.
The accepted report is JSON with separate sections for prerequisites, input hashes, container identities, scenario responses and cleanup. It records the Docker engine version, pinned images, application runtime and Nginx version alongside the test outcome.
For every application configuration, the report preserves the status and selected response body. Redirect cases retain the destination. The fixed case retains the direct-spoof result and the proxy-overwrite result. These details allow another reviewer to assess whether the assertions correspond to the intended contract.
The same report also retains the expected failures at the recipe level. Missing environment and occupied port have clear preflight errors. The injected network-stage failure contains the cleanup record. The full suite contains five HTTP configurations. These are different kinds of evidence, so a single aggregate “four tests passed” would be too thin on its own.
The report does not record performance. A functional run taking a certain wall-clock duration is not a service-latency measurement. Startup polling, image inspection, certificate creation and cleanup are mixed into that elapsed time. Turning it into an application response-time claim would move the stopwatch boundary without telling the reader.
The source and artifact hashes are particularly useful for future maintenance. If an article changes only explanatory prose, that does not imply the deployment was rerun. If the runtime image or recipe code changes, the earlier receipt still describes the earlier inputs. The current page can explain that relationship rather than replacing an old date with a new one and losing the distinction.
The executed negative and positive cases
05 / See the idea
Negative cases and cleanup were executed
The accepted suite contains three expected recipe failures and the complete successful HTTP fixture. Both the injected partial failure and the successful run recorded no cleanup errors.
Interpretation and evidence
- Missing environment fails before provisioning.
- Partial creation is cleaned, not forgotten.
- Five HTTP configurations belong to the full run.
The final accepted suite returned a passing status with four recipe-level cases, five HTTP configurations and an empty cleanup-error list. The Nginx process reported 1.28.0, and the application reported runtime 10.0.15.
| Recipe case | Required observation | Observed result |
|---|---|---|
| Missing environment | Refuse before provisioning | Expected scope error |
| Occupied loopback port | Refuse without closing its owner | Expected bind failure; test listener retained |
| Failure after network creation | Remove the owned partial resource | Expected injected failure; cleanup completed |
| Full fixture | Observe all application cases and close resources | Five configurations passed; cleanup completed |
The full fixture includes deliberately broken and corrected behavior. Missing or untrusted scheme reconstruction gives a repeated self-redirect. Missing path-base handling gives 404. A missing artifact gives 500 after routing succeeds. The corrected configuration gives 200, and spoofed forwarding input does not bypass the trusted-peer boundary.
Those observations are stronger than a screenshot of a running container, but narrower than a production release acceptance. The example does not test internet routing, production DNS, certificate renewal, multi-node failover, identity-provider integration or cloud deployment permissions.
That limit is a feature of a useful recipe. It tells the next engineer what can be reused and what must be added when the environment changes. A page that claims universal readiness would hide exactly the dependencies that later make reproduction difficult.
Keep a negative test independent of its own convenience
03 / A concrete example
A busy port is not permission to kill its owner
The negative test holds a loopback listener, asks the recipe to use that port and expects preflight refusal. The test listener remains open and no provisioning begins.
Interpretation and evidence
- Construct the conflict deliberately.
- Refuse before creating resources.
- Do not terminate an unrelated process.
The occupied-port case begins by opening a listener owned by the test harness. The recipe is then asked to use that exact port. Its expected behavior is refusal before provisioning, while the harness retains ownership of the listener. This tests the recipe's restraint as well as its ability to notice a conflict.
The fixture does not search for an arbitrary busy port on the user's machine and infer that the process there can be interrupted. That would make the test dependent on unrelated work and expand its authority unnecessarily. A controlled negative condition is both easier to reproduce and safer to interpret.
The partial-resource case follows the same principle. The injected failure occurs after a known network creation point, so the expected cleanup is concrete. It is not enough to see a nonzero exit code: the report must show that the created network was removed and that cleanup did not report another error.
These tests also avoid treating every setup problem as a successful negative result. If Docker itself is unavailable, the partial-resource test has not reached its injection point. If the published DLL is missing, the HTTP scenarios have not exercised their behavior. The expected-error fragment and retained stage evidence distinguish the intended failure from an unrelated one.
Reproduction should not depend on yesterday's shell
The runner passes application settings to the specific container invocation. It does not rewrite the user's environment, change SSH configuration or rely on a secret value left in the shell. The script's environment argument selects only the declared local fixture scope.
The published artifact remains an explicit prerequisite. Another engineer can rebuild it with the local SDK selector, inspect the manifest and then run the recipe from a different working directory because file references are resolved relative to the recipe file. The output report path is separately supplied, so the caller can choose where accepted evidence is retained without changing the owned temporary-resource directory.
The fixture still depends on the host's Docker installation, filesystem mounts and Python runtime. Recording those dependencies honestly is better than calling the recipe hermetic. The goal is to remove accidental hidden state that the procedure can control, while naming the remaining environment assumptions and checking the ones that affect its result.
Give the verification date a precise meaning
A date attached to a recipe should answer a question. It might mean that the source was reviewed, dependencies were restored, a build succeeded or the complete functional run passed. Those are different events and should not share one ambiguous label.
For this fixture, the execution receipt's timestamp refers to the actual run. The source hashes identify what that run used. The article's preparation date describes the writing. A later correction to prose should not imply that Docker, Nginx and the application were all executed again.
When a dependency changes, first identify the boundary it could invalidate. A changed image can affect startup, certificates or networking. A middleware change can affect forwarded-header trust. A change to the script's cleanup logic requires evidence about ownership and partial failure. Repeating unrelated checks adds noise without resolving the changed risk.
This also provides a stopping rule. Once the declared prerequisites, behavioral assertions and resource cleanup pass on the accepted inputs, preserve the receipt and deliver the artifact. Do not keep rerunning the same environment merely to produce a fresher-looking timestamp. Repetition is justified by changed inputs, a failure or a specific unresolved question.
Use the local recipe as a model for a later cloud recipe
The principles transfer to Azure, but the evidence does not transfer automatically. A cloud recipe would need a different resource manifest, identity boundary, permission preflight, deployment-completion observation and cleanup or retention policy. The local container network is not a substitute for those checks.
The safe reuse is structural: declare the task, identify prerequisites, separate preparation from execution, record owned resource IDs, verify a user-relevant result and preserve cleanup evidence. The service-specific implementation must then be checked against the actual platform.
The included PowerShell entry point invokes the Python recipe without changing execution policy or environment variables. The caller supplies the Python executable and explicit environment scope. The Python standard library performs HTTP, TLS and process orchestration; Docker supplies the existing local container runtime. No additional persistent service is installed by the recipe.
The final artifact is therefore more than a command list but less than an infrastructure platform. It is one bounded, reviewable procedure with a result another person can reproduce or challenge. That is enough to make a verification date useful: the date points to observed work, the observed work points to exact inputs, and the cleanup record shows where the recipe's responsibility ended.
Source code and execution evidence
Download the complete lab, source record and raw execution results. This package supports the specific execution claims in this article. It is not a production certification.