Ubuntu Systemd – Puma with Socket Activation Errno::EADDRINUSE

rubyruby-on-railssocketsystemdUbuntu

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:

Best Answer

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.

Related Topic