Where console.log Actually Goes When You Self-Host Next.js
Tutorial

Where console.log Actually Goes When You Self-Host Next.js

On Vercel, a console.log just shows up in a dashboard somewhere. Self-hosted, it goes through a chain most people never trace end to end — and the two most common questions I get are why a log doesn't show up at all, and why the disk filled with logs nobody remembers writing.

Published November 2, 202511 min readUpdated Sep 6, 2026

Written by · Full-Stack Agentic AI Software Engineer — AI Agents, Automation & Revenue Systems for GTM/RevOps teams

In brief

When you self-host a Next.js app, where does console.log actually end up, and what changes between server and client components?

A Server Component's console.log only ever runs on the server and only ever appears in the server's own stdout/stderr. A Client Component is different: on a direct page visit it renders once on the server during SSR and again in the browser during hydration, so the exact same console.log appears in both the terminal and the browser console — but on a client-side navigation afterward, it only fires in the browser. From there, whatever hits stdout goes through Docker's logging driver — json-file by default, which does not rotate unless you configure max-size and max-file — onto disk, where `docker logs` reads it back. Coolify's log viewer is reading that same stream, with the same defaults, unless configured otherwise, and unrotated logs are a real, mundane way a small VPS fills its disk.

  • Server Components log only on the server, in the terminal or docker logs output — never in the browser console
  • Client Components log on the server too, during SSR on a direct visit, and then again in the browser during hydration — a client-side navigation afterward logs in the browser only
  • Docker's json-file logging driver is the default and does not rotate by default — max-size is -1 (unlimited) and max-file is 1 unless set explicitly
  • docker logs reads the same json-file log Docker already wrote; it isn't a separate log store, so whatever rotation policy you set is the only thing standing between normal logging and a full disk
  • grep across raw console.log lines stops scaling once you have more than one service or need to correlate a request across containers — that's the point to move to structured logging, not before

Evidence notes

Server and Client Component logging behavior

Next.js's own docs on the Server/Client boundary state a Client Component renders on the server and again in the browser on a direct visit, and that its log appears in both the terminal and the browser console — on a client-side navigation, only in the browser.

Docker json-file logging driver defaults

Docker's own docs state the json-file driver's max-size defaults to -1 (unlimited) and max-file to 1, meaning no rotation happens unless explicitly configured via --log-opt.

Next.js self-hosting guide

Next.js's own self-hosting docs (this repo runs Next 16.3.3) confirm middleware is now the Proxy convention (proxy.ts), that next/image optimization works self-hosted with zero configuration under next start, and that a reverse proxy in front of next start is the documented recommendation.

Continue with purpose

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.

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.

  1. Set an explicit max-size and max-file on any container that logs meaningfully, rather than relying on json-file's unlimited default.

  2. 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.

  3. 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.

  4. 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.

  5. 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

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.

Where did that log actually go?

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.

1 / 5
A console.log in a Server Component. Where does it appear?

Apply this article

How to turn insights into execution

A practical sequence for teams turning concepts into production outcomes.

console.logNext.jsDocker loggingself-hostingstructured loggingCoolifyserver componentstutorials

Audit your current state

Map the bottlenecks and constraints connected to the article’s core problem.

Choose one bounded change

Test the most useful recommendation on one workflow before widening the scope.

Measure what changed

Keep the parts that improve the work, document what failed, and make the next decision from evidence.

Next step

Need help applying this in your stack?

I can translate these patterns into a concrete implementation plan for your team.

Discuss implementationBack to blog

Replies usually within 24 hours.

Next Steps

Continue reading

How to Self-Host Next.js With Coolify: A Complete 2026 Walkthrough

Every Coolify tutorial stops at "push to deploy." This one covers the parts that actually break a real app: standalone output, a build that OOMs on a small box, env vars baked in at the wrong time, and a health check that would have caught all of it.

Navigate

How I Restore an Entire Server From Backup (And How Long It Takes)

The order matters more than people expect, and the answer to "how long will this take" isn't a number I can hand you — it's dominated by your database size, your download bandwidth, and one DNS setting most people only think to change after they needed to.

Navigate

The Docker Concepts You Need Before Coolify Hides Them From You

Coolify's whole pitch is that you shouldn't have to think about Docker, and most days that's true. The days it isn't are the days a container is 'running' but unhealthy, a redeploy quietly ate a volume, or an env var vanished — and the dashboard doesn't explain any of it.

Navigate

Wildcard Domains With Coolify and Cloudflare

A wildcard certificate can't be proven the way a normal one is — there's no single page to fetch for a domain that doesn't exist yet. That's why it needs a DNS record instead of an HTTP request, and why Cloudflare's API has to be involved at all.

Navigate

How to Back Up and Restore a Coolify Server Properly

Coolify's built-in backup covers less than most people assume, and a filesystem copy of a live Postgres data directory is not a valid database backup no matter how confident it looks in a file listing. Here's what actually has to be backed up, and a restore you've actually tried before you need it.

Navigate

Browser Cache vs CDN Cache vs Next.js Cache, Explained

A deploy goes out, the origin is correct, and a user still sees the old page for hours. That's not a bug in any one layer — it's three separate caches, each keyed differently, each invalidated by something else entirely.

Navigate

How I Back Up PostgreSQL to S3-Compatible Storage on a Schedule

pg_dump, pg_basebackup and WAL archiving aren't three ways to do the same thing — they answer three different questions about how much data you're willing to lose. Here's which one to run, from a container that has no business having shell access to your host, pushed to storage that makes restoring free instead of expensive.

Navigate

How to Host Unlimited Domains on One VPS With Free SSL

"Unlimited domains, one server" sounds like a sales pitch, but it's a genuinely accurate description of what SNI-based virtual hosting and Let's Encrypt automation do together. The real ceiling isn't domain count. It's RAM.

Navigate

Docker Compose Is More Powerful Than You Think

Most people learn Compose from a five-line docker-compose.yml in a README and stop there. Profiles, real healthchecks, resource limits, and layered override files cover most of what people reach for Kubernetes to get — you're probably one flag away from using the tool you already have.

Navigate

How I Get Alerted the Moment a Self-Hosted App Goes Down

A monitor that checks the wrong thing, sends to a channel you don't watch, and pages you for every 30-second blip is worse than no monitor — it trains you to ignore it. Here's how to build alerting that actually works, for one person who eventually has to sleep.

Navigate