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) andKafkaContainer(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.
PostgreSQLContainerstarted and answeredSELECT 1over JDBC. - Node: testcontainers 12.2.1 on Node.js 25.9.0. A
GenericContainerrunningredis:7-alpinewith a log wait strategy, a mapped port, theredis6.3.0 client andexecpassed. - 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:
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:
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 -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:
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
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(); }
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 awithblock or a try-with-resources. If a test process crashes, its containers can stay running:docker ps -a --filter label=org.testcontainers=truelists them anddocker rm -fremoves them. - Bind mounts only from allowed folders.
with_volume_mapping,withFileSystemBindand other host-path mounts work only below a folder you name when starting the daemon, for examplekosa8 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/amd64fail withExec format error.