diff --git a/ci-containers/build.gradle.kts b/ci-containers/build.gradle.kts
new file mode 100644
index 0000000000000000000000000000000000000000..504442f95d76597dac1abc2999c0cbd9740430d3
--- /dev/null
+++ b/ci-containers/build.gradle.kts
@@ -0,0 +1,11 @@
+plugins {
+  kotlin("jvm")
+}
+
+dependencies {
+  implementation(kotlin("stdlib"))
+
+  val testContainersVersion: String by project.extra
+  api("org.testcontainers", "testcontainers", testContainersVersion)
+  api("org.testcontainers", "junit-jupiter", testContainersVersion)
+}
diff --git a/ci-containers/src/main/kotlin/de/kuschku/ci_containers/CiContainers.kt b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/CiContainers.kt
new file mode 100644
index 0000000000000000000000000000000000000000..96cb82e030a4b7799c775f89376720d450995045
--- /dev/null
+++ b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/CiContainers.kt
@@ -0,0 +1,28 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.ci_containers
+
+import org.junit.jupiter.api.extension.ExtendWith
+
+@MustBeDocumented
+@Target(AnnotationTarget.CLASS, AnnotationTarget.ANNOTATION_CLASS)
+@Retention(AnnotationRetention.RUNTIME)
+@ExtendWith(CiContainersExtension::class)
+annotation class CiContainers
diff --git a/ci-containers/src/main/kotlin/de/kuschku/ci_containers/CiContainersExtension.kt b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/CiContainersExtension.kt
new file mode 100644
index 0000000000000000000000000000000000000000..3a9d84d190569301260e3692fefc1dd86bb9e544
--- /dev/null
+++ b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/CiContainersExtension.kt
@@ -0,0 +1,47 @@
+/*
+ * Quasseldroid - Quassel client for Android
+ *
+ * Copyright (c) 2021 Janne Mareike Koschinski
+ * Copyright (c) 2021 The Quassel Project
+ *
+ * This program is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License version 3 as published
+ * by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License along
+ * with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+package de.kuschku.ci_containers
+
+import org.junit.jupiter.api.extension.AfterEachCallback
+import org.junit.jupiter.api.extension.BeforeEachCallback
+import org.junit.jupiter.api.extension.ExtensionContext
+
+class CiContainersExtension : BeforeEachCallback, AfterEachCallback {
+  private fun getContainers(context: ExtensionContext?): List<ProvidedContainer> {
+    val containers = mutableListOf<ProvidedContainer>()
+    context?.requiredTestInstances?.allInstances?.forEach { instance ->
+      instance.javaClass.declaredFields.map { field ->
+        if (field.type == ProvidedContainer::class.java) {
+          field.trySetAccessible()
+          containers.add(field.get(instance) as ProvidedContainer)
+        }
+      }
+    }
+    return containers
+  }
+
+  override fun beforeEach(context: ExtensionContext?) {
+    getContainers(context).forEach(ProvidedContainer::start)
+  }
+
+  override fun afterEach(context: ExtensionContext?) {
+    getContainers(context).forEach(ProvidedContainer::stop)
+  }
+}
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/GitlabCiProvidedContainer.kt b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/GitlabCiProvidedContainer.kt
similarity index 95%
rename from libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/GitlabCiProvidedContainer.kt
rename to ci-containers/src/main/kotlin/de/kuschku/ci_containers/GitlabCiProvidedContainer.kt
index d3b246fefc214e28b56d70fdc45a9e00ebe487ac..2f49a38e870793326e5024ded02a49ee034455c2 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/GitlabCiProvidedContainer.kt
+++ b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/GitlabCiProvidedContainer.kt
@@ -17,7 +17,7 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel.testutil
+package de.kuschku.ci_containers
 
 import java.net.InetSocketAddress
 
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/ProvidedContainer.kt b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/ProvidedContainer.kt
similarity index 95%
rename from libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/ProvidedContainer.kt
rename to ci-containers/src/main/kotlin/de/kuschku/ci_containers/ProvidedContainer.kt
index 743d735bbcfdf270c3668d5d49100dbe9e3b3571..7056b507aee6d14879a8f7d393cc5875c34d6637 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/ProvidedContainer.kt
+++ b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/ProvidedContainer.kt
@@ -17,7 +17,7 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel.testutil
+package de.kuschku.ci_containers
 
 import java.net.InetSocketAddress
 
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestContainersProvidedContainer.kt b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/TestContainersProvidedContainer.kt
similarity index 96%
rename from libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestContainersProvidedContainer.kt
rename to ci-containers/src/main/kotlin/de/kuschku/ci_containers/TestContainersProvidedContainer.kt
index cc91fb4b1cd5415249f4e8707de6221f5e522d3e..7b76f4bf36387bbe0a6d8ee3d8508709e16a5c64 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestContainersProvidedContainer.kt
+++ b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/TestContainersProvidedContainer.kt
@@ -17,7 +17,7 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel.testutil
+package de.kuschku.ci_containers
 
 import org.testcontainers.containers.GenericContainer
 import java.net.InetSocketAddress
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/providedContainer.kt b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/providedContainer.kt
similarity index 96%
rename from libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/providedContainer.kt
rename to ci-containers/src/main/kotlin/de/kuschku/ci_containers/providedContainer.kt
index 8864b423ed257ecd80adad4e67c3393a12c610e3..219fb4d55592d6defde38d9098fe27008be11c73 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/providedContainer.kt
+++ b/ci-containers/src/main/kotlin/de/kuschku/ci_containers/providedContainer.kt
@@ -17,7 +17,7 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel.testutil
+package de.kuschku.ci_containers
 
 import java.net.InetSocketAddress
 
diff --git a/libquassel/build.gradle.kts b/libquassel/build.gradle.kts
index 3c3971b474ab81bc2c6f469f8320e1d2067155ef..47ed5b5abc610a74ed7d036429c7ad1b801f2783 100644
--- a/libquassel/build.gradle.kts
+++ b/libquassel/build.gradle.kts
@@ -31,8 +31,7 @@ dependencies {
   testRuntimeOnly("org.junit.jupiter", "junit-jupiter-engine", junit5Version)
   val hamcrestVersion: String by project.extra
   testImplementation("org.hamcrest", "hamcrest-library", hamcrestVersion)
-  val testContainersVersion: String by project.extra
-  testImplementation("org.testcontainers", "testcontainers", testContainersVersion)
-  testImplementation("org.testcontainers", "junit-jupiter", testContainersVersion)
+
+  testImplementation(project(":ci-containers"))
   testImplementation("org.slf4j", "slf4j-simple", "1.7.30")
 }
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt
index abfc7710cd57fb62a4e6723db13e4737042611a1..54ad06b48f616e62d9306327ca57f656de1a4e44 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt
@@ -20,6 +20,8 @@
 package de.kuschku.libquassel
 
 import de.kuschku.bitflags.of
+import de.kuschku.ci_containers.CiContainers
+import de.kuschku.ci_containers.CiContainersExtension
 import de.kuschku.libquassel.protocol.connection.*
 import de.kuschku.libquassel.protocol.features.FeatureSet
 import de.kuschku.libquassel.protocol.io.ChainedByteBuffer
@@ -34,13 +36,13 @@ import de.kuschku.libquassel.testutil.quasselContainer
 import de.kuschku.quasseldroid.protocol.io.CoroutineChannel
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.runBlocking
-import org.junit.jupiter.api.AfterEach
-import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
 import java.nio.ByteBuffer
 import javax.net.ssl.SSLContext
 
 @ExperimentalCoroutinesApi
+@CiContainers
 class EndToEndTest {
   private val quassel = quasselContainer()
 
@@ -53,16 +55,6 @@ class EndToEndTest {
   private val sendBuffer = ChainedByteBuffer(direct = true)
   private val channel = CoroutineChannel()
 
-  @BeforeEach
-  fun start() {
-    quassel.start()
-  }
-
-  @AfterEach
-  fun stop() {
-    quassel.stop()
-  }
-
   @Test
   fun testConnect() = runBlocking {
     channel.connect(quassel.address)
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/quasselContainer.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/quasselContainer.kt
index 37a304e912882f5b701c1bceadd7187402e090da..44b7c1e7857d0fe6eed1edf8ebb8690cd8948cef 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/quasselContainer.kt
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/quasselContainer.kt
@@ -19,6 +19,9 @@
 
 package de.kuschku.libquassel.testutil
 
+import de.kuschku.ci_containers.TestContainersProvidedContainer
+import de.kuschku.ci_containers.providedContainer
+
 fun quasselContainer() = providedContainer("QUASSEL_CONTAINER") {
   TestContainersProvidedContainer(
     QuasselCoreContainer(),
diff --git a/settings.gradle.kts b/settings.gradle.kts
index 74ba77b6a260ca2d563ae84312cb9e05d96612cb..e1c2b62c8ec8074aebd8458566a999998f5102bd 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -25,5 +25,6 @@ include(
   ":bitflags",
   ":protocol",
   ":coverage-annotations",
-  ":libquassel"
+  ":libquassel",
+  ":ci-containers"
 )