Skip to content

Include container logs in the error when a container fails to start - #1510

Draft
cristianrgreco wants to merge 9 commits into
mainfrom
claude/logs-on-failed-start-6a663c
Draft

cristianrgreco wants to merge 9 commits into
mainfrom
claude/logs-on-failed-start-6a663c

Conversation

@cristianrgreco

@cristianrgreco cristianrgreco commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

When a container fails its wait strategy, waitForContainer stops and removes it and rethrows the wait strategy's error, so the container's output is lost unless a log consumer was attached beforehand. The error now carries the last 50 lines of the container's logs:

URL /health not accessible after 60000ms
Container logs (last 50 lines):
Error: connect ECONNREFUSED 10.0.0.5:5432
  • waitForContainer reads the log tail, then stops and removes the container as before. The read does not follow the log stream, so it ends whatever state the container is in. client.container.logs now honours follow: false for this; it still follows by default.
  • The same error object is rethrown with the logs appended to message (and to stack, which Node prints for uncaught errors and console.error). The original message stays first.
  • Logs are not appended when the message already contains them, compared ignoring whitespace. This keeps the HTTP abortOnContainerExit message (Container exited during HTTP healthCheck, last 50 logs: ...) unchanged.
  • On restart(), only logs since the restart are included, matching how Wait.forLogMessage reads logs after a restart.
  • A failure to read the logs is logged as a warning and the original error is thrown unchanged.
  • This covers every caller of waitForContainer: start(), the reuse path, restart(), DockerComposeEnvironment.up() and the Kafka and Redpanda modules.

Tail length is 50 lines: it matches the existing HTTP abortOnContainerExit path, so both report the same amount and the duplicate check is exact, and it keeps error messages readable in test reporter output.

Other implementations

  • Java (GenericContainer.tryStart) and Go (applyLifecycleHooks → printLogs) write the full container log to the logger when a start fails. The exception does not carry it.
  • .NET (ContainerNotRunningException) and Python (_get_container_logs_for_debugging in the log wait) put the output in the exception message. Python truncates it.
  • Rust reports no logs.

Borrowed: reporting on any start failure including timeouts (Java, Go), putting the output in the error itself (.NET, Python, and this library's HTTP path), and bounding it (Python).

Not included

Failures inside docker compose up itself, such as a depends_on: service_healthy dependency that never becomes healthy (#1210), do not go through waitForContainer and are unchanged.

Verification

npm run format
npm run lint
npm run knip
npm run check-compiles
npm test -- packages/testcontainers/src/wait-strategies packages/testcontainers/src/generic-container/generic-container.test.ts packages/testcontainers/src/generic-container/generic-container-restart.test.ts packages/testcontainers/src/generic-container/generic-container-reuse.test.ts packages/testcontainers/src/generic-container/generic-container-logs.test.ts packages/testcontainers/src/docker-compose-environment packages/testcontainers/src/container-runtime/clients/container packages/modules/mssqlserver/src/mssqlserver-container.test.ts

Test results

  • Format, lint, knip and compile pass.
  • Targeted tests on Docker: 178 passed, 2 failed, 1 skipped (existing it.skip). Neither failure is caused by this change:
    • GenericContainer restart > should restart persisting state failed with ECONNRESET on the request made after the restart. should restart fails the same way in 4 of 6 local runs against main's source.
    • MSSQLServerContainer > should connect and return a query result with valid custom password failed with a connection pool abort while six SQL Server containers ran at once on a 4 GB Docker VM. It passes when run on its own.
  • The two existing exact-match tests for abortOnContainerExit in http-wait-strategy.test.ts pass unchanged, which covers the no-duplication case.

First CI run

The first push failed two checks, both fixed in the second commit:

  • Tests (mssqlserver): should throw error with invalid password compared the whole error message. It now matches the original message as a substring.
  • Tests (testcontainers, 24.x, podman): the first version read the logs with follow: true after stopping the container. On Podman 4.9.3 that read did not always return (two containers never got past Fetching container logs...), so the health check tests timed out. The read no longer follows. Podman is not reproduced locally (local Podman is 6.1.0), so the Podman jobs on this PR are the check for that.

Red-green for the two new tests:

Before the change:

FAIL  generic-container.test.ts > should include the container logs in the error when the container fails to become ready
-   "message": "Container is not ready
- Container logs (last 50 lines):
- connecting to database",
+   "message": "Container is not ready",

FAIL  generic-container-restart.test.ts > should include the container logs since the restart in the error when the restart fails
-   "message": "Log stream ended and message \"first start\" was not received
- Container logs (last 50 lines):
- second start",
+   "message": "Log stream ended and message \"first start\" was not received",

After the change:

Test Files  2 passed (2)
     Tests  2 passed | 54 skipped (56)

The restart test also fails when the since filter is removed (first start appears in the logs).

The first test starts a container that is still running when the wait fails. With a following read it times out (Test timed out in 180000ms), so it also guards the non-following read.

New client test, before logs honoured follow: false:

FAIL  docker-container-client.test.ts > should end the log stream after the current logs when not following
AssertionError: expected '' to be 'first line\nsecond line\n'

After: Tests 4 passed (4).

Not breaking

  • The error thrown is the same object as before, so its class and any properties are unchanged.
  • The original message is still the start of message, so substring and prefix matching keep working. Only an exact-equality match on the message of a failed start sees a difference, and only when the container produced output. One test in this repository did that (mssqlserver) and is updated here.
  • No public API or type changes. ContainerLogsOptions already had follow; the client ignored it and always followed, which stays the default.

Closes #1506

When a container fails its wait strategy, waitForContainer now reads the
last 50 lines of its logs before removing it and appends them to the
error. The logs are skipped when the error message already contains
them, and a failed restart reports only the logs since the restart.
@cristianrgreco cristianrgreco added enhancement New feature or request minor Backward compatible functionality labels Oct 12, 2026
Following the logs of a stopped container can hang on Podman, which
timed out the health check tests. The logs are now read without follow
and before the container is stopped, so the read ends whatever state the
container is in.

Match the SQL Server invalid password error by substring now that the
error carries the container logs.
main removed streamToString from common and made Readable a type-only
import in the container client, so the merged branch no longer compiled.
…-start-6a663c

# Conflicts:
#	packages/testcontainers/src/container-runtime/clients/container/docker-container-client.ts
@cristianrgreco
cristianrgreco marked this pull request as draft October 13, 2026 15:55

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request minor Backward compatible functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Include container logs in the error when a container fails to start

1 participant