The erlang docker images now include rebar3, so it’s very easy to get started with a new project (the name needs to be an erlang term, so underscores not dashes!):
$ docker run -v $PWD:/app/foo_app -w /app erlang:25 rebar3 new release foo_app
Unable to find image 'erlang:25' locally
25: Pulling from library/erlang
e756f3fdd6a3: Pull complete
...
Digest: sha256:4eafc58e4475a7be2416af55ea142a7cd00c14b6ec2490a38db3a0869efde7e4
Status: Downloaded newer image for erlang:25
===> Writing foo_app/apps/foo_app/src/foo_app_app.erl
===> Writing foo_app/apps/foo_app/src/foo_app_sup.erl
===> Writing foo_app/apps/foo_app/src/foo_app.app.src
===> Writing foo_app/rebar.config
===> Writing foo_app/config/sys.config
===> Writing foo_app/config/vm.args
===> Writing foo_app/.gitignore
===> Writing foo_app/LICENSE
===> Writing foo_app/README.md
As dockerd is running as root, you then need to chown
the generated files (you can run the docker cmd as the current user, but that didn’t go well when I tried it).
Then you need a Dockerfile:
# Build stage 0
FROM erlang:25-alpine
RUN apk add --no-cache git
# Set working directory
RUN mkdir /buildroot
WORKDIR /buildroot
# Copy our Erlang test application
COPY rebar.config .
COPY apps/ apps/
COPY config/ config/
# And build the release
RUN rebar3 as prod release
# Build stage 1
FROM alpine
# Install some libs
RUN apk add --no-cache openssl ncurses-libs libstdc++
# Install the released application
COPY --from=0 /buildroot/_build/prod/rel/foo_app /foo_app
# Expose relevant ports
EXPOSE 8080
CMD ["/foo_app/bin/foo_app", "foreground"]
At this point, you should be able to build the image:
$ docker build -t foo_app .
Sending build context to Docker daemon 104.4kB
Step 1/13 : FROM erlang:25-alpine
25-alpine: Pulling from library/erlang
2408cc74d12b: Already exists
1e90e213ba89: Pull complete
Digest: sha256:1fdd18a383206eeba257f18c5dd22d7f381942eda4cd76a88e36a1d3247c4130
Status: Downloaded newer image for erlang:25-alpine
---> 8f1437fc7749
Step 2/13 : RUN apk add --no-cache git
---> Running in 1af099a684a7
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
(1/6) Installing brotli-libs (1.0.9-r6)
(2/6) Installing nghttp2-libs (1.47.0-r0)
(3/6) Installing libcurl (7.83.1-r1)
(4/6) Installing expat (2.4.8-r0)
(5/6) Installing pcre2 (10.40-r0)
(6/6) Installing git (2.36.1-r0)
Executing busybox-1.35.0-r13.trigger
OK: 23 MiB in 30 packages
Removing intermediate container 1af099a684a7
---> 99d4529dc490
Step 3/13 : RUN mkdir /buildroot
---> Running in 47daa05e777b
Removing intermediate container 47daa05e777b
---> ac097064f098
Step 4/13 : WORKDIR /buildroot
---> Running in 51479ca5c35a
Removing intermediate container 51479ca5c35a
---> 649f740b900b
Step 5/13 : COPY rebar.config .
---> 3fe63710afa6
Step 6/13 : COPY apps/ apps/
---> 91e46ef9dfc9
Step 7/13 : COPY config/ config/
---> 4194973d4e23
Step 8/13 : RUN rebar3 as prod release
---> Running in 7e7a2112d86e
===> Verifying dependencies...
===> Analyzing applications...
===> Compiling foo_app
===> Assembling release foo_app-0.1.0...
===> Release successfully assembled: _build/prod/rel/foo_app
Removing intermediate container 7e7a2112d86e
---> 91154e8634db
Step 9/13 : FROM alpine
latest: Pulling from library/alpine
2408cc74d12b: Already exists
Digest: sha256:686d8c9dfa6f3ccfc8230bc3178d23f84eeaf7e457f36f271ab1acc53015037c
Status: Downloaded newer image for alpine:latest
---> e66264b98777
Step 10/13 : RUN apk add --no-cache openssl ncurses-libs libstdc++
---> Running in 935857dec575
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/main/x86_64/APKINDEX.tar.gz
fetch https://dl-cdn.alpinelinux.org/alpine/v3.16/community/x86_64/APKINDEX.tar.gz
(1/5) Installing libgcc (11.2.1_git20220219-r2)
(2/5) Installing libstdc++ (11.2.1_git20220219-r2)
(3/5) Installing ncurses-terminfo-base (6.3_p20220521-r0)
(4/5) Installing ncurses-libs (6.3_p20220521-r0)
(5/5) Installing openssl (1.1.1o-r0)
Executing busybox-1.35.0-r13.trigger
OK: 9 MiB in 19 packages
Removing intermediate container 935857dec575
---> e62baafed2cd
Step 11/13 : COPY --from=0 /buildroot/_build/prod/rel/foo_app /foo_app
---> d768a4b774ea
Step 12/13 : EXPOSE 8080
---> Running in 11f013006a16
Removing intermediate container 11f013006a16
---> 0e347e1c2d46
Step 13/13 : CMD ["/foo_app/bin/foo_app", "foreground"]
---> Running in 7bf493256ded
Removing intermediate container 7bf493256ded
---> dc0f1632d27f
Successfully built dc0f1632d27f
Successfully tagged foo_app:latest
This is a good approach for building a final deployable image, but it’s a bit painful for local development, and doesn’t make the best use of the docker layer caching. You probably want to use a mounted vol instead.
You should now be able to run the application:
$ docker run -p 8080:8080 --rm foo_app
Exec: /foo_app/erts-13.0.1/bin/erlexec -noinput +Bd -boot /foo_app/releases/0.1.0/start -mode embedded -boot_var SYSTEM_LIB_DIR /foo_app/lib -config /foo_app/releases/0.1.0/sys.config -args_file /foo_app/releases/0.1.0/vm.args -- foreground
Root: /foo_app
/foo_app
Unfortunately Ctrl+C doesn’t work, so you need to docker kill
it.
This isn’t a web app, yet, so we need to add cowboy to the dependencies list in rebar.config
:
{deps, [
{cowboy,"2.9.0"}
]}.
I’m using the latest tag, but you might want a specific version. If you build again, you should see some extra steps:
Step 8/13 : RUN rebar3 as prod release
---> Running in 95791181399b
===> Verifying dependencies...
===> Fetching cowboy v2.9.0
===> Fetching cowlib v2.11.0
===> Fetching ranch v1.8.0
===> Analyzing applications...
===> Compiling cowlib
===> Compiling ranch
===> Compiling cowboy
===> Analyzing applications...
===> Compiling foo_app
===> Assembling release foo_app-0.1.0...
===> Release successfully assembled: _build/prod/rel/foo_app
You also need to add cowboy to the list in the app.src
file:
{applications,
[kernel,
stdlib,
cowboy
]},
You can then configure cowboy, in your _app
file:
start(_StartType, _StartArgs) ->
Port = 8080,
Dispatch = cowboy_router:compile([
{'_', [{"/ping", ping_handler, []}]}
]),
{ok, _} = cowboy:start_clear(my_http_listener,
[{port, Port}],
#{env => #{dispatch => Dispatch}}
),
foo_app_sup:start_link().
And create a handler:
-module(ping_handler).
-export([init/2]).
init(Req, State) ->
{ok, Req, State}.
This is the absolute minimum, and will return a 204 for any request:
$ curl -I -XGET "http://localhost:8080/ping"
HTTP/1.1 204 No Content
Or, for something a bit more realistic:
init(#{method := Method} = Req, _State) ->
handle_req(Method, Req).
handle_req(<<"GET">>, Req) ->
{ok, text_plain(Req, <<"pong">>)};
handle_req(_Method, Req) ->
cowboy_req:reply(404, Req).
text_plain(Request, ResponseBody) ->
ResponseHeaders = #{
<<"content-type">> => <<"text/plain">>
},--
cowboy_req:reply(200, ResponseHeaders, ResponseBody, Request).
This will return some text, for a GET:
$ curl "http://localhost:8080/ping"
pong
And a 404 for any other HTTP method:
$ curl -I -XPOST "http://localhost:8080/ping"
HTTP/1.1 404 Not Found
content-length: 0
date: Fri, 17 Jun 2022 11:54:35 GMT
server: Cowboy