diff --git a/apps/consul_proxy/test/common_steps.erl b/apps/consul_proxy/test/common_steps.erl
index 8a28a1d5694137952cf5a77cf5ba87e8274d17a9..55053660b4e98cc017a16b0d611a41a2fee07806 100644
--- a/apps/consul_proxy/test/common_steps.erl
+++ b/apps/consul_proxy/test/common_steps.erl
@@ -3,13 +3,11 @@
 
 %% Exported functions
 -export([
-    setup_feature/1,
+    setup_feature/2,
     setup_scenario/2,
     teardown_feature/1,
     teardown_scenario/1,
-    given/2,
-    'when'/2,
-    then/2
+    given/2
 ]).
 
 -include("consul_proxy.hrl").
@@ -18,85 +16,45 @@
 %%====================================================================
 %% Exported functions
 %%====================================================================
-setup_feature(_Tokens) ->
+setup_feature(_Tokens, State) ->
     Env = lists:map(
         fun(Env) ->
             [K, V] = binary:split(list_to_binary(Env), <<"=">>),
             {K, V}
         end, os:getenv()),
-    lager:debug("~p", [Env]),
-    application:ensure_all_started(hackney),
-    {ok, Pid} = dockerl:start_link(socket, <<"/var/run/docker.sock">>),
     IPAddress = list_to_binary(inet:ntoa(local_ip_v4())),
