kosa8 / Integrations

Integration · Testcontainers

Testcontainers on kosa8

Point Testcontainers at kosa8's Docker-compatible socket and your existing tests run in kosa8's microVM. Python, Java and Node all need the same two environment variables and no code changes.

01What was tested

Tested with kosa8 v0.9.10, on 15 October 2026 on an Apple silicon Mac (macOS, arm64), with the released v0.9.10 binaries and guest image.

  • Python: testcontainers-python 4.15.0, docker (the Python SDK) 7.2.0, Python 3.12.13. RedisContainer, PostgresContainer (SQLAlchemy and psycopg2) and KafkaContainer (confluentinc/cp-kafka:7.6.0, produce and consume with kafka-python 3.0.11) passed.
  • Java: testcontainers-java 1.21.3 with JUnit 5, OpenJDK 21 and Maven. PostgreSQLContainer started and answered SELECT 1 over JDBC.
  • Node: testcontainers 12.2.1 on Node.js 25.9.0. A GenericContainer running redis:7-alpine with a log wait strategy, a mapped port, the redis 6.3.0 client and exec passed.
  • Needs a setting: Ryuk, the reaper container Testcontainers starts by default, is refused by kosa8 on purpose (see Limits), so it has to be switched off in every language.

02Set it up

Start the daemon, then point Testcontainers at kosa8's socket and switch off Ryuk. The same two variables work for Python, Java and Node. Put the exports in your shell profile or your CI job's environment.

Check that the Docker CLI (if you have it) reaches kosa8 before you run tests:

a quick check
docker run --rm alpine echo hi
hi

Nothing else is needed: Java no longer needs TESTCONTAINERS_CHECKS_DISABLE, because kosa8 reports a Docker Engine version (28.3.0, API 1.51).

03Python

Nothing in the test is specific to kosa8:

test_kosa8.py
import sqlalchemy
from testcontainers.core.container import DockerContainer
from testcontainers.core.waiting_utils import wait_for_logs
from testcontainers.postgres import PostgresContainer
from testcontainers.redis import RedisContainer
 
 
def test_generic():
    with DockerContainer("alpine:3.20").with_command("sh -c 'echo ready; sleep 30'") as c:
        wait_for_logs(c, "ready", timeout=60)
        code, out = c.exec("echo hello-from-exec")
        assert code == 0 and b"hello-from-exec" in out
 
 
def test_redis():
    with RedisContainer("redis:7-alpine") as r:
        client = r.get_client()
        client.set("k", "v")
        assert client.get("k") == b"v"
 
 
def test_postgres():
    with PostgresContainer("postgres:16-alpine") as pg:
        engine = sqlalchemy.create_engine(pg.get_connection_url())
        with engine.connect() as conn:
            version = conn.execute(sqlalchemy.text("select version()")).scalar()
            assert "PostgreSQL 16" in version
pytest
pytest -v test_kosa8.py
test_kosa8.py::test_generic PASSED
test_kosa8.py::test_redis PASSED
test_kosa8.py::test_postgres PASSED

KafkaContainer works the same way; it copies its start script into the container, which kosa8 now supports.

04Java

Add org.testcontainers:postgresql and org.testcontainers:junit-jupiter 1.21.3 (plus the PostgreSQL JDBC driver) as test dependencies. This is the test that was run with mvn test:

PostgresTest.java
package e2e;
 
import static org.junit.jupiter.api.Assertions.assertEquals;
 
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.ResultSet;
import org.junit.jupiter.api.Test;
import org.testcontainers.containers.PostgreSQLContainer;
 
class PostgresTest {
  @Test
  void selectOne() throws Exception {
    try (PostgreSQLContainer<?> pg = new PostgreSQLContainer<>("postgres:16-alpine")) {
      pg.start();
      try (Connection c = DriverManager.getConnection(pg.getJdbcUrl(), pg.getUsername(), pg.getPassword());
           ResultSet rs = c.createStatement().executeQuery("SELECT 1")) {
        rs.next();
        assertEquals(1, rs.getInt(1));
      }
    }
  }
}

Testcontainers may log a warning that the image architecture arm64 does not match the server's aarch64. They are the same thing, and nothing is emulated.

05Node

redis-test.mjs
import { GenericContainer, Wait } from "testcontainers";
import { createClient } from "redis";
 
const container = await new GenericContainer("redis:7-alpine")
  .withExposedPorts(6379)
  .withWaitStrategy(Wait.forLogMessage("Ready to accept connections"))
  .start();
try {
  const url = `redis://${container.getHost()}:${container.getMappedPort(6379)}`;
  const client = createClient({ url });
  await client.connect();
  await client.set("k", "v1");
  console.log("get:", await client.get("k"));
  await client.quit();
 
  const ex = await container.exec(["redis-cli", "get", "k"]);
  console.log("exec:", ex.exitCode, ex.output.trim());
} finally {
  await container.stop();
}
output
node redis-test.mjs
get: v1
exec: 0 v1

06Limits

  • No Ryuk. kosa8 never shares the Docker API socket into a container, because a container holding it controls every other container. Containers are still removed when stop() runs, for example at the end of a with block or a try-with-resources. If a test process crashes, its containers can stay running: docker ps -a --filter label=org.testcontainers=true lists them and docker rm -f removes them.
  • Bind mounts only from allowed folders. with_volume_mapping, withFileSystemBind and other host-path mounts work only below a folder you name when starting the daemon, for example kosa8 daemon start --allow-mount ~/src/fixtures (read-only) or --allow-mount DIR:rw. Anything else fails with a 400 error that says how to allow it. Copying files in (with_copy_to_container, withCopyFileToContainer) needs no allow list.
  • Apple silicon runs arm64 images. Images published only for linux/amd64 fail with Exec format error.