Docker packaged the app, not the lifecycle
R7K1 could start a container from a repeatable image and still lacked scheduling, routing, readiness, isolation, expiry, and evidence.
Docker had already improved X8B6's test environment. A Dockerfile captured runtime and system dependencies, and a named image made one failure reproducible.
R7K1 asked the next question: could the same image model make a preview environment appear for every branch?
The first prototype built an application image and ran a container. That was the moment the demo declared success.
The container had no reviewer URL, no isolated data, no application readiness, no expiry, no resource budget, and no durable receipt connecting it to the branch request. Docker had packaged the process. R7K1 still had to build the environment lifecycle around it.
An image answered one valuable question
The application image declared the runtime filesystem, installed dependencies, startup command, and exposed port. Building it in a clean environment found host assumptions. Running it gave R7K1 a consistent unit for the application process.
That was a substantial improvement over provisioning each preview host through an accumulating setup script.
The image did not answer:
which host should run this instance?
which network and data may it reach?
when is the application ready for review?
which hostname maps to it?
how many resources may it consume?
when should it stop and disappear?
what proves the running instance matches the candidate?Those were not shortcomings in Docker's promise. They were responsibilities the product had placed around a container.
The architecture improved when I stopped treating packaging as lifecycle completion.
Placement was a scheduling decision
R7K1 initially ran every preview on one host. The next available port and container name were chosen by a shell script.
As previews accumulated, placement needed constraints: CPU and memory capacity, required runtime features, data locality, isolation class, and maintenance state of the host. “First host in the list” could overload one machine while another sat idle.
I built a small scheduler appropriate to the personal project. It filtered eligible hosts, measured declared allocations, chose a fit, and recorded the decision in the environment receipt.
environment env-271
host preview-02
CPU share and memory limit
placement policy versionThe scheduler did not promise perfect utilization or automatic failover. It made placement explicit and rejected provisioning when safe capacity was unavailable.
Starting a container was an effect after placement, not the algorithm that decided placement.
A broken preview could consume memory until the host killed unrelated environments. The first prototype relied on process behavior and host generosity.
R7K1 assigned bounded CPU, memory, process, and storage expectations to each environment. The exact controls depended on the Docker and host capabilities available. The environment page showed requested and observed use.
Limits were not a security wall or performance guarantee. They reduced the ability of one unaccepted branch to exhaust a shared preview host and made capacity planning possible.
A container killed for exceeding memory did not restart forever without explanation. Lifecycle state recorded the reason and bounded automatic attempts. Reviewers saw that the candidate exceeded its preview budget.
Build jobs and runtime containers had different limits. An expensive build should not compete indefinitely with ready reviews.
Resource policy turned “we can run another container” into “we can support another environment without degrading the rest.”
A published port was not a reviewer route
Docker could publish a container port on the host. Sending reviewers preview-02:32781 exposed host placement and made rescheduling break links.
R7K1 placed a routing layer in front. The branch route mapped to one verified environment generation and resolved its host and port. Environment identity remained stable if the internal port changed during reprovisioning.
Route activation happened after readiness. Detachment happened before removal. The routing record carried expected predecessor to prevent a late old build from replacing a new one.
The application received the external hostname through controlled configuration so it generated correct links without trusting arbitrary forwarded headers. The proxy set and sanitized those headers at the boundary.
Port mapping connected processes. Routing connected reviewer intent to a named environment under lifecycle control.
Process running was not readiness
The container runtime could report that the application process existed. R7K1's first check opened the TCP port and marked the preview ready.
One candidate accepted connections while still running migrations. Another returned a generic 200 maintenance page. A third served the wrong build because a volume mounted stale source over the image.
Readiness checked an application endpoint that returned artifact identity, fixture schema, and capability status. A smoke task then requested the preview under its real hostname and followed a representative route.
process: running
application: initialized
schema: expected fixture version
artifact: matches environment receipt
route: serves representative pageOnly the complete evidence allowed branch-route activation. Liveness after activation remained narrower: can the instance continue serving or should R7K1 replace it?
Docker could execute a health command. R7K1 still had to define what health meant for this product.
Restart policy could hide a deterministic failure
The first runtime policy restarted any exited container. A branch with invalid configuration entered a rapid loop, consumed logs and CPU, and looked transiently “starting” forever.
R7K1 classified exits. A host interruption or one early crash might justify a bounded restart. Repeated exit under the same artifact and configuration produced a failed environment with evidence.
The attempt counter belonged to environment generation, not container name. Recreating the container did not reset history.
Backoff protected the host. A manual retry required unchanged or changed input to be explicit. If input changed, it became a new generation rather than erasing the failure.
Restart is an availability mechanism for recoverable faults. It is not a substitute for surfacing deterministic application errors.
The container runtime supplied the action. Lifecycle policy supplied the stop condition.
Volumes introduced state outside the image
R7K1 mounted configuration, temporary data, and sometimes source during development. Each mount weakened the claim that the image fully described what ran.
Candidate environments used immutable application images without source mounts. Runtime configuration came from a declared profile. Preview databases and uploads lived in environment-scoped storage with explicit recreation and expiry rules.
Mount inventory entered the receipt:
fixture volume: recreatable from fixture catalog v4
temporary output: disposable
review upload volume: preserved until environment expiry
host socket mounts: forbiddenThe last rule mattered. Mounting a container-management socket into unaccepted application code would grant control far beyond its preview. Convenience could erase isolation.
An image is only one filesystem layer. Every mount must be part of the runtime trust model.
Each preview received a separate database or schema identity, credentials, and fixture. Application configuration scoped it to that data.
R7K1 avoided copying production data. Synthetic fixtures represented meaningful states without carrying real identities or secrets. Database users lacked access to other preview schemas.
Network rules constrained which services the application could reach. A container on a shared bridge was not assumed isolated merely because it had a unique name. Management endpoints and host metadata remained inaccessible.
Cleanup revoked credentials and removed the scoped data before tombstoning the environment. A leaked container without valid credentials had reduced reach.
Docker supplied network primitives. R7K1 combined them with database policy, credential scope, and environment identity.
The preview was unaccepted code and deserved a default posture of limited trust.
The first image build copied a development environment file into the image. Anyone able to pull the artifact could inspect it, and every environment shared credentials.
R7K1 removed secrets from build context and image layers. Runtime provisioning issued environment-scoped credentials or mounted narrowly readable values. The receipt recorded secret identity and policy version, never value.
Logs redacted known sensitive fields and scripts avoided printing entire environments. Debug access required authorization and produced an audit record.
A preview rarely needed production integrations. Where a feature required an external service, it used a sandbox account and scoped token. Missing access was preferable to silently borrowing the host's identity.
Container packaging made artifact distribution easy. That increased the importance of keeping reusable secrets outside the artifact.
Image provenance needed a receipt
Running docker ps showed an image tag, often mutable. R7K1 verified the immutable artifact identity and the build manifest attached to it.
Provisioning recorded:
image registry and digest
source and build manifest identity
expected startup command
runtime configuration profile
host and container provider IDThe application readiness endpoint returned the embedded public build identity. A mismatch failed activation, catching stale tags and accidental mounts.
The runtime did not rebuild or mutate candidate images. If an emergency package change was required, R7K1 produced a new artifact and environment generation.
This made a running container evidence-backed. “Same branch tag” stopped being proof that two instances ran the same bytes.
Container output was easy to capture and hard to interpret when names were reused. R7K1 tagged log streams with environment, generation, artifact, host, and container instance identity.
Build logs remained separate from application logs. Routing and lifecycle transitions had their own records. A reviewer opening the environment page could see the relevant stream without merging unrelated candidates.
Log collection survived container removal for a bounded diagnostic window. Sensitive fixture values and credentials were not dumped indiscriminately.
The last lines before an out-of-memory kill, readiness failure, or expiry were attached to the tombstone. Deleting a container did not delete the evidence needed to explain it.
Docker made standard output the natural application stream. R7K1 supplied ownership and retention.
Host failure exposed the absence of durable control state
If all environment state lived in container labels on one host, losing that host would erase R7K1's record of what should exist and which route pointed there.
The lifecycle database stored desired environment, artifact, route generation, fixture, expiry, and receipts outside preview hosts. Host agents reported observed containers.
When a host disappeared, R7K1 marked affected environments unavailable and detached or failed over routes according to policy. It could recreate environments whose data was declared reproducible. It did not pretend to recover review-entered local state that lacked a backup.
The personal system did not attempt seamless multi-host high availability. It made failure consequence explicit and prevented a missing host from leaving routes pointing into silence indefinitely.
Containers were disposable only when the state required to recreate them was durable elsewhere.
Expiry was not docker rm alone
Removing the application container left database, volume, route, credentials, DNS or certificate state, logs, and build references.
R7K1's expiry lifecycle detached the route, stopped the instance, removed scoped data and credentials, verified resources absent, then retained a compact tombstone.
Every resource carried environment ownership labels or tags where the provider supported them. A leak scanner compared external resources with active and removing lifecycle records.
Already-absent was a safe removal result only after the system verified it had addressed the intended resource identity. A same-name resource owned by another generation caused review.
The container was one item in the environment's resource graph. Lifecycle completeness required the graph to reach its removal state.
A verified image could later be understood to contain a vulnerable operating-system package or dependency. Artifact immutability meant its contents did not change; risk knowledge did.
R7K1 recorded base image and dependency inputs so affected previews could be found. Short-lived environments limited exposure but did not remove the need to rebuild or revoke a dangerous artifact.
Unaccepted code ran with constrained network and secrets, reducing consequence. The registry could mark artifacts disallowed for new provisioning while existing environments entered removal or explicit exception.
I did not claim a perfect 2016 scanning pipeline. The model made later analysis possible by retaining provenance and ownership.
Immutability helps identify what runs. It does not make that content safe forever.
Docker upgrades became infrastructure releases
Changing the container engine or host configuration could affect networking, storage, limits, and lifecycle commands for every environment.
R7K1 drained one preview host, ran fixture environments through build, readiness, route, restart, and expiry scenarios, then returned it to eligibility. It did not upgrade every host under active reviews simultaneously.
The scheduler understood maintenance state. Existing environments could finish or be recreated elsewhere according to data policy. Host agent versions appeared in provisioning receipts.
This applied the same candidate discipline to infrastructure that M31V used for content. A new engine version was not adopted because installation succeeded; its product behaviors were verified.
Containers reduced application-environment drift and created a shared engine whose lifecycle required its own care.
The platform stayed deliberately small
R7K1 could have adopted a broader orchestration system. In 2016, the personal project's scale did not justify every abstraction that path would bring.
A small scheduler, host agent, routing layer, lifecycle database, and Docker runtime were enough to learn the responsibilities. The implementation was not a claim that those pieces should always be handwritten.
The value was understanding what any chosen platform would need to supply or expose:
placement and capacity
runtime identity
network and secret boundaries
readiness and routing
restart and stop conditions
data custody
expiry and leak detection
receipts and observabilityLater tools could replace adapters without changing R7K1's product model. Container orchestration was an implementation choice beneath the environment lifecycle.
The first scheduler always chose a host and trusted the operating system to cope. When several branches built at once, memory pressure made ready previews slow and caused unrelated containers to restart.
R7K1 added an admission decision before provisioning. It compared requested runtime limits, current allocations, host reserve, build workload, and maintenance state. If no host fit, the environment remained queued with a visible reason instead of starting partially.
The scheduler did not treat declared limits as perfect predictions. It recorded observed peaks and used them to improve default profiles. A branch that repeatedly exceeded its class could request a larger profile or fail with an actionable budget explanation.
Queues had age and priority policy. An old abandoned branch could not occupy scarce capacity forever while a current review waited. Expiry applied to queued requests as well as running environments.
The environment page distinguished “waiting for safe capacity” from build or application failure. Reviewers could see that the candidate had not yet started, and operators could add capacity or remove stale work deliberately.
Admission control made scarcity honest. Starting every container immediately created the appearance of throughput by moving the wait into contention and failure.
This was another responsibility outside the image. A package can declare expected resources; the platform must decide whether the shared world can grant them without breaking existing promises.
Packaging was the beginning
Docker solved a difficult, useful problem for R7K1. It turned the application runtime into a buildable, distributable image and gave the system a standard way to create and stop process instances.
The preview product began around that primitive. Scheduling chose a safe host. Scoped data and secrets limited reach. Readiness proved the application and artifact. Routing connected reviewer identity. Receipts explained what ran. Expiry removed every owned resource.
Calling the container the environment would have hidden all of that work in scripts and operator memory.
The 2014 lesson was that containers changed what I could reproduce. The 2016 lesson was the next correction: reproducible packaging does not make a lifecycle. It gives the lifecycle a better unit to manage.
That distinction kept the project from blaming Docker for responsibilities it had never claimed and from praising it for policies R7K1 still needed to invent. The image boundary was real progress. The environment boundary was a larger product made from placement, state, trust, time, and evidence.
Docker packaged the app. R7K1 became responsible for everything required to make that package a safe, useful, temporary place. The success of the system was not how quickly it could run docker start; it was whether a reviewer could trust the route, a host could survive the workload, and expiry could safely leave no unexplained resource behind after the final container was gone for everyone involved.