-    {ok, #{os_env => Env, test_env => [{<<"ip_address">>, IPAddress}], applications => [], containers => [], dockerl_pid => Pid}}.
+    {ok, State#{{?MODULE, env} => Env, {?MODULE, test_env} => [{<<"ip_address">>, IPAddress}], {?MODULE, applications} => []}}.
 
-teardown_feature(#{dockerl_pid := Pid, containers := Containers, applications := Applications}) ->
+teardown_feature(State = #{{?MODULE, applications} := Applications}) ->
     [application:stop(Application) || Application <- lists:reverse(Applications)],
-    lists:foreach(
-        fun({Name, Id}) ->
-            ok = dockerl:stop_container(Pid, Id),
-            lager:notice("Docker container ~s stopped", [Name]),
-            ok = dockerl:remove_container(Pid, Id),
-            lager:notice("Docker container ~s removed", [Name])
-        end, Containers),
-    ok;
-teardown_feature(_State) ->
-    ok.
+    {ok, State};
+teardown_feature(State) ->
+    {ok, State}.
 
 setup_scenario(_Tokens, State) ->
     {ok, State}.
 
-teardown_scenario(_State) ->
-    ok.
+teardown_scenario(State) ->
+    {ok, State}.
 
 %% noinspection ErlangUnboundVariable
-given([<<"a">>, <<"docker">>, <<"container">>, <<"named">>, Name, <<"running">>, Image, <<"with">>, <<"commands:">>, {docstring, Args}], State = #{dockerl_pid := Pid, containers := Containers}) ->
-    InterpolatedImage = consul_proxy_utils:string_interpolate(Image, maps:get(os_env, State, []) ++ maps:get(test_env, State, []), []),
-    {ok, _} = dockerl:pull_image(Pid, Image),
-    {ok, Id} = dockerl:create_container(Pid, Image,
-        #{
-            'Tty' => true,
-            'Cmd' => binary:split(Args, <<" ">>, [trim_all, global]),
-            'PublishAllPorts' => true
-        }),
-    lager:notice("Docker container ~s running ~s created: ~s", [Name, InterpolatedImage, Id]),
-    ok = dockerl:start_container(Pid, Id),
-    lager:notice("Docker container ~s started", [Id]),
-    {ok, State#{containers => [{Name, Id} | Containers]}};
-
-given("$Name logs match $Pattern", State = #{dockerl_pid := Pid, containers := Containers}) ->
-    Id = proplists:get_value(Name, Containers),
-    {ok, Stream} = dockerl:container_logs(Pid, Id),
-    ok = match_logs(Stream, Pattern),
-    {ok, State};
-
-given("consul_proxy is connected to $Name port $Port", State = #{dockerl_pid := Pid, containers := Containers, applications := Applications}) ->
-    Id = proplists:get_value(Name, Containers),
-    {ok, Ports} = dockerl_utils:get_ports(Pid, Id),
-    [{_, IntPort}] = maps:get(Port, Ports),
-    BinPort = integer_to_binary(IntPort),
-    BinAddress = case os:type() of
-                     {unix, darwin} -> %% Todo: remove this hack once routing is fixed in Docker for Mac
-                         <<"127.0.0.1">>;
-                     _ ->
-                         {ok, Gateway} = dockerl_utils:get_gateway(Pid, Id),
-                         list_to_binary(inet:ntoa(Gateway))
-                 end,
+given("consul_proxy is connected to $Name container port $Port", State = #{{?MODULE, applications} := Applications}) ->
+    {BinAddress, BinPort} = dockerl_steps:get_port(Name, Port, State),
     URL = binary_to_list(<<"http://", BinAddress/binary, ":", BinPort/binary>>),
     lager:notice("Consul URL: ~s", [URL]),
     ok = application:set_env(consul_proxy, consul_urls, URL, [{timeout, infinity}, {persistent, true}]),
     case application:ensure_all_started(consul_proxy) of
         {ok, Started} ->
             lager:notice("Started ~p applications: ~p on ~p~n", [erlang:length(Started), Started, node()]),
-            {ok, HttpcPid} = inets:start(httpc, [{profile, consul_proxied}]),
+            {ok, _} = inets:start(httpc, [{profile, consul_proxied}]),
             httpc:set_options([{proxy, {{"localhost", 8080}, []}}], consul_proxied),
-            {ok, State#{consul_proxied_httpc => HttpcPid, applications => Applications ++ Started}};
+            {ok, State#{{?MODULE, applications} => Applications ++ Started}};
         {error, Reason} ->
             lager:error("~p", [Reason]),
             {error, Reason}
     end;
 
-given("a UDP listener registered as $ServiceName", State = #{test_env := TestEnv}) ->
+given("a UDP listener registered as $ServiceName", State = #{{?MODULE, test_env} := TestEnv}) ->
     {ok, Socket} = gen_udp:open(0, [binary, {ip, {0, 0, 0, 0}}, {active, false}]),
     {ok, IntPort} = inet:port(Socket),
     BinPort = integer_to_binary(IntPort),
@@ -107,7 +65,7 @@ given("a UDP listener registered as $ServiceName", State = #{test_env := TestEnv
     spawn(fun() -> udp_receive(Socket) end),
     {ok, State#{test_env => [{<<ServiceName/binary, "_port">>, BinPort} | TestEnv]}};
 
-given("an HTTP Server with root $Root and handlers $Handlers registered as $ServiceName", State = #{test_env := TestEnv}) ->
+given("an HTTP Server with root $Root and handlers $Handlers registered as $ServiceName", State = #{{?MODULE, test_env} := TestEnv}) ->
     Modules = [binary_to_atom(Handler, utf8) || Handler <- binary:split(Handlers, <<",">>, [trim_all, global])],
     ServiceConfig = [
         {bind_address, "0.0.0.0"},
@@ -130,7 +88,6 @@ given("an HTTP Server with root $Root and handlers $Handlers registered as $Serv
 
 given([<<"consul">>, <<"client">>, <<"has">>, <<"set">>, Key, <<"to:">>, {docstring, Value}], State) ->
     InterpolatedValue = consul_proxy_utils:string_interpolate(Value, maps:get(os_env, State, []) ++ maps:get(test_env, State, []), []),
-    lager:debug("~p -> ~p", [Key, InterpolatedValue]),
     case consul_client:set(Key, InterpolatedValue) of
         {ok, true} ->
             {ok, State};
@@ -153,42 +110,11 @@ given("consul client has loaded values from $Filename", State) ->
             end
         end, KVData),
     lager:debug("~p keys set", [length(R)]),
-    {ok, State};
-
-given(Tokens, _State) ->
-    lager:warning("Unhandled tokens: ~p", [Tokens]),
-    {error, lists:flatten(io_lib:format("Unhandled tokens: ~p", [Tokens]))}.
-
-'when'(Tokens, _State) ->
-    lager:warning("Unhandled tokens: ~p", [Tokens]),
-    {error, lists:flatten(io_lib:format("Unhandled tokens: ~p", [Tokens]))}.
-
-then(Tokens, _State) ->
-    lager:warning("Unhandled tokens: ~p", [Tokens]),
-    {error, lists:flatten(io_lib:format("Unhandled tokens: ~p", [Tokens]))}.
+    {ok, State}.
 
 %%===================================================================
 %% Internal functions
 %%===================================================================
-
-match_logs(Stream, Pattern) ->
-    receive
-        done ->
-            {error, no_match};
-        Msg ->
-            case re:run(Msg, Pattern) of
-                {match, _} ->
-                    Stream ! stop,
-                    ok;
-                _ ->
-                    match_logs(Stream, Pattern)
-            end
-    after
-        10000 ->
-            Stream ! stop,
-            {error, timeout}
-    end.
-
 udp_receive(Socket) ->
     case gen_udp:recv(Socket, 0, 500) of
         {ok, {Address, Port, Packet}} ->
diff --git a/apps/consul_proxy/test/feature_consul_proxy.erl b/apps/consul_proxy/test/feature_consul_proxy.erl
index 3ba9c3537d638a52749ab721ebaa0405b36515fb..7709eb5969b21a1073d39d1c9d7c94cdcc16fc81 100644
--- a/apps/consul_proxy/test/feature_consul_proxy.erl
+++ b/apps/consul_proxy/test/feature_consul_proxy.erl
@@ -19,20 +19,21 @@
 %% Exported functions
 %%====================================================================
 setup_feature(Tokens) ->
-    common_steps:setup_feature(Tokens).
+    Pipeline = build_pipeline([common_steps, dockerl_steps]),
+    Pipeline(?FUNCTION_NAME, Tokens, #{{?MODULE, pipeline} => Pipeline}).
 
-teardown_feature(State) ->
-    common_steps:teardown_feature(State).
+teardown_feature(State = #{{?MODULE, pipeline} := Pipeline}) ->
+    Pipeline(?FUNCTION_NAME, no_tokens, State).
 
-setup_scenario(Tokens, State) ->
-    common_steps:setup_scenario(Tokens, State).
+setup_scenario(Tokens, State = #{{?MODULE, pipeline} := Pipeline}) ->
+    Pipeline(?FUNCTION_NAME, Tokens, State).
 
-teardown_scenario(State) ->
-    common_steps:teardown_scenario(State).
+teardown_scenario(State = #{{?MODULE, pipeline} := Pipeline}) ->
+    Pipeline(?FUNCTION_NAME, no_tokens, State).
 
 %% noinspection ErlangUnboundVariable
-given(Tokens, State) ->
-    common_steps:given(Tokens, State).
+given(Tokens, State = #{{?MODULE, pipeline} := Pipeline}) ->
+    Pipeline(?FUNCTION_NAME, Tokens, State).
 
 %% noinspection ErlangUnboundVariable
 'when'("the consul client lists nodes", State) ->
@@ -67,8 +68,8 @@ given(Tokens, State) ->
     },
     {ok, State#{response => Response}};
 
-'when'(Tokens, State) ->
-    common_steps:'when'(Tokens, State).
+'when'(Tokens, State = #{{?MODULE, pipeline} := Pipeline}) ->
+    Pipeline(?FUNCTION_NAME, Tokens, State).
 
 %% noinspection ErlangUnboundVariable
 then("the result should have $Count $Unit", State = #{result := Result}) ->
@@ -98,9 +99,40 @@ then("the status code should be $ExpectedCode", State = #{response := #{status_c
             {error, lists:flatten(io_lib:format("Received ~p", [StatusCode]))}
     end;
 
-then(Tokens, State) ->
-    common_steps:then(Tokens, State).
+then(Tokens, State = #{{?MODULE, pipeline} := Pipeline}) ->
+    Pipeline(?FUNCTION_NAME, Tokens, State).
 
 %%===================================================================
 %% Internal functions
 %%===================================================================
+build_pipeline(Modules) ->
+    fun(Step, Tokens, State) ->
+        {Result, Matches} = lists:foldl(
+            fun
+                (Module, {{ok, StateIn}, Matches}) ->
+                    try
+                        case Tokens of
+                            no_tokens ->
+                                {Module:Step(StateIn), Matches + 1};
+                            _ ->
+                                {Module:Step(Tokens, StateIn), Matches + 1}
+                        end
+                    catch
+                        error:undef ->
+                            {{ok, StateIn}, Matches};
+                        error:function_clause ->
+                            {{ok, StateIn}, Matches};
+                        Class:ExceptionPattern ->
+                            {{Class, ExceptionPattern, erlang:get_stacktrace()}, Matches}
+                    end;
+                (_Module, Error) ->
+                    Error
+            end, {{ok, State}, 0}, Modules),
+        case Matches of
+            0 ->
+                lager:warning("Unhandled tokens: ~p", [Tokens]),
+                {error, lists:flatten(io_lib:format("Unhandled tokens: ~p", [Tokens]))};
+            _ ->
+                Result
+        end
+    end.
diff --git a/features/consul_proxy.feature b/features/consul_proxy.feature
index 04d0bffa21c884e6abce5507500c0e5465e978f8..b98831df7763f2dd6b056c8fb67474d4d78d48e1 100644
--- a/features/consul_proxy.feature
+++ b/features/consul_proxy.feature
@@ -8,8 +8,8 @@ Feature: Proxy functionality
     """
     agent -dev -ui-dir /ui -client 0.0.0.0
     """
-    And consul logs match "Synced service 'consul'"
-    And consul_proxy is connected to consul port 8500/tcp
+    And consul container logs match "Synced service 'consul'"
+    And consul_proxy is connected to consul container port 8500/tcp
     And a UDP listener registered as logstash
     And an HTTP Server with root "apps/consul_proxy/priv" and handlers "eunit_www" registered as eunit
     And consul client has set consul_proxy/watchers/kv to:
diff --git a/rebar.config b/rebar.config
index 1694dcb22758f7816c058f942e9fa9ac387b3a82..0e11059844b222a911b7410e7a1221b20833a3ac 100644
--- a/rebar.config
+++ b/rebar.config
@@ -40,7 +40,7 @@
     {test, [
         {deps, [
             {gurka, "0.1.7"},
-            {dockerl, {git, "https://gitlab.hedenstroem.com/erlang-ninja/dockerl.git", {tag, "0.1.0"}}}
+            {dockerl, {git, "https://gitlab.hedenstroem.com/erlang-ninja/dockerl.git", {tag, "0.2.0"}}}
         ]},
         {eunit_opts, [{report, {eunit_surefire, [{dir, "_build/test"}]}}]},
         {erl_opts, [debug_info, nowarn_unused_vars]}