1

I have a Rails 5 app, running on puma 3.12.1, MRI 2.6.2, and Ubuntu 18.04. It used to run with pumactl and a custom control script but I want to configure it properly with systemd, using socket activation to have zero-downtime deployment.

The problem is that the socket holds the port and puma wants to bind on the same one so it gives this error:

/opt/myapp/shared/vendor/ruby/2.6.0/gems/puma-3.12.1/lib/puma/binder.rb:273:in `initialize': 
Address already in use - bind(2) for "0.0.0.0" port 3000 (Errno::EADDRINUSE)

I set it up everything according to the puma docs and systemd docs.

I reloaded systemd config of course and tried to restart several times. I don't deeply understand how systemd does this socket activation but for me puma's error message seems reasonable :/

Maybe puma shouldn't try to bind on that port? But how can I communicate to puma to not bind to it but use systemd's forwarded stream?

My configuration:

$ cat /etc/systemd/system/puma.socket
[Unit]
Description=Puma HTTP Server Accept Sockets

[Socket]
ListenStream=0.0.0.0:3000

# Socket options matching Puma defaults
NoDelay=true
ReusePort=true
Backlog=1024

[Install]
WantedBy=sockets.target
$ cat /etc/systemd/system/puma.service
[Unit]
Description=API with Puma server
After=network.target
Requires=puma.socket

[Service]
Type=simple
WorkingDirectory=/opt/myapp/current
ExecStart=/opt/myapp/current/script/bootup_puma
SyslogIdentifier=api-puma
PIDFile=/opt/myapp/current/tmp/pids/puma.pid
Restart=no
TimeoutSec=30
User=ubuntu

[Install]
WantedBy=multi-user.target
$ cat script/bootup_puma
#!/bin/bash

# [setting up some envvars here]

bundle exec puma -C config/puma.rb
$ cat config/puma.rb
# frozen_string_literal: true

app_dir = File.expand_path("..", __dir__)

workers ENV.fetch("API__PUMA_WORKERS", 4).to_i
threads 1, ENV.fetch("RAILS_MAX_THREADS", 8).to_i

bind "tcp://0.0.0.0:#{ENV.fetch('PORT', 3000)}"

pidfile "#{app_dir}/tmp/pids/puma.pid"

directory ENV.fetch("API__PUMA_DIRECTORY") unless ENV.fetch("RAILS_ENV", "development") == "development"

As a desperate attempt, I also tried to use a unix socket instead of TCP but that ended up with a similar error despite I was being careful to not reference the socket through a symlink.

/opt/myapp/shared/vendor/ruby/2.6.0/gems/puma-3.12.1/lib/puma/binder.rb:367:in `add_unix_listener': 
There is already a server bound to: /opt/myapp/shared/tmp/puma.sock (RuntimeError)

Other useful resources I already went through:

jm3
  • 2,597
  • 1
  • 15
  • 10
vbalazs
  • 119
  • 4
  • 3
    You should not have a shell script that starts puma. Move everything directly into the systemd `.service` unit and call puma (or a binstub) directly. – Michael Hampton Mar 26 '19 at 22:48
  • 1
    Thanks @MichaelHampton , I cannot accept it as a solution since you wrote it as a comment but it was indeed the issue. Thanks for taking the time to review my config and pointing it out! Systemd is still like a black box for me, I'm not confident in how this socket activation works :| – vbalazs Mar 29 '19 at 07:34
  • I don't care that much about points, I have enough of them. It's just about helping people find their solutions. – Michael Hampton Mar 29 '19 at 12:40

1 Answers1

0

The problem was the intermediate shell script bootup_puma as Micheal Hampton pointed out in the comments.

Changing the service to ExecStart=/opt/myapp/current/bin/puma -C config/puma.rb and providing the environment with the EnvironmentFile directives solved the issue.

vbalazs
  • 119
  • 4
  • 1
    Damn, we need an intermediate shell script because of SELinux executable labelling. – lzap May 21 '20 at 09:38