A deploy is a change to a running system, not a copy of files onto a disk. The bytes arriving is the easy half. The hard half is everything already in motion, all on its own schedule: names cached in resolvers you do not control, responses stored around the world, certificates with expiry dates, requests mid-flight, and a database that will not reverse when your code does. What follows is that second half — what changes the moment you release, what changes minutes later, and what never changes back.
Each chapter opens with the short version. Tap one to read the detail.
Your domain is a delegation, and changes expire rather than travel
~2 min
"Propagation" sounds like an update crossing the internet. Nothing crosses anything: resolvers reuse the answer they hold until the lifetime you published before the change expires — so the old setting governs the switch.
Renting a name and answering questions about it are two different jobs, often at different companies. The registry publishes NS records naming which servers are authoritative for your zone; those servers hold everything else. Edit records at a provider that is not the delegated one and nothing happens: no error, no effect. Two documented shapes make it concrete — a full setup moves the zone's nameservers to the new provider; a partial setup leaves them and points one hostname in by CNAME.
The records have their own rules. A maps a name to an IPv4 address, AAAA to IPv6. A CNAME points at another name, and a name holding one may hold no other record type — which is why the apex normally cannot be a CNAME, and why providers offer flattening that resolves the target and answers with a plain address. Mail lives in separate MX records, so repointing a site must not sweep them away, and a stale CAA record naming the wrong certificate authority silently blocks renewal.
Then the clock. Time to live (TTL) is how many seconds a resolver may reuse an answer — 300 by default on proxied records, and the documentation warns a local cache may hold on longer still. Cut over with a one-day value in place and the change saves instantly while strangers reach the old server all day.
Rule: lower the lifetime before the cutover and raise it after — the value already published decides how long the old answer survives.
The padlock describes one leg of the trip
~2 min
A certificate is a signed claim that a key belongs to a hostname, and the padlock reports only on the browser's own connection. Behind a proxy there are two legs — the second can be plaintext while the first shows a lock.
Issuance is not where certificates fail. An authority signs only after you prove control of the name, and how that proof happens decides whether renewal is silent forever or a chore someone forgets. Where the provider also runs your authoritative nameservers, validation completes on its own; leave the nameservers elsewhere and someone has to complete it by hand. Every certificate carries a validity period and must be reissued, so the outage arrives from a broken renewal, never from the original signing.
The bigger misconception is that encryption is one thing you either have or lack. Behind a proxy it is two independent connections, and the documented modes differ exactly there. Off is cleartext throughout. Flexible encrypts the visitor's connection and calls your server over plain HTTP — a padlock in the address bar with the second leg in the clear. Full encrypts to your server but does not validate its certificate, so a self-signed or expired one is accepted happily. Only Full (strict) validates it, and a stricter mode always connects over HTTPS with validation however the visitor arrived.
So "we use HTTPS" is not a finding but a question about which leg — and a server still reachable at its raw address bypasses the proxy altogether, along with everything the proxy was doing for you.
Rule: state each leg separately — browser to edge, edge to origin — and treat a mode that skips validation as unverified.
A shared cache is one copy per location, and the key decides who shares it
~2 min
Caching reads like a switch you flip for the site. It is one copy per location, admitted only when the response permits storing and shared only between requests whose cache key matches exactly — so one extra header there can halve your hit rate.
A miss fetches from your server and fills that one location, so only the second request there is fast. What gets stored by default is narrower than people assume: one large content delivery network (CDN) caches by file extension — stylesheets, scripts, images, fonts — and does not cache HTML or JSON at all. Headers decide the rest. Nothing is stored when the response says private, no-store, no-cache, max-age=0, or carries a Set-Cookie; it is stored when Cache-Control says public with a max-age above zero, and where both a max-age and an Expires appear, the former wins.
The key is the part people skip: by default the full URL including query string, plus the Origin header and a few forwarded ones. Add a cookie, a device class or a language and one URL becomes many stored objects — the documentation calls this sharding and warns it reduces the hit rate. Concretely: leave a per-visitor tracking parameter in the key and every visitor mints their own entry, so nobody shares a hit and your cache is an expensive miss counter.
Purging inherits the problem, because purging by URL means reproducing that exact key. Content-hashed filenames sidestep it: a changed file is a different URL, so nothing needs invalidating at all.
Rule: design the cache key before the cache, and let a release change URLs rather than depend on a flush.
What sits between the reader and your server
~2 min
Before any of your code runs, a client resolves a name, connects, completes a handshake and sends a request — four steps that fail in four unrelated ways. In front of them sits a proxy deciding which of your machines, if any, hears about it.
The machine that answers the client and the machine that owns the content are not the same. A proxied hostname resolves to the platform's addresses; a name marked as plain domain name system (DNS) hands your server's own address straight to the client — and that one flag decides whether anything sits in front of you at all.
Routing happens at two documented levels: which pool takes the request, and which endpoint inside it — with policies that steer by geography or measured proximity rather than a fixed order. Underneath both sits health, and health is measured, not declared. A monitor asks two questions — did the endpoint answer inside the timeout, and did it return the expected status code and body text — because answering is not the same as working. Probes come from three data centers per selected region and a majority rules — most data centers agreeing makes the region healthy, most regions agreeing makes the endpoint healthy. Requiring several consecutive results before a state flips stops a blip evicting a working server, and a pool below its health threshold stops receiving traffic unless it is the fallback.
The deploy consequence is timing: removal is learned, not announced, so a shutting-down instance still gets requests for a moment. Container runtimes make that window explicit — SIGTERM first, then SIGKILL after a grace period defaulting to 10 seconds on Linux.
Rule: drain before you stop — fail the readiness check first, keep serving what is in flight, then exit.
Where the work happens, and what the meter counts
~2 min
The choice is not static versus dynamic but when each page is computed — at build, in the background, or during the request. That sets your failure surface and your bill together: a page nobody computes costs nothing and cannot crash.
A prerendered page is an object with no runtime to scale, patch or exploit; a per-request page needs a live function whose cost and failure surface grow with traffic. The middle option: serve the cached copy immediately and regenerate in the background, on a timer or an explicit call, so visitors keep the old version while the new one builds. If regeneration fails — a network error, an unexpected status, a runtime error — the platform keeps serving the existing content and retries shortly after. Note the scope: each deployment gets its own cache and does not reuse the previous one, while earlier deployments' content is retained, so a rollback still has something to serve.
The runtime has its own shape, and one mistake dominates: reading a limit stated in seconds as a cap on elapsed time. An instance is created per concurrent request, so a cold one pays for whatever your module-level code does on import. Limits are stated in processor time, not elapsed time — waiting on a network call or a database query does not count — so a cap reading like 30 seconds can host a far longer request while the client stays connected. Work started but never awaited dies when the response goes out, unless you use the explicit hook.
The bill follows the same line: requests are billed, compute only when the code actually runs, and on that platform bandwidth out is not billed at all.
Rule: decide when a page is computed before deciding where it runs — that answer sets the invoice and the blast radius at once.
Compute is disposable; state has to live somewhere that isn't
~2 min
Every deploy replaces the instances, so anything written to local disk is gone — and was never visible to the others anyway. Moving it to a globally replicated store fixes durability and hands you a read allowed to be a minute stale.
The first move is easy: local disk is scratch space, and user files belong in an object store. The second holds the surprise. A globally distributed key-value store is eventually consistent on purpose, and its own documentation is blunt about the size of "eventually": a write is visible immediately at the location that made it, and can take up to 60 seconds or more to appear elsewhere as cached copies expire — longer, not shorter, in locations that recently read the previous value.
Picture a user saving a setting, reloading, and being served from a different location. They see the old value, nothing errored, and support cannot reproduce it because their own location has the new one. That is the store working as designed, which tells you what it is for: read-heavy, highly cacheable data such as configuration and lookup tables. It is the wrong home for a counter, a lock, a rate limit or a seat reservation, each needing exactly one authoritative location rather than a copy per region.
Distance is the other half: code close to users but far from its single-region database is slower than a plain server beside that database, because the trip is paid once per query rather than once in total.
Rule: choose the store by what a stale read costs — configuration can be a minute behind, a balance cannot be a second behind.
The artifact: layers you cannot un-write, and a name that can move
~2 min
An image is a stack of immutable layers, so deleting a file later leaves the earlier layer untouched — and build arguments are recorded in the image history. A credential that ever entered the build stays extractable from the finished image.
The mistake is picturing an image as its final filesystem: layers are immutable once created, so a later instruction stacks another change rather than editing what came before. Only some instructions create layers: RUN, COPY and ADD do, while ENV, ARG, CMD, ENTRYPOINT and USER set metadata. ARG is build-time only and is not embedded in the final image; ENV persists into the running container and overrides an ARG of the same name. Build arguments, the documentation warns, appear in the image history, so credentials belong in a secret mount instead.
The build cache has one rule with large consequences: once a layer is invalidated, every layer after it rebuilds whether or not its own inputs changed. So order instructions from least to most frequently changing: the canonical case is copying the dependency manifest and installing before copying the source — otherwise editing one line of code reinstalls every package, every time. An unignored node_modules or .git inflates the context and busts the cache on every build, which is what .dockerignore is for.
Multi-stage builds close the loop: a second FROM ... AS starts a fresh stack and COPY --from lifts only the finished artifact, so the compiler and the source never ship.
Names move too: a tag is mutable — the publisher can repoint it at any time — while a sha256: digest cannot. FROM node:latest is a moving target: the same file can build a different image tomorrow, and you will debug it as though your code changed.
Rule: build in one stage, ship another, and pin what you depend on by digest — a tag is a bookmark, not a version.
Anything read at build time is frozen into the artifact
~2 min
An environment variable feels like runtime configuration. A value read during the build is baked into the output instead, so the artifact cannot be re-pointed afterwards — and if it reached client code, it was not configured, it was published.
One artifact that runs everywhere requires everything differing between environments to be read at run time, from outside the artifact. Break that and staging never tested the bytes production runs.
The clearest documented case is a variable prefixed NEXT_PUBLIC_, which the framework inlines at build time into the JavaScript sent to browsers, replacing each reference with a hard-coded value. The docs state the consequence outright: after the build the application no longer responds to changes in those variables, so one image promoted through several environments carries whichever value the build saw. A dynamic lookup is not inlined — which is why reading the same variable through a computed name comes back empty in the browser and works fine on the server.
The publication half matters as much: an inlined value ships to every visitor, so the prefix is an explicit opt-in to publishing and a credential must never take it. Real secrets are write-only once set: one platform stops showing a secret's value in its tools after you define it, and warns against holding sensitive values in plaintext configuration.
One more timing trap: changing a variable does not touch existing deployments, only the next one. A credential you can rotate only by rebuilding will not be rotated during the incident that demands it.
Rule: sort every value into build-time, runtime, or public — and never let the third category hold anything you would mind reading in a browser.
Build once, promote the bytes
~2 min
"Works locally" is a statement about your machine, and rebuilding per environment quietly makes it one about your build server too. The pipeline's job is to produce a single artifact and move that same artifact forward.
A deployment is the result of a successful build, and each keeps its own permanent address. That turns promotion and rollback into an alias switch rather than a rebuild: a preview deployment reviewers already exercised is promoted as-is, and the production domain simply starts pointing at it.
The mistaken assumption is that the same commit yields the same artifact. Rebuilding per environment sounds equivalent and is not, for reasons the previous chapters give. A base image tag can be repointed under you, a lock file can resolve differently, and any value read during a build is captured from whatever environment ran it. Same commit, different artifact, and the difference surfaces in production.
Two traps sit either side. A preview environment wired to the production database is a production write path wearing a temporary URL: reviewers behave as though nothing counts, and everything counts. And a pipeline whose checks cannot actually fail the run is a deploy button with extra steps — the ritual of a gate with none of the function.
Rule: promote the artifact you tested, never a fresh build of the same commit — if you cannot promote, that is a bug in the pipeline.
Rollback is a plan, and half of it does not roll back
~2 min
The revert button covers your code and nothing else. Rolling back is fast precisely because it only re-points traffic at an artifact that still exists — while the migration you applied and the emails you sent stay where they are.
Separate the two ideas the word "deploy" welds together. A version is a snapshot of code, assets, bindings and settings, with its own identifier. A deployment decides which version or versions serve traffic, and it can name two at once with a percentage split between them. So during a gradual rollout both versions serve real users at once — the normal case to design for, not an edge case. Storage sits explicitly outside that snapshot, which is the whole problem in one sentence.
The mistake is treating rollback as an undo. It is a re-point: one documented flow aims production at a previous deployment without rebuilding, taking effect within seconds because it happens at the routing layer. Fast, reversible, boring — and it covers only what the snapshot covered.
Everything else is one-way: an applied migration, a drained queue, an email already sent. This is why schema changes ship in stages — add the new column, release code that writes both old and new, backfill, and only then drop the old one. Renaming a column in a single release breaks whichever version is still running, and during a percentage rollout a second version is still running, by design. The same logic covers a feature flag: ship the code dark and switch behavior on separately, so turning it off costs seconds and no deploy.
Rule: make each schema change survivable by both versions before releasing either one, because the traffic split guarantees both are live at once.
A deploy is a measurement, or it is a guess
~2 min
A green build tells you an artifact was produced, not that it works. The only signal that settles it comes from production — and must be attributable to a specific deployment, or you cannot tell a bad release from a bad Tuesday.
Three kinds of data answer different questions: logs are discrete events, metrics are aggregates over time, traces follow one request across hops. Platforms record request counts, error rates, processor time and wall time without instrumentation, and tracing captures outbound calls, storage operations and handler invocations automatically.
The mistake is reading a green pipeline as evidence about production. The release-specific ingredient is attribution, and one published incident flow shows the shape. Confirm the problem by querying production logs for server errors over the last thirty minutes. Roll back. Query the same filter again over a five-minute window to confirm recovery. Then investigate at your own pace: compare error logs between the last good deployment and the bad one by identifier, and where several releases went out together, binary-search to find which introduced the regression. Every step keys on a deployment identity, which exists only because each build kept its own.
That ordering is the lesson — restore service first, diagnose second, because the rollback buys the calm to read anything properly. It also sets a bar for logging: fields you can filter and group beat sentences you can only read, and a request identifier threaded through every hop makes one user's failure findable an hour later.
Rule: decide before the release which numbers you will watch, over what window, and what reading sends you back — a rollback that takes seconds needs a signal you can read in seconds.
Written by Keentune. We are not affiliated with or endorsed by the organizations whose documentation informs this guide, and any linked sources belong to their respective owners.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.