diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7abeb5f85dbfc69a5f896f5d84c611614204c079..8656f980f98220efff476d93886f5ba3bfed40c8 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -30,6 +30,11 @@ build:
 
 test:
   stage: "test"
+  services:
+    - name: "k8r.eu/justjanne/quassel-docker"
+      alias: "quasselcore"
+  variables:
+    QUASSEL_CONTAINER: "quassel:4242"
   script:
     - "dockerd-rootless.sh"
     - "./gradlew check -x connectedCheck coberturaTestReport"
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt
index 5d4454fa2ac9ada37c7ea25817fa8d02f19d577e..abfc7710cd57fb62a4e6723db13e4737042611a1 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/EndToEndTest.kt
@@ -29,36 +29,20 @@ import de.kuschku.libquassel.protocol.serializers.handshake.ClientInitSerializer
 import de.kuschku.libquassel.protocol.serializers.primitive.HandshakeMapSerializer
 import de.kuschku.libquassel.protocol.serializers.primitive.IntSerializer
 import de.kuschku.libquassel.protocol.variant.into
+import de.kuschku.libquassel.testutil.TestX509TrustManager
+import de.kuschku.libquassel.testutil.quasselContainer
 import de.kuschku.quasseldroid.protocol.io.CoroutineChannel
 import kotlinx.coroutines.ExperimentalCoroutinesApi
 import kotlinx.coroutines.runBlocking
-import kotlinx.coroutines.test.runBlockingTest
-import org.junit.Before
+import org.junit.jupiter.api.AfterEach
 import org.junit.jupiter.api.BeforeEach
 import org.junit.jupiter.api.Test
-import org.slf4j.LoggerFactory
-import org.testcontainers.containers.BindMode
-import org.testcontainers.containers.output.Slf4jLogConsumer
-import org.testcontainers.junit.jupiter.Container
-import org.testcontainers.junit.jupiter.Testcontainers
-import org.testcontainers.utility.MountableFile
-import java.net.InetSocketAddress
 import java.nio.ByteBuffer
 import javax.net.ssl.SSLContext
 
 @ExperimentalCoroutinesApi
-@Testcontainers
 class EndToEndTest {
-  @Container
-  val quassel = QuasselContainer()
-    .withExposedPorts(4242)
-    .withClasspathResourceMapping("/quasseltest.crt", "/quasseltest.crt", BindMode.READ_WRITE)
-    .withEnv("SSL_CERT_FILE", "/quasseltest.crt")
-    .withClasspathResourceMapping("/quasseltest.key", "/quasseltest.key", BindMode.READ_WRITE)
-    .withEnv("SSL_KEY_FILE", "/quasseltest.key")
-    .withEnv("CONFIG_FROM_ENVIRONMENT", "true")
-    .withEnv("DB_BACKEND", "SQLite")
-    .withEnv("AUTH_AUTHENTICATOR", "Database")
+  private val quassel = quasselContainer()
 
   private val sslContext = SSLContext.getInstance("TLSv1.3").apply {
     init(null, arrayOf(TestX509TrustManager), null)
@@ -70,16 +54,18 @@ class EndToEndTest {
   private val channel = CoroutineChannel()
 
   @BeforeEach
-  fun setUp() {
-    quassel.followOutput(Slf4jLogConsumer(LoggerFactory.getLogger(EndToEndTest::class.java)))
+  fun start() {
+    quassel.start()
+  }
+
+  @AfterEach
+  fun stop() {
+    quassel.stop()
   }
 
   @Test
   fun testConnect() = runBlocking {
-    channel.connect(InetSocketAddress(
-      quassel.host,
-      quassel.getMappedPort(4242)
-    ))
+    channel.connect(quassel.address)
 
     println("Writing protocol")
     write(sizePrefix = false) {
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/QuasselContainer.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/GitlabCiProvidedContainer.kt
similarity index 73%
rename from libquassel/src/test/kotlin/de/kuschku/libquassel/QuasselContainer.kt
rename to libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/GitlabCiProvidedContainer.kt
index 903852c31741d00a15988bb64c7ab745fedb6050..d3b246fefc214e28b56d70fdc45a9e00ebe487ac 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/QuasselContainer.kt
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/GitlabCiProvidedContainer.kt
@@ -17,11 +17,13 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel
+package de.kuschku.libquassel.testutil
 
-import org.testcontainers.containers.GenericContainer
-import org.testcontainers.utility.DockerImageName
+import java.net.InetSocketAddress
 
-class QuasselContainer : GenericContainer<QuasselContainer>(
-        DockerImageName.parse("k8r.eu/justjanne/quassel-docker:latest")
-)
+class GitlabCiProvidedContainer(
+  override val address: InetSocketAddress
+) : ProvidedContainer {
+  override fun start() = Unit
+  override fun stop() = Unit
+}
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/ProvidedContainer.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/ProvidedContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..743d735bbcfdf270c3668d5d49100dbe9e3b3571
--- /dev/null
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/ProvidedContainer.kt
@@ -0,0 +1,29 @@
+/*
+ * 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.libquassel.testutil
+
+import java.net.InetSocketAddress
+
+interface ProvidedContainer {
+  fun start()
+  fun stop()
+
+  val address: InetSocketAddress
+}
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/QuasselCoreContainer.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/QuasselCoreContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..7cb9b728f4cb4e4e1404a24c005300294b46fc85
--- /dev/null
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/QuasselCoreContainer.kt
@@ -0,0 +1,59 @@
+/*
+ * 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.libquassel.testutil
+
+import org.slf4j.LoggerFactory
+import org.testcontainers.containers.BindMode
+import org.testcontainers.containers.GenericContainer
+import org.testcontainers.containers.output.Slf4jLogConsumer
+import org.testcontainers.utility.DockerImageName
+
+class QuasselCoreContainer : GenericContainer<QuasselCoreContainer>(
+  DockerImageName.parse("k8r.eu/justjanne/quassel-docker:latest")
+) {
+  init {
+    withExposedPorts(QUASSEL_PORT)
+    withClasspathResourceMapping(
+      "/quasseltest.crt",
+      "/quasseltest.crt",
+      BindMode.READ_WRITE)
+    withEnv("SSL_CERT_FILE", "/quasseltest.crt")
+    withClasspathResourceMapping(
+      "/quasseltest.key",
+      "/quasseltest.key",
+      BindMode.READ_WRITE)
+    withEnv("SSL_KEY_FILE", "/quasseltest.key")
+    withEnv("CONFIG_FROM_ENVIRONMENT", "true")
+    withEnv("DB_BACKEND", "SQLite")
+    withEnv("AUTH_AUTHENTICATOR", "Database")
+  }
+
+  override fun start() {
+    super.start()
+    followOutput(Slf4jLogConsumer(logger))
+  }
+
+  companion object {
+    @JvmStatic
+    private val logger = LoggerFactory.getLogger(QuasselCoreContainer::class.java)
+
+    const val QUASSEL_PORT = 4242
+  }
+}
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestContainersProvidedContainer.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestContainersProvidedContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..cc91fb4b1cd5415249f4e8707de6221f5e522d3e
--- /dev/null
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestContainersProvidedContainer.kt
@@ -0,0 +1,36 @@
+/*
+ * 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.libquassel.testutil
+
+import org.testcontainers.containers.GenericContainer
+import java.net.InetSocketAddress
+
+class TestContainersProvidedContainer<T : GenericContainer<T>>(
+  private val container: T,
+  private val port: Int
+) : ProvidedContainer {
+  override fun start() = container.start()
+  override fun stop() = container.stop()
+  override val address
+    get() = InetSocketAddress(
+      container.containerIpAddress,
+      container.getMappedPort(port)
+    )
+}
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/TestX509TrustManager.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestX509TrustManager.kt
similarity index 96%
rename from libquassel/src/test/kotlin/de/kuschku/libquassel/TestX509TrustManager.kt
rename to libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestX509TrustManager.kt
index 6d838bf1146e1991a58a2c6df7d6cbc7f7f40812..4ace77892250771bd883a680e97b868501010f45 100644
--- a/libquassel/src/test/kotlin/de/kuschku/libquassel/TestX509TrustManager.kt
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/TestX509TrustManager.kt
@@ -17,7 +17,7 @@
  * with this program. If not, see <http://www.gnu.org/licenses/>.
  */
 
-package de.kuschku.libquassel
+package de.kuschku.libquassel.testutil
 
 import java.security.cert.X509Certificate
 import javax.net.ssl.X509TrustManager
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/providedContainer.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/providedContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..8864b423ed257ecd80adad4e67c3393a12c610e3
--- /dev/null
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/providedContainer.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.libquassel.testutil
+
+import java.net.InetSocketAddress
+
+fun providedContainer(
+  envVariable: String,
+  f: () -> ProvidedContainer
+) = when {
+  !System.getenv(envVariable).isNullOrEmpty() -> {
+    val (host, port) = System.getenv(envVariable).split(":")
+    GitlabCiProvidedContainer(InetSocketAddress(host, port.toInt()))
+  }
+  else -> f()
+}
diff --git a/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/quasselContainer.kt b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/quasselContainer.kt
new file mode 100644
index 0000000000000000000000000000000000000000..37a304e912882f5b701c1bceadd7187402e090da
--- /dev/null
+++ b/libquassel/src/test/kotlin/de/kuschku/libquassel/testutil/quasselContainer.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.libquassel.testutil
+
+fun quasselContainer() = providedContainer("QUASSEL_CONTAINER") {
+  TestContainersProvidedContainer(
+    QuasselCoreContainer(),
+    QuasselCoreContainer.QUASSEL_PORT
+  )
+}