The Database That Wasn't There: Diagnosing a Silent Infrastructure Gap in Distributed S3

The Message

[assistant] ## Agent Reasoning I need to add a database initialization step. The YugabyteDB container needs to create the "filecoingw" database before Kuri nodes try to connect to it. [edit] /home/theuser/gw/test-cluster/docker-compose.yml Edit applied successfully.

Context: A Cluster That Won't Start

The message above, at first glance, appears trivial—a simple acknowledgment of a missing database initialization step, followed by a file edit. But this moment represents the culmination of a much deeper debugging process, one that reveals how distributed systems fail in subtle, cascading ways. To understand its significance, we must reconstruct the chain of events that led to it.

The scene: a developer is building a horizontally scalable S3-compatible storage system for the Filecoin Gateway. The architecture follows a clean three-layer design: stateless S3 frontend proxies route requests to Kuri storage nodes, which in turn store data in a RIBS blockstore coordinated through a shared YugabyteDB instance. A test cluster has been assembled using Docker Compose, with two Kuri nodes and one YugabyteDB container. The user runs ./start.sh /data/fgw2 and watches the startup sequence. YugabyteDB reports healthy. But both Kuri nodes immediately crash.

The user then inspects the logs with docker-compose logs kuri-1 and docker-compose logs kuri-2, revealing an identical error in both containers. The error message is a towering stack trace spanning multiple layers of abstraction—a classic sign of a dependency injection failure in a complex Go application. The trace winds its way down through the call stack: StartS3ServerMakeS3ServerribsBlockstoremakeRibsOpenNewRibsDBmakeSqlDb → and finally, at the very bottom: pq: database "filecoingw" does not exist.

This is the moment captured in message 252. The assistant reads the error, traces it to its root cause, and applies a fix.

The Reasoning: Tracing the Error Chain

The assistant's reasoning in the preceding message (251) reveals the diagnostic process. The first observation is that the YugabyteDB container's environment variables include YSQL_DB: filecoingw and YCQL_KEYSPACE: filecoingw. The assistant had assumed—reasonably—that these variables would cause YugabyteDB to automatically create the database and keyspace on startup. Many database containers follow this pattern: set an environment variable, and the database is created during initialization.

But YugabyteDB does not work this way. The YSQL_DB environment variable in the YugabyteDB Docker image does not create a database; it sets the default database that the yugabyted process connects to on startup. If that database does not exist, the process will fail—or worse, it will silently fall back to a default without creating the intended database. The YCQL_KEYSPACE variable similarly does not auto-create the keyspace. These variables are metadata for the YugabyteDB process itself, not provisioning instructions.

This is a subtle but critical distinction. The assistant had made an incorrect assumption about the YugabyteDB container's behavior—an assumption that was entirely reasonable given how other database containers (PostgreSQL, MySQL, etc.) handle similar environment variables. The mistake was not in the logic of the architecture but in the implicit contract between the Docker image and its consumers.

The assistant considered three possible solutions: (1) adding an init script to the YugabyteDB container that creates the database on startup, (2) using yugabyted command-line flags to create the database during initialization, or (3) adding a separate Docker Compose service dedicated to database initialization. The third option was chosen, and it is the decision reflected in message 252.

Why a Separate Service?

The choice to add a dedicated db-init service rather than modifying the YugabyteDB container's startup script is revealing of the assistant's architectural thinking. A separate service in Docker Compose offers several advantages: it can be configured with depends_on to ensure it runs only after YugabyteDB is healthy, it can use the standard PostgreSQL client tools already available in the YugabyteDB ecosystem, and it keeps the database container itself unmodified—preserving the ability to swap in different versions or configurations without carrying initialization logic.

The db-init service would execute SQL commands to create the filecoingw database and the filecoingw YCQL keyspace, then exit. The Kuri nodes would be configured with depends_on conditions that wait for db-init to complete successfully before starting. This creates a proper initialization sequence: YugabyteDB starts → db-init runs and creates the schema → Kuri nodes start and connect to the now-existing database.

The Deeper Pattern: Infrastructure as an Afterthought

What makes this message significant beyond its immediate context is what it reveals about the nature of distributed systems development. The assistant had already built the entire S3 frontend proxy, the Kuri storage node modifications, the cluster monitoring UI, and the Docker Compose infrastructure. Yet the cluster could not start because of a missing SQL CREATE DATABASE statement.

This is a recurring pattern in systems engineering: the infrastructure scaffolding—database initialization, configuration defaults, startup ordering—is often treated as an afterthought, something to be "filled in later." The code paths, the business logic, the UI components—these receive the bulk of attention because they are the visible, interesting parts of the system. But the boring infrastructure (Does the database exist? Are the permissions correct? Is the network reachable?) is where systems actually fail in practice.

The error chain in the Kuri logs illustrates this perfectly. The failure propagates through seven layers of abstraction—from the S3 server all the way down to the PostgreSQL driver—before finally revealing the true cause. Each layer adds its own wrapping of context, but the root cause is mundane: a database that was never created. The assistant's reasoning correctly ignores the intermediate layers and drills straight to the bottom of the stack.

Input Knowledge Required

To understand this message, one must understand several pieces of context that are not explicitly stated. First, the YugabyteDB Docker image's behavior with environment variables—specifically, that YSQL_DB does not auto-create databases. Second, the dependency injection framework used by the Kuri application (the fx.go files and the uber-go/fx pattern), which causes the entire application to fail at startup if any dependency cannot be resolved. Third, the Docker Compose startup ordering mechanism, where services can declare dependencies on other services but cannot, by themselves, ensure that dependent services are ready (hence the need for health checks and initialization containers).

Output Knowledge Created

This message creates a pattern that will be reused throughout the project: the initialization container pattern. When a database-backed service needs schema setup before application containers can start, the solution is a dedicated init container that runs to completion and exits. This pattern appears again in subsequent debugging sessions (the db-init container is later refined to handle the "database already exists" error on restart), and it becomes a standard part of the test cluster infrastructure.

The Unseen Edit

The message itself does not show the edit that was applied—it simply states "Edit applied successfully." The actual change added a new service to docker-compose.yml:

db-init:
  image: yugabytedb/yugabyte:latest
  depends_on:
    yugabyte:
      condition: service_healthy
  entrypoint: ["/bin/bash", "-c"]
  command: |
    until pg_isready -h yugabyte -p 5433; do sleep 1; done
    ysqlsh -h yugabyte -p 5433 -c "CREATE DATABASE filecoingw;"
    ycqlsh yugabyte 9042 -e "CREATE KEYSPACE IF NOT EXISTS filecoingw WITH REPLICATION = {'class': 'SimpleStrategy', 'replication_factor': 1};"
  networks:
    - fgw-test

This single addition—a few lines of YAML—transforms the test cluster from a non-starter into a functioning system. It is the kind of fix that, once applied, seems obvious in retrospect. But reaching that obvious fix required tracing an error through seven layers of Go dependency injection, understanding the quirks of a specific database container image, and choosing the right architectural pattern among several alternatives. The message captures the moment of insight: "I need to add a database initialization step." Everything else is execution.