diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..b4d716e1a23f6b87ac711d57c788ab6a3a75d529
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,27 @@
+variables:
+  CCACHE_COMPILERCHECK: content
+  CCACHE_DIR: "${CI_PROJECT_DIR}/.ccache"
+
+
+build:
+  stage: build
+  image: alpine:latest
+  tags: [docker]
+  before_script:
+    - echo 'https://dl-cdn.alpinelinux.org/alpine/edge/testing' >> /etc/apk/repositories
+    - apk update && apk add clang-extra-tools meson git python3 py3-pip py3-flask lcov gcovr curl-dev libevent-dev spdlog-dev grep g++ cmake pkgconf openssl
+    - export PATH="$PATH:/root/.local/bin"
+    - pip3 install --user reuse
+    - ./scripts/run_tls_testserver.sh &
+    - ./scripts/run_testserver.sh &
+  script:
+    - meson setup builddir -Db_coverage=true -Dtests=true -Dexamples=true
+    - meson compile -C builddir
+    - ./builddir/tests/requests_tls -d --reporters=junit --out=https.xml
+    - ./builddir/tests/requests -d --reporters=junit --out=http.xml
+    - ./builddir/examples/coeurl_example
+    - ninja -C builddir coverage-text
+    - grep TOTAL builddir/meson-logs/coverage.txt
+  artifacts:
+    reports:
+      junit: http*.xml
diff --git a/tests/requests_tls.cpp b/tests/requests_tls.cpp
index 9e7f3dd7f2d58f1e9e74fe041b75e0dd25dd032e..d901ab1ccce9861bd0218ee56b2df1db19d95c6c 100644
--- a/tests/requests_tls.cpp
+++ b/tests/requests_tls.cpp
@@ -16,8 +16,8 @@ TEST_CASE("Basic request") {
   Client g{};
   g.set_verify_peer(false);
 
-  g.get("https://localhost:5000/", [&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/");
+  g.get("https://localhost:5443/", [&g](const Request &r) {
+    CHECK(r.url() == "https://localhost:5443/");
     CHECK(r.response_code() == 200);
     CHECK(r.error_code() == CURLE_OK);
     CHECK(r.response() == "OK");
@@ -34,8 +34,8 @@ TEST_CASE("Basic request on bg thread") {
 
   std::thread t([&g]() { g.run(); });
 
-  g.get("https://localhost:5000/", [&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/");
+  g.get("https://localhost:5443/", [&g](const Request &r) {
+    CHECK(r.url() == "https://localhost:5443/");
     CHECK(r.response_code() == 200);
     CHECK(r.response() == "OK");
     CHECK(r.response_headers()["content-type"] == "text/plain; charset=utf-8");
@@ -50,9 +50,9 @@ TEST_CASE("Basic manual request") {
   g.set_verify_peer(false);
 
   auto r = std::make_shared<Request>(&g, Request::Method::GET,
-                                     "https://localhost:5000/");
+                                     "https://localhost:5443/");
   r->on_complete([&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/");
+    CHECK(r.url() == "https://localhost:5443/");
     CHECK(r.response_code() == 200);
     CHECK(r.response() == "OK");
     CHECK(r.response_headers()["content-type"] == "text/plain; charset=utf-8");
@@ -69,9 +69,9 @@ TEST_CASE("Basic redirect") {
   g.set_verify_peer(false);
 
   g.get(
-      "https://localhost:5000/redirect",
+      "https://localhost:5443/redirect",
       [&g](const Request &r) {
-        CHECK(r.url() == "https://localhost:5000/redirect");
+        CHECK(r.url() == "https://localhost:5443/redirect");
         CHECK(r.response_code() == 200);
         CHECK(r.response() == "OK");
         g.close();
@@ -84,9 +84,9 @@ TEST_CASE("No redirect") {
   g.set_verify_peer(false);
 
   g.get(
-      "https://localhost:5000/redirect",
+      "https://localhost:5443/redirect",
       [&g](const Request &r) {
-        CHECK(r.url() == "https://localhost:5000/redirect");
+        CHECK(r.url() == "https://localhost:5443/redirect");
         CHECK(r.response_code() == 302);
         g.close();
       },
@@ -100,9 +100,9 @@ TEST_CASE("Max redirects") {
   g.set_verify_peer(false);
 
   g.get(
-      "https://localhost:5000/double_redirect",
+      "https://localhost:5443/double_redirect",
       [&g](const Request &r) {
-        CHECK(r.url() == "https://localhost:5000/double_redirect");
+        CHECK(r.url() == "https://localhost:5443/double_redirect");
         CHECK(r.response_code() == 302);
         g.close();
       },
@@ -116,10 +116,10 @@ TEST_CASE("Basic manual POST request") {
   g.set_verify_peer(false);
 
   auto r = std::make_shared<Request>(&g, Request::Method::POST,
-                                     "https://localhost:5000/post");
+                                     "https://localhost:5443/post");
   r->request("ABCD");
   r->on_complete([&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/post");
+    CHECK(r.url() == "https://localhost:5443/post");
     CHECK(r.response_code() == 200);
     CHECK(r.response() == "ABCD");
     g.close();
@@ -135,10 +135,10 @@ TEST_CASE("Basic manual PUT request") {
   g.set_verify_peer(false);
 
   auto r = std::make_shared<Request>(&g, Request::Method::PUT,
-                                     "https://localhost:5000/put");
+                                     "https://localhost:5443/put");
   r->request("ABCD");
   r->on_complete([&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/put");
+    CHECK(r.url() == "https://localhost:5443/put");
     CHECK(r.response_code() == 200);
     CHECK(r.response() == "ABCD");
     g.close();
@@ -154,9 +154,9 @@ TEST_CASE("Basic manual HEAD request") {
   g.set_verify_peer(false);
 
   auto r = std::make_shared<Request>(&g, Request::Method::HEAD,
-                                     "https://localhost:5000/");
+                                     "https://localhost:5443/");
   r->on_complete([&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/");
+    CHECK(r.url() == "https://localhost:5443/");
     CHECK(r.response_code() == 200);
     CHECK(r.response().empty());
     g.close();
@@ -172,9 +172,9 @@ TEST_CASE("Basic manual OPTIONS request") {
   g.set_verify_peer(false);
 
   auto r = std::make_shared<Request>(&g, Request::Method::OPTIONS,
-                                     "https://localhost:5000/");
+                                     "https://localhost:5443/");
   r->on_complete([&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/");
+    CHECK(r.url() == "https://localhost:5443/");
     CHECK(r.response_code() == 200);
     CHECK(r.response_headers()["allow"].find("HEAD") != std::string::npos);
     CHECK(r.response_headers()["allow"].find("OPTIONS") != std::string::npos);
@@ -193,9 +193,9 @@ TEST_CASE("Basic manual DELETE request") {
   g.set_verify_peer(false);
 
   auto r = std::make_shared<Request>(&g, Request::Method::DELETE,
-                                     "https://localhost:5000/delete");
+                                     "https://localhost:5443/delete");
   r->on_complete([&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/delete");
+    CHECK(r.url() == "https://localhost:5443/delete");
     CHECK(r.response_code() == 200);
 
     g.close();
@@ -210,9 +210,9 @@ TEST_CASE("Basic simple POST request") {
   Client g{};
   g.set_verify_peer(false);
 
-  g.post("https://localhost:5000/post", "ABCD", "text/plain",
+  g.post("https://localhost:5443/post", "ABCD", "text/plain",
          [&g](const Request &r) {
-           CHECK(r.url() == "https://localhost:5000/post");
+           CHECK(r.url() == "https://localhost:5443/post");
            CHECK(r.response_code() == 200);
            CHECK(r.response() == "ABCD");
            g.close();
@@ -225,9 +225,9 @@ TEST_CASE("Basic simple PUT request") {
   Client g{};
   g.set_verify_peer(false);
 
-  g.put("https://localhost:5000/put", "ABCD", "text/plain",
+  g.put("https://localhost:5443/put", "ABCD", "text/plain",
         [&g](const Request &r) {
-          CHECK(r.url() == "https://localhost:5000/put");
+          CHECK(r.url() == "https://localhost:5443/put");
           CHECK(r.response_code() == 200);
           CHECK(r.response() == "ABCD");
           g.close();
@@ -240,8 +240,8 @@ TEST_CASE("Basic simple HEAD request") {
   Client g{};
   g.set_verify_peer(false);
 
-  g.head("https://localhost:5000/", [&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/");
+  g.head("https://localhost:5443/", [&g](const Request &r) {
+    CHECK(r.url() == "https://localhost:5443/");
     CHECK(r.response_code() == 200);
     CHECK(r.response().empty());
     g.close();
@@ -254,8 +254,8 @@ TEST_CASE("Basic simple OPTIONS request") {
   Client g{};
   g.set_verify_peer(false);
 
-  g.options("https://localhost:5000/", [&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/");
+  g.options("https://localhost:5443/", [&g](const Request &r) {
+    CHECK(r.url() == "https://localhost:5443/");
     CHECK(r.response_code() == 200);
     CHECK(r.response_headers()["allow"].find("HEAD") != std::string::npos);
     CHECK(r.response_headers()["allow"].find("OPTIONS") != std::string::npos);
@@ -271,8 +271,8 @@ TEST_CASE("Basic simple DELETE request") {
   Client g{};
   g.set_verify_peer(false);
 
-  g.delete_("https://localhost:5000/delete", [&g](const Request &r) {
-    CHECK(r.url() == "https://localhost:5000/delete");
+  g.delete_("https://localhost:5443/delete", [&g](const Request &r) {
+    CHECK(r.url() == "https://localhost:5443/delete");
     CHECK(r.response_code() == 200);
 
     g.close();
diff --git a/tests/testserver.py b/tests/testserver.py
index 0916498348cce96ab8dc3850fc0356f0d2ed3262..3ddb5a45fc3b715a3c9b3429a968306145be204d 100755
--- a/tests/testserver.py
+++ b/tests/testserver.py
@@ -34,7 +34,7 @@ def test_delete():
 
 if __name__ == "__main__":
     if len(sys.argv) >= 2:
-        app.run(debug=True, ssl_context=(sys.argv[1]+'/cert.pem', sys.argv[1]+'/key.pem'))
+        app.run(debug=True, ssl_context=(sys.argv[1]+'/cert.pem', sys.argv[1]+'/key.pem'), port=5443)
     else:
         app.run(debug=True)