git push finishes in about a second. What it sets off takes much longer, and nearly every step can fail separately.
When your code is connected to a deployment platform, one commit starts a chain of systems that each hand work to the next. I built that chain for Spluur, so I'll walk through it the way I built it, and mark where each step breaks.
GitHub → API → Queue → Build → Container → Traefik → Live1. GitHub tells the platform
Pushing a commit doesn't contact the deployment platform directly. GitHub does, by sending a webhook: an HTTP request to a URL the platform registered, containing the repository, branch and commit.
Two things matter here.
First, the platform must check the request is really from GitHub. Otherwise anyone who finds the URL can trigger deployments. GitHub signs each webhook with a shared secret, and the platform recomputes that signature and compares:
import { createHmac, timingSafeEqual } from "node:crypto";
export function isFromGitHub(rawBody: Buffer, header: string, secret: string) {
const expected =
"sha256=" + createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(header);
return a.length === b.length && timingSafeEqual(a, b);
}You need the raw body for this, not the parsed JSON, because re-serialising it can change the bytes and break the signature.
Second, the handler must answer quickly and do almost nothing. Its only job is to say "received" and hand the work to something else.
2. The work goes into a queue
A build can take minutes. A web request can't wait that long, and a crash in the middle shouldn't lose the deployment.
So the handler puts a job on a queue (Spluur uses BullMQ, which stores jobs in Redis) and returns. A separate worker picks the job up later. This gives you things a request handler can't:
- Retries. If a worker dies mid-build, the job isn't gone.
- Limits. You decide how many builds run at once, so one busy morning doesn't flatten the server.
- Visibility. A job has a state. You can see what is waiting, running and failed.
3. The code becomes something runnable
The worker clones the repository and has to turn source code into something that runs.
Many projects don't have a Dockerfile. Spluur uses Nixpacks, which looks at the project, works out the language and the build steps, and produces a container image without anyone writing a Dockerfile.
Most failed deployments fail here. A missing lockfile. A Node version the project didn't pin. A build that runs out of memory. A build command that only works on the developer's laptop. This is also why build logs matter: when something breaks here, the log is the only evidence.
4. A container runs it
The image starts as a Docker container. This is where isolation happens. The app gets its own filesystem, its own environment variables, and its own limits on memory and CPU, so one project can't eat the whole machine.
A running container still isn't reachable from the internet. It lives on an internal Docker network.
5. Traefik routes the traffic
Traefik is a reverse proxy. All traffic arrives at the server on ports 80 and 443, and Traefik decides who gets it by reading the Host header on each request:
my-app.spluur.app → container A
api.theircompany.com → container BIt also handles HTTPS. It requests certificates from Let's Encrypt and renews them, so each app is served over TLS without anyone touching a certificate.
6. DNS points the world at the server
None of the above matters if the domain doesn't lead to the server. DNS maps a name to an address. For a custom domain, the user has to add a record pointing at the platform, and until that has propagated, the certificate can't be issued either.
DNS is also the step nobody can speed up. You change a record and then you wait.
7. And it's live
The full path of a request, once everything has worked:
Browser
↓
DNS
↓
Server
↓
Traefik (picks the container by hostname, handles TLS)
↓
Container
↓
Your appThe URL is the part everyone sees. The seven steps underneath are the part that decides whether it works.
Why I wanted to build it
I could have used a platform that already does all this, and for many projects I would. But I wanted to see each box on the diagram and know what it does when it breaks.
Once you've seen the path, deployment stops feeling like magic. It is a handful of ordinary systems passing work to each other, and each one can be understood.