diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
new file mode 100644
index 0000000000000000000000000000000000000000..34a895cfd1804f379fefdfdeb60b54f27592b543
--- /dev/null
+++ b/.gitlab-ci.yml
@@ -0,0 +1,37 @@
+image: hedenstroem/gitlab-builder-erlang:19.1
+
+cache:
+  paths:
+    - .rebar3
+
+before_script:
+  - export PATH=${ERL_HOME}/bin:$PATH
+
+stages:
+  - test
+  - publish
+
+test:
+  stage: test
+  script:
+    - rebar3 test
+    - coverage.escript _build/test/cover/eunit.coverdata
+
+hex_publish:
+  stage: publish
+  only:
+    - /^\d+[.]\d+[.]\d+$/ # Only publish HEAD tagged with semantic version
+  script:
+    - mkdir -p ~/.hex && printf "{key,<<\"$HEX_KEY\">>}.\n{username,<<\"$HEX_USERNAME\">>}.\n" > ~/.hex/hex.config
+    - echo "Y" | rebar3 hex publish
+
+aws_s3:
+  stage: publish
+  only:
+    - /^\d+[.]\d+[.]\d+$/ # Only publish HEAD tagged with semantic version
+  script:
+    - rebar3 edoc
+    - aws s3 cp doc s3://s3.erlang.ninja/tsuru/$CI_BUILD_REF_NAME/ --recursive
+    - rebar3 as production do tar
+    - aws s3 cp _build/production/rel/tsuru/tsuru-$CI_BUILD_REF_NAME.tar.gz s3://s3.erlang.ninja/tsuru/
+    - aws s3 cp s3://s3.erlang.ninja/tsuru/tsuru-$CI_BUILD_REF_NAME.tar.gz s3://s3.erlang.ninja/tsuru/tsuru-latest.tar.gz
diff --git a/README.md b/README.md
index 53f25e6e436a5d09e01d72b2d9f2b20196364260..08c1c4a890ed67cd7178d8d946b26619bc7d1080 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,8 @@
-dockerl
-=====
+DockERL
+=======
 
-An OTP application
+[![hex.pm version](https://img.shields.io/hexpm/v/dockerl.svg)](https://hex.pm/packages/dockerl)
 
-Build
------
+[![build status](https://gitlab.hedenstroem.com/erlang-ninja/dockerl/badges/master/build.svg)](https://gitlab.hedenstroem.com/erlang-ninja/dockerl)
 
-    $ rebar3 compile
+A simple Docker API client
diff --git a/config/sys.config b/config/sys.config
index 772ce429ae897cc441d1bec6477094bca9f9aaed..e07e626cbe0dd870b7eb21d6ceb374be4ee9660c 100644
--- a/config/sys.config
+++ b/config/sys.config
@@ -25,8 +25,5 @@
         {crash_log_date, "$D0"},
         {crash_log_count, 5},
         {error_logger_redirect, true}
-    ]},
-    {dockerl, [
-        {docker_host, "localhost"}
     ]}
 ].
diff --git a/rebar.config b/rebar.config
index e05acfa2d43a7bdd3f708a9dc8e3df1f1125bf9d..8caf04a7a316795ac9edd025113addf8aa18d43f 100644
--- a/rebar.config
+++ b/rebar.config
@@ -1,6 +1,7 @@
 {global_rebar_dir, ".rebar3"}.
 
 {plugins, [
+    rebar3_hex,
     rebar_alias,
     {rebar_cmd, "0.2.3"}
 ]}.
diff --git a/src/dockerl.app.src b/src/dockerl.app.src
index bcfa7011693e426dce5a49065f4beb634471bc09..c1b6458c0d74399f0d0ff213109e3143ba7d063e 100644
--- a/src/dockerl.app.src
+++ b/src/dockerl.app.src
@@ -11,7 +11,7 @@
     ]},
     {env, []},
     {modules, []},
-    {maintainers, []},
-    {licenses, []},
+    {maintainers, ["Erik Hedenstrom"]},
+    {licenses, ["MIT"]},
     {links, [{"GitLab", "https://gitlab.hedenstroem.com/erlang-ninja/dockerl"}]}
 ]}.
diff --git a/src/dockerl.erl b/src/dockerl.erl
index 07f5acb4bbfc0e78ddc2e03a2f2f5b7aa724e96c..09b95209268772198e199020d409a63248f96612 100644
--- a/src/dockerl.erl
+++ b/src/dockerl.erl
@@ -1,8 +1,3 @@
-%%%-------------------------------------------------------------------
-%% @doc dockerl public API
-%% @end
-%%%-------------------------------------------------------------------
-
 -module(dockerl).
 
 -behaviour(gen_server).
@@ -291,13 +286,13 @@ send_request({ok, ConnRef}, Method, Path, ReqBody) ->
         {<<"Accept">>, <<"application/json">>},
         {<<"Content-Type">>, <<"application/json">>}
     ],
-    lager:info("~p ~p", [Method, Path]),
+    lager:debug("~p ~p", [Method, Path]),
     try hackney:send_request(ConnRef, {Method, Path, ReqHeaders, ReqBody}) of
         {ok, 204, _Headers, _ConnRef} ->
             ok;
         {ok, Status, Headers, ConnRef} when 200 =< Status, Status < 300 ->
             parse_body(read_body(infinite, ConnRef, <<>>), Headers);
-        {ok, Status, Headers, ConnRef} when 400 =< Status, Status < 500 ->
+        {ok, Status, _Headers, ConnRef} when 400 =< Status, Status < 500 ->
             error_message(read_body(infinite, ConnRef, <<>>));
         {ok, 500, _Headers, _ConnRef} ->
             {error, server_error};
