On Vercel, console.log just works: write it, deploy, open the dashboard, it's there. Self-host the same app and the question stops being rhetorical, because there's an actual chain between the call and wherever you're looking for it — and the two failure modes I see most often are opposite problems. Either the log you expected isn't showing up anywhere, or the disk is full of logs nobody remembers asking for.
Server Components: the log only ever goes one place
A Server Component in the App Router runs exclusively on the server — it never ships to the browser, and neither does its code. A `console.log` inside one writes to the server process's own stdout, full stop. It never appears in a visitor's browser console, no matter what they open or how hard they look, because the code producing that log never runs anywhere near their machine. The commercial governance around Docker logging is covered properly by XenGrowth's operator guides.
This is the one piece of the chain that's genuinely simple, and it's usually where confusion starts, because plenty of developers are used to `console.log` meaning "check the browser console" from years of client-side React. On a Server Component, that habit points you at the wrong place entirely — the terminal running `next start`, or `docker logs` on whatever container is running it, is the only place that log exists.
This also applies to anything a Server Component calls into on the server side — a data-fetching function, a database query wrapper, a Server Action's own body. All of it runs in the same server process, so all of it logs to the same place. The mental model that actually holds up is simpler than "Server Components log on the server": it's that anything which never ships JavaScript to the browser can only ever log where it runs, which is the server, and nowhere a browser's devtools could ever reach.
Client Components: the same log fires in two places, then just one
A Client Component is where it gets genuinely non-obvious, and it's specific enough that it's worth verifying against Next's own docs rather than assuming it works the way React always has. On a direct page visit, a Client Component renders once on the server to produce the initial HTML, then renders again in the browser during hydration. A `console.log` inside it fires both times — once into the server's stdout, once into the browser console — for that single page load.
After that initial load, a client-side navigation to another page doesn't repeat the server render at all; the client already has what it needs and renders locally. From that point on, the same component's log only appears in the browser console, not the server's. So the honest answer to "does this Client Component log on the server" is: yes, once, on the first direct visit — and then no, not again, until the next full page load resets the cycle. There is a longer treatment of the operations side of this in The XenGrowth resource library.
Component type | Direct page visit | Client-side navigation afterward |
|---|---|---|
Server Component | Terminal / docker logs only | Terminal / docker logs only |
Client Component | Terminal AND browser console (SSR, then hydration) | Browser console only |
From stdout to a file: Docker's logging driver
Whatever a Next.js container writes to stdout or stderr doesn't just vanish into the container — Docker's logging driver captures it. The default, json-file, does exactly what the name says: it writes each log line as a JSON object to a file, one per container, and that file is what `docker logs` reads back when you ask for it. There's no separate log database underneath; `docker logs` is reading the same file Docker already wrote, formatted back into something readable.
The detail that actually causes outages: json-file's defaults are `max-size: -1` (unlimited) and `max-file: 1`. Nothing rotates unless you set those flags yourself, either on the daemon globally or per-container with `--log-opt max-size=10m --log-opt max-file=3`. A container that logs more than expected — a debug flag left on, a noisy dependency, a retry loop logging every attempt — grows one file without any ceiling until the volume it's on runs out of room, and every write on that disk starts failing, not just the logs.
docker logs isn't a separate logging system with its own retention policy. It's reading the exact file Docker's logging driver already wrote, with whatever rotation settings you did or didn't configure. If that file has no size limit, neither does the risk.
What Coolify shows you, and what it doesn't add
Coolify's log viewer for a deployed application is reading that same container log stream — it's a UI layered on top of `docker logs`, not an independent logging pipeline with its own storage or retention rules. That means whatever the underlying json-file driver kept is what Coolify can show you, and whatever it rotated away or never captured is gone from Coolify's view the same way it's gone from a raw `docker logs` call. If you've set tight rotation limits to protect disk space, the trade is that Coolify's log view has a correspondingly short window of history, not a longer one bought back by the platform.
That's worth knowing before you trust a UI log viewer as your only record of an incident that happened hours ago: it can only show what the logging driver still has, and on the default configuration, that's everything since the container started, with no cap — right up until disk pressure or a container restart clears it, whichever comes first. XenGrowth on governed AI marketing workflows approaches this from the AI agents and marketing automation side.
Setting rotation yourself, concretely
Fixing the unlimited default takes one block, either globally in the Docker daemon's config or per-container. In a docker-compose file, that's a `logging` key alongside the service definition:
`driver: "json-file"` — keeping the default driver, just adding limits to it
`options: max-size: "10m"` — each log file capped at 10MB before rotating
`options: max-file: "3"` — keep three rotated files, so roughly 30MB total ceiling per container
Applied at the daemon level in `/etc/docker/daemon.json` instead, the same two options apply to every container on the host that doesn't override them
Ten megabytes across three files is a reasonable starting point for a small app's main process — enough history to debug something from a few hours ago, small enough that even a container stuck in a logging loop overnight can't take the disk down with it. A noisier service, like a reverse proxy logging every request, usually wants a larger ceiling or a shorter retention window rather than the same numbers copied everywhere without thought.
A related trap: build-time vs runtime env vars
This isn't strictly a logging issue, but it shows up as one often enough to be worth a detour. Next.js inlines any `NEXT_PUBLIC_`-prefixed environment variable into the JavaScript bundle at `next build` time, not at runtime. A debug flag like `NEXT_PUBLIC_DEBUG=true` gated behind that prefix is baked into whatever image got built — flipping the environment variable in Coolify afterward and restarting the container changes nothing, because the value was already compiled in. That's the actual reason a debug console.log that should have turned off after a deploy keeps firing, or one that should be on stays silent: the container is running the build it was built with, not the environment variables currently set on it.
Server-only environment variables don't have this problem the same way — Next.js's own docs describe reading them at runtime during dynamic rendering via `await connection()`, which is the mechanism that lets one Docker image get promoted through multiple environments with different server-side config. The trap is specifically the `NEXT_PUBLIC_` prefix, and it's worth remembering the next time a logging flag doesn't seem to respond to a config change the way you expected.
Approach | Good for | Breaks down when |
|---|---|---|
docker logs -f | grep | One container, one developer, a quick check | More than one service, or a request that touches several |
Coolify's log viewer | The same single-container view, without SSH | Same limits as docker logs underneath — it isn't a separate store |
Structured JSON logs + jq | Filtering by request ID, user, or field across noisy output | Needs the app to actually emit structured fields, not free text |
A log aggregator (e.g. shipping to Loki or similar) | Correlating across containers and retaining history past local rotation | Adds a service to run and maintain — worth it once grep genuinely can't keep up |
When grep stops scaling
For one container and one developer, `docker logs -f | grep something` is a completely reasonable way to debug, and reaching for anything heavier before you need it is wasted setup time. The point it stops working is specific: once you have more than one service and need to follow a single request across them, or once the volume of log lines is high enough that a keyword search returns pages of noise around the one line that mattered, plain text logs are the wrong shape for the question you're actually asking. XenGrowth on building one SEO and GEO content system approaches this from the AI search, GEO and discovery side.
Structured logging — emitting each line as JSON with a consistent set of fields, a request ID chief among them — turns that same search into a filter instead of a guess. A tool as simple as `jq` against a JSON log stream can answer "show me everything tagged with this request ID across every container" in a way grep on free-text lines can't reliably do once log volume grows past what one person can skim.
Set an explicit max-size and max-file on any container that logs meaningfully, rather than relying on json-file's unlimited default.
Remember that a Server Component's log never reaches the browser — check the terminal or docker logs, not devtools, when a server-side log seems to have vanished.
Remember a Client Component logs twice on a direct visit and once (browser-only) after that, so "it's not in the terminal anymore" after clicking around the site is expected, not a bug.
Treat Coolify's log viewer as a window into the same docker logs stream, not a separate archive — whatever rotation ate, it can't show you either.
Move to structured JSON logging with a request ID once grep starts returning more noise than signal, not before — for a single small app, that point comes later than most guides imply.
None of this is exotic — it's the same stdout-to-file chain every self-hosted container goes through, and the same json-file defaults every fresh Docker install ships with. The only real risk is not knowing the chain exists until an unrotated log file has already filled the disk, which is the same failure mode covered from the memory side in what happens when a VPS runs out of RAM — a different resource, the identical shape of surprise.
It's a genuinely small amount of mechanism to hold in your head once you've traced it once: a component either runs on the server only or on both, whatever it writes goes to stdout, Docker's driver turns stdout into a file with whatever rotation you did or didn't set, and every tool you use afterward — docker logs, Coolify's viewer, a grep pipeline — is reading that same file, not a separate record of the truth. The whole chain fits in one paragraph. What doesn't fit in one paragraph is the debugging session that happens when you don't know the chain exists and start guessing instead — checking the browser console for a server log, assuming Coolify has more history than the driver actually kept, or wondering why a debug flag survived three deploys it should have died on. Tracing it once, deliberately, is cheaper than any of those guesses.
Further reading from XenGrowth
The XenGrowth resource library — what you'll learn: how the commercial side of this work is run, across search, automation and revenue operations.
XenGrowth on governed AI marketing workflows — what you'll learn: how the teams running AI marketing agents keep them governed and measurable.
XenGrowth on building one SEO and GEO content system — what you'll learn: how search and AI-answer visibility get run as a single content system.
Where this work meets go-to-market
XenGrowth's work on go-to-market systems writes for the teams who have to run Docker logging day to day.
Five questions tracing one console.log from your code to a file on disk. The chain is short, but almost every step is somewhere people expect the log to be and it isn't.













