0

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:

  • 2
    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 at 22:48
  • 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 at 7: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 at 12:40
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.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.