DEV Community

Just_a_kumar
Just_a_kumar

Posted on

Building an AI Subtitle App with Next.js and SigNoz (With Plenty of Docker Debugging Along the Way)

My AI subtitle generator works. Right now, if I upload a video, Next.js processes it, OpenTelemetry captures every span along the way, and SigNoz shows me the whole thing as a live distributed trace. That part's real and it's done.

What I didn't expect is that getting SigNoz itself to behave would turn into its own separate project — one fought entirely in Docker volumes, a login screen that made no sense, and a PowerShell terminal that hated me personally. This is the honest version of that story: not a clean tutorial, but what actually happened.

The Success, Up Front

Because I don't want this to read like a two-thousand-word bug report: it worked. The app takes a video upload, kicks off backend processing to generate cinematic AI-style subtitles, and every one of those steps — upload, processing request, whatever happens in between — shows up as a real trace in SigNoz's Traces Explorer. That was the actual goal of the hackathon submission, and I hit it.

But I want to be upfront that the road there was not smooth, and I think the messy part is more useful to write about than the finished screenshot. If you're about to self-host SigNoz for the first time, the app code is not going to be your hard part. Your local environment is.

What I Built and Why Tracing Actually Mattered Here

The project itself: a Next.js web app that takes a video upload and automatically generates cinematic, AI-style subtitles for it. Sounds like one feature, but it's really a pipeline — a file comes in, gets handed off for backend processing, something transcribes/times the audio, something else styles the subtitle output, and eventually a result comes back to the user. Every one of those steps can fail or slow down on its own, and without visibility into which step, "my video is stuck processing" just means staring at a spinner and guessing.

So the plan was straightforward: instrument the app with OpenTelemetry so every upload and processing request generates a real trace, run those traces into a local SigNoz stack, and actually see the pipeline instead of adding console.log statements and hoping.

The Stack

  • App layer: Next.js, instrumented with OpenTelemetry so uploads and backend processing requests each produce spans and a trace.
  • Observability backend: SigNoz, self-hosted locally.
  • Storage: ClickHouse for the telemetry data itself (traces/metrics/logs), PostgreSQL for user and metadata storage.
  • Orchestration: Docker containers, spun up and managed through Foundry using a config file we named casting.yaml.

We used Foundry as a wrapper around Docker Compose to easily orchestrate and manage our multi-container stack from a single casting.yaml file.

For the instrumentation itself, I used the instrumentation.ts convention Next.js picks up automatically, with @vercel/otel handling the SDK wiring instead of me hand-rolling the whole OpenTelemetry setup:

// instrumentation.ts (project root)
import { registerOTel } from '@vercel/otel';

export function register() {
  registerOTel({
    serviceName: 'subtitle-studio',
  });
}
Enter fullscreen mode Exit fullscreen mode
# .env.local
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
Enter fullscreen mode Exit fullscreen mode

That part, honestly, was the easy part. It's well-documented, it's a known pattern, and it worked close to the first try. The infrastructure underneath it is where things got real.

The Gritty Middle: When Your Own Local Environment Becomes the Boss Fight

Here's the thing nobody really warns you about with local observability stacks: the app code is the small problem. The stack running underneath it — the containers, the volumes, the state that quietly persists between runs — is where you actually lose your evening.

I brought the SigNoz stack up through Foundry (foundryctl.exe cast -f casting.yaml), fully expecting the standard first-run experience: a setup wizard, create an admin account, done. Instead I got bounced straight to /login with a message telling me self-registration was disabled.

I want to sit on that for a second because it's such a specifically confusing error to get on what I thought was a brand-new install. Self-registration disabled implies someone already registered. I hadn't. Nobody had. Except — and this took me embarrassingly long to piece together — my Docker volumes disagreed. They were holding onto state from an earlier attempt at standing this stack up, an attempt where an org and an admin account had technically already been created. So SigNoz wasn't lying to me. It was correctly telling me it was already initialized. I just had zero credentials for the version of the world it remembered.

This is the part of local infra work that I think is genuinely harder than writing application code: the bug isn't in front of you, it's behind you, sitting in a volume you forgot existed from a run you forgot you did.

My first instinct was to patch around it instead of admitting the environment was compromised — because tearing everything down and starting over felt like giving up.

Attempt one: dig through environment variables related to auth/registration, on the theory there was a flag somewhere I could just flip back to "allow registration." No dice — the state causing the problem wasn't in an env var, it was already sitting in the database.

Attempt two: go straight into Postgres and manually INSERT an admin row myself. This is where I properly hit a wall. SigNoz's schema has real relational constraints — an org, an admin, and a user aren't three independent rows you can fake into existence, they're expected to be created together through the app's own flow. My hand-written insert kept getting rejected by schema constraints it had every right to reject me on.

Attempt three, which somehow made it worse: I was doing all of this from PowerShell, and trying to escape a multi-field SQL statement's quotes correctly through PowerShell's quoting rules turned into a fully separate debugging side-quest that had nothing to do with SigNoz at all. I burned real time just trying to get a single INSERT statement to survive the terminal without PowerShell mangling the quotes.

At this point I'd spent more time fighting my own terminal than I had setting up the actual tracing. That's the part I want other people attempting this to know going in: local debugging on a self-hosted observability stack can absolutely eat more time than the feature you're trying to observe.

The Turn: Stop Patching, Wipe the Slate

Eventually I stopped trying to be clever about it. If the environment itself was the problem, patching the environment wasn't going to fix it — I needed to actually reset it.

docker compose down
docker volume prune
foundryctl.exe cast -f casting.yaml
Enter fullscreen mode Exit fullscreen mode

Stopping the containers and pruning the volumes genuinely wiped the leftover ClickHouse and Postgres data instead of leaving ghosts behind for the next run to trip over. Re-casting the stack from casting.yaml afterward gave me an actually-fresh instance — and this time SigNoz served up the real first-run setup wizard instead of bouncing me to a login screen for an account I didn't have.

There's a specific kind of relief in watching a setup wizard appear where a broken login screen used to be. It's a small thing, but after that stretch of fighting SQL constraints and PowerShell quoting, it felt like an actual win.

I created my admin account, and — because this weekend clearly wasn't done testing me — SigNoz threw a ClickHouse metadata error on first boot while its container init scripts were still finishing structuring the schema. This time, though, I didn't panic and start patching. I recognized it for what it was: the database still setting itself up. I let it finish. It resolved on its own once ClickHouse had fully initialized its tables.

The Relatable Win

Then the actual payoff: I uploaded a test video through the Next.js app and watched a real trace land in SigNoz's Traces Explorer at http://localhost:8080/traces-explorer — the upload request and the backend processing request, connected, with real timing data sitting right there on screen.

Not going to pretend that wasn't a genuinely good moment. After a stretch of ghost data, schema constraint errors, and quoting fights that had nothing to do with the actual project, seeing a real trace show up — one that mapped to a video I'd just uploaded, seconds earlier — was the first time the whole weekend felt like it was working for me instead of against me.

What I Actually Learned

  • A confusing auth error is often stale state, not broken config. The self-registration message sent me looking at auth settings first, when the real issue was Docker volumes remembering a previous run. If a "fresh" deploy behaves like it isn't fresh, check the volumes before you touch any code or config.
  • Don't hand-write inserts into a system with real schema relationships. SigNoz expects org/admin/user creation to happen through its own flow. A manual INSERT was never going to be a clean shortcut, and I lost real time learning that the hard way.
  • PowerShell quoting is a tax on anything multi-field. Next time I'd write inline SQL/JSON to a file and pipe it in rather than fight inline escaping in the terminal.
  • docker volume prune + re-cast beat every "smart" fix combined. The fastest debugging move was admitting the environment itself was the bug and rebuilding it, instead of trying to outsmart it in place.
  • Local infra debugging can genuinely be harder than the application code. Writing the OpenTelemetry instrumentation was a known, documented pattern that mostly just worked. Getting the observability backend into a clean, working state was where all the real friction lived — and I don't think that's talked about enough going into a project like this.
  • It was worth it. Once traces were flowing, "is the upload slow or is processing slow" stopped being a guess and became something I could just look at.

If I did this again, I'd write down the "nuke it and re-cast" steps before trying anything clever with env vars or manual SQL — because that's what I ended up doing anyway, just an hour later than I needed to.

Wrapping Up

The finished result: a working local SigNoz setup tracking real video-processing traces, with no errors, for an app that actually does what it's supposed to do. Getting there meant fighting through stale Docker state, a login wall I didn't expect, schema constraints, and a PowerShell quoting battle that had nothing to do with observability at all — and then winning that fight with a clean wipe and a re-cast. That's the real version of how this project came together, mess and all.

Top comments (0)