What's the best way to serve restricted ports (e.g. 80, 443) with Phoenix?

Hi there, I’m working through my first release with elixir/phoenix. I’ve built a release with distillery and found that it crashes when I start it since it’s trying to access port 80 and linux restricts ports under 1024. Starting and running the server as root would work but I’m not going to do that.

In the past, working with other stacks the solutions I’ve used have been:

  1. Use apache/nginx which do have elevated permissions to start but then drop them.

  2. Use rails or node (or what ever I’m building with) on a higher port and run nginx in front as a proxy. (though Jose has said there’s usually no need to run Phoenix behind nginx)

  3. Use IP tables (and break ipv6)

Based on what I’ve seen online, the best solution may be to use setcap to give the binary permission to use the port. However I’ve tried that on erl, mix, iex and elixir and still can’t get the server to bind to port 80.

$ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/lib/erlan/bin/erl
$ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/lib/elixir/bin/mix
$ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/lib/elixir/bin/iex
$ sudo setcap CAP_NET_BIND_SERVICE=+eip /usr/local/lib/elixir/bin/elixir

$ MIX_ENV=prod PORT=80 mix phoenix.server
06:10:14.161 [error] Failed to start Ranch listener Myapp.Endpoint.HTTP in :ranch_tcp:listen([port: 80]) for reason :eacces (permission denied)

Any suggestions?

4 Likes

The executable that’s actually opening the port is the BEAM VM. Start iex in one window, then run ps aux | grep beam in another to see the details of the process. In my case, the executable is /usr/lib/erlang/erts-7.2/bin/beam.smp. Try setting the capabilities on that file instead.

Update: confirmed on Ubuntu 14.04.

$ sudo setcap 'cap_net_bind_service=+ep' /usr/lib/erlang/erts-7.2/bin/beam.smp
$ iex
Erlang/OTP 18 [erts-7.2] [source] [64-bit] [smp:4:4] [async-threads:10] [hipe] [kernel-poll:false]

Interactive Elixir (1.2.0) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)> :gen_tcp.listen(443, [])
{:ok, #Port<0.1426>}
iex(2)>
11 Likes

If you only have one server, I personally am a fan of the nginx reverse proxy approach, kind of simple to setup. With it, you could also run multiple instances of your app listening on different ports and have nginx distribute incoming requests, it would then be easy in case you need rolling upgrades (which in Elixir might not be necessary). It would also be easier if you want to have multiple Elixir apps binding to different ports but want to use the same domain, for example.

But is it the best way? I don’t know :smile: Looking for some information too in case someone with more experience chimes in.

Fantastic! This works for starting the server manually. Still get [error] Failed to start Ranch listener Myapp.Endpoint.HTTP in :ranch_tcp:listen([port: 80]) for reason :eacces (permission denied) when running the script created by distillery, but this is progress!

I haven’t used Exrm or Distillery for a while, but I think it typically bundles the Erlang runtime system. That means you need to set the capabilities on the BEAM binary that’s included in your release.

…and probably after it’s been deployed :slight_smile: I suspect that tar doesn’t preserve capabilities on archived files.

1 Like

Got it! I had to use setcap on a file distillery created called run_erl. On my system it was:
sudo setcap CAP_NET_BIND_SERVICE=+eip /home/ubuntu/myapp/_build/prod/rel/myapp/erts-8.1/bin/run_erl

Now I can start the web server from its run script and with just sudo start myapp (since I’d already created the /etc/init/myapp.conf script)

Wow this was a bit of a rabbit but it should be easier for the next person searching on google. Thanks @voltone

I’ll definitely keep your idea in mind too, @bobbypriambodo. Nginx is always a possibility in the future if I need it for filtering spammy requests to “admin.php” and other non-existent URLs that seem to be popular for bots to hit.

8 Likes

On FreeBSD I just bind ports 80 and 443 to ports 8080 and 8443, respectively, with pf. And start the app without any need for sudo. I think it’s also possible on Linux, albeit with another firewall.

1 Like

This worked for me:
sudo setcap ‘cap_net_bind_service=+ep’ /home/elixir_deployer/myapp/erts-8.3/bin/beam

Then I was able to logout and deploy with a non-root user (elixir_deployer in this case)

The below failed for me for reference:
sudo setcap CAP_NET_BIND_SERVICE=+eip /home/elixir_deployer/myapp/erts-8.3/bin/run_erl
sudo setcap ‘cap_net_bind_service=+ep’ /home/elixir_deployer/myapp/erts-8.3/bin/run_erl
sudo setcap ‘cap_net_bind_service=+ep’ /home/elixir_deployer/myapp/erts-8.3/bin/beam.smp
sudo setcap ‘cap_net_bind_service=+ep’ /home/elixir_deployer/myapp/bin/myapp

For this configuration:

alexandrubagu@alexandrubagu:~/devel$ uname -a
Linux alexandrubagu 4.10.0-20-generic #22-Ubuntu SMP Thu Apr 20 09:22:42 UTC 2017 x86_64 x86_64 x86_64 GNU/Linux
alexandrubagu@alexandrubagu:~/devel$ cat /etc/*-release
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=17.04
DISTRIB_CODENAME=zesty
DISTRIB_DESCRIPTION="Ubuntu 17.04"
NAME="Ubuntu"
VERSION="17.04 (Zesty Zapus)"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 17.04"
VERSION_ID="17.04"
HOME_URL="https://www.ubuntu.com/"
SUPPORT_URL="https://help.ubuntu.com/"
BUG_REPORT_URL="https://bugs.launchpad.net/ubuntu/"
PRIVACY_POLICY_URL="https://www.ubuntu.com/legal/terms-and-policies/privacy-policy"
VERSION_CODENAME=zesty
UBUNTU_CODENAME=zesty
alexandrubagu@alexandrubagu:~/devel$ erl
Erlang/OTP 20 [RELEASE CANDIDATE 2] [erts-9.0] [source] [64-bit] [smp:1:1] [ds:1:1:10] [async-threads:10] [hipe] [kernel-poll:false]

Eshell V9.0  (abort with ^G)

The following command works:
sudo setcap 'cap_net_bind_service=+ep' /HOME/myapp/erts-8.3/bin/beam.smp

1 Like

This also worked for me.

However, a not for those that follow. After a server update/dist-upgrade, I had to redo this command. Using Ubuntu16.04`.

On Ubuntu you can alternatively use authbind, which lets an administrator grant permissions to bind to specific ports by user/group. This means the permission is not bound to any particular application binary, and therefore persists when the binary is updated.

5 Likes

Excellent! Will give switching over to this a try.

Actually I’m a little put off by authbind not having ipv6 support. Will probably stick with setcap for now and handle update case.

1 Like