@@ -352,9 +347,9 @@ decode_response(_ContentType, Body) ->
 read_body(MaxLength, Ref, Body) when MaxLength =:= infinite; MaxLength > byte_size(Body) ->
     case hackney:stream_body(Ref) of
         {ok, Data} ->
-            lager:info("~p", [Data]),
             read_body(MaxLength, Ref, <<Body/binary, Data/binary>>);
         done ->
+            lager:debug("~p", [Body]),
             {ok, Body};
         {error, Reason} ->
             {error, Reason}
@@ -368,7 +363,7 @@ stream_body(Pid, Ref) ->
             stream_body(Pid, NewRef);
         {hackney_response, NewRef, {headers, _Headers}} ->
             stream_body(Pid, NewRef);
-        {hackney_response, NewRef, done} ->
+        {hackney_response, _NewRef, done} ->
             Pid ! done,
             ok;
         {hackney_response, NewRef, Chunk} ->
diff --git a/src/dockerl_utils.erl b/src/dockerl_utils.erl
new file mode 100644
index 0000000000000000000000000000000000000000..935d312eef0d9229a17f486e68d3c37e1bb88f43
--- /dev/null
+++ b/src/dockerl_utils.erl
@@ -0,0 +1,28 @@
+-module(dockerl_utils).
+
+-export([get_ports/2]).
+
+get_ports(ServerRef, Id) ->
+    case dockerl:inspect_container(ServerRef, Id) of
+        {ok, #{<<"NetworkSettings">> := #{<<"Ports">> := Ports}}} ->
+            ParsedPorts = maps:map(
+                fun(_, HostPorts) ->
+                    lists:foldl(
+                        fun(#{<<"HostIp">> := HostIp, <<"HostPort">> := HostPort}, AccIn) ->
+                            case inet:parse_address(binary_to_list(HostIp)) of
+                                {ok, Address} ->
+                                    [{Address, binary_to_integer(HostPort)} | AccIn];
+                                _ ->
+                                    AccIn
+                            end;
+                            (_, AccIn) ->
+                                AccIn
+                        end, [], HostPorts)
+                end, Ports),
+            {ok, ParsedPorts};
+        {ok, _} ->
+            {error, no_ports};
+        Error ->
+            Error
+    end.
+
diff --git a/test/dockerl_test.erl b/test/dockerl_test.erl
index 25665fa10a3b6596fd91bad4b10d9e7a06b93dcd..620f76686d785735b152ea56c7899479cc1ccd65 100644
--- a/test/dockerl_test.erl
+++ b/test/dockerl_test.erl
@@ -19,6 +19,7 @@ rpc_test_() ->
 %%%====================================================================
 start() ->
     lager:start(),
+    lager:set_loglevel(lager_console_backend, debug),
     hackney:start(),
     {ok, Pid} = dockerl:start_link(socket, <<"/var/run/docker.sock">>),
     Pid.
@@ -34,7 +35,7 @@ instantiator(Pid) ->
         version(Pid),
         {timeout, 60, pull_image(Pid)},
         {timeout, 60, start_container(Pid)},
-        remove_image(Pid)
+        {timeout, 60, remove_image(Pid)}
     ].
 
 %%%====================================================================
@@ -47,17 +48,19 @@ version(Pid) ->
     ?_assertMatch({ok, #{}}, dockerl:version(Pid)).
 
 pull_image(Pid) ->
-    ?_assertMatch({ok, _}, dockerl:pull_image(Pid, <<"nginx:latest">>)).
+    ?_assertMatch({ok, _}, dockerl:pull_image(Pid, <<"nginx:alpine">>)).
 
 remove_image(Pid) ->
-    ?_assertMatch({ok, _}, dockerl:remove_image(Pid, <<"nginx:latest">>)).
+    ?_assertMatch({ok, _}, dockerl:remove_image(Pid, <<"nginx:alpine">>)).
 
 start_container(Pid) ->
     fun() ->
-        {ok, Id} = dockerl:create_container(Pid, <<"foobar">>),
+        {ok, Id} = dockerl:create_container(Pid, <<"nginx:alpine">>, #{'PublishAllPorts' => true, 'Tty' => true}),
         ?assertMatch(ok, dockerl:start_container(Pid, Id)),
+        {ok, #{<<"80/tcp">> := [{_Address, Port}]}} = dockerl_utils:get_ports(Pid, Id),
+        spawn(fun() -> send_request(Port) end),
         {ok, Stream} = dockerl:container_logs(Pid, Id),
-        loop(Stream),
+        ?assertEqual(ok, match_logs(Stream, <<"eunit">>, 1)),
         ?assertMatch(ok, dockerl:stop_container(Pid, Id)),
         ?assertMatch(ok, dockerl:remove_container(Pid, Id))
     end.
@@ -65,14 +68,30 @@ start_container(Pid) ->
 %%%===================================================================
 %%% Internal functions
 %%%===================================================================
-loop(Stream) ->
+send_request(Port) ->
+    receive
+    after 1000 ->
+        BPort = integer_to_binary(Port),
+        hackney:request(get, <<"http://127.0.0.1:", BPort/binary>>, [{<<"User-Agent">>, <<"eunit">>}], <<>>, []),
+        send_request(Port)
+    end.
+
+match_logs(_Stream, _Pattern, 10) ->
+    {error, no_match};
+match_logs(Stream, Pattern, Count) ->
     receive
         done ->
-            ok;
+            {error, no_match};
         Msg ->
             lager:info("~p", [Msg]),
-            loop(Stream)
+            case re:run(Msg, Pattern) of
+                {match, _} ->
+                    ok;
+                _ ->
+                    match_logs(Stream, Pattern, Count + 1)
+            end
     after
         3000 ->
-            Stream ! stop
+            Stream ! stop,
+            {error, timeout}
     end.