How unicorn talks to nginx - an introduction to unix sockets in Ruby

Ruby application servers are typically used together with a web server like nginx. When user requests a page from your Rails app, nginx delegates the request to the application server. But how exactly does that work? How does nginx talk with unicorn?

One of the most efficient options is to use unix sockets. Let's see how they work! In this post we'll start with the basics of sockets, and end by creating our own simple application server that is proxied by nginx.

Sockets allow programs to talk with each other as if they were writing to or reading from a file. Sockets allow programs to talk with each other as if they were writing to or reading from a file. In this example, Unicorn creates the socket and monitors it for connections. Nginx can then connect to the socket and talk with Unicorn.

What's a unix socket?

Unix sockets let one program talk to another in a way that kind of resembles working with files. They're a type of IPC, or inter-process communication.

To be accessible via a socket, your program first creates a socket and "saves" it to disk, just like a file. It monitors the socket for incoming connections. When it receives one, it uses standard IO methods to read and write data.

Ruby provides everything you need to work with unix sockets via a couple of classes:

  • UNIXServer - It creates the socket, saves it to disk,  and lets you monitor it for new connections.

  • UNIXSocket - Open existing sockets for IO.

NOTE: Other kinds of sockets exist. Most notably TCP sockets. But this post only deals with unix sockets. How do you tell the difference? Unix sockets have file names.

The Simplest Socket

We're going to look at two little programs.

The first is is the "server." It simply creates an instance of the UnixServer class, then uses server.accept to wait for a connection. When it receives a connection, it exchanges a greeting.

It's worth noting that both the accept and readline methods block program execution until they receive what they're waiting for.

require "socket"

server ='/tmp/simple.sock')

puts "==== Waiting for connection"
socket = server.accept

puts "==== Got Request:"
puts socket.readline

puts "==== Sending Response"
socket.write("I read you loud and clear, good buddy!")


So we have a server. Now all we need is a client.

In the example below, we open the socket created by our server. Then we use normal IO methods to send and receive a greeting.

require "socket"

socket ='/tmp/simple.sock')

puts "==== Sending"
socket.write("Hello server, can you hear me?\n")

puts "==== Getting Response"
puts socket.readline 


To demonstrate, we first need to run the server. Then we run the client. You can see the results below:

Example of a simple UNIX socket client/server interaction Example of a simple UNIX socket client/server interaction. The client is on the left. The server is on the right.

Interfacing with nginx

Now that we know how to create a unix socket "server" we can easily interface with nginx.

Don't believe me? Let's do a quick proof-of-concept. I'm going to adapt the code above to make it print out everything it receives from the socket.

require "socket"

# Create the socket and "save it" to the file system
server ='/tmp/socktest.sock')

# Wait until for a connection (by nginx)
socket = server.accept

# Read everything from the socket
while line = socket.readline
  puts line.inspect


Now if I configure nginx to forward requests to the socket at /tmp/socktest.sock   I can see what data nginx is sending. (Don't worry, we'll discuss configuration in a minute)

When I make a web request, nginx sends the following data to my little server:

http request

Pretty cool! It's just a normal HTTP request with a few extra headers added.  Now we're ready to build a real app server. But first, let's discuss nginx configuration.

Installing and Configuring Nginx

If you don't already have nginx installed on your development machine, take a second and do that now. It's really easy on OSX via homebrew:

brew install nginx

Now we need to configure nginx to forward requests on localhost:2048 to a upstream server via a socket named /tmp/socktest.sock. That name isn't anything special. It just needs to match the socket name used by our web server.

You can save this configuration to /tmp/nginx.conf and then run nginx with the command nginx -c /tmp/nginx.conf to load it.

# Run nginx as a normal console program, not as a daemon
daemon off;

# Log errors to stdout
error_log /dev/stdout info;

events {} # Boilerplate

http {

  # Print the access log to stdout
  access_log /dev/stdout;

  # Tell nginx that there's an external server called @app living at our socket
  upstream app {
    server unix:/tmp/socktest.sock fail_timeout=0;

  server {

    # Accept connections on localhost:2048
    listen 2048;
    server_name localhost;

    # Application root
    root /tmp;

    # If a path doesn't exist on disk, forward the request to @app
    try_files $uri/index.html $uri @app;

    # Set some configuration options on requests forwarded to @app
    location @app {
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_redirect off;
      proxy_pass http://app;


This configuration causes nginx to run like a normal terminal app, not like a daemon. It also writes all logs to stdout. When you run nginx it should look something like so:

Nginx running in non-daemon mode. Nginx running in non-daemon mode.

A DIY Application Server

Now that we've seen how to connect nginx to our program, it's a pretty simple matter to build a simple application server. When nginx forwards a request to our socket it's a standard HTTP request. After a little tinkering I was able to determine that if the socket returns a valid HTTP response, then it'll be displayed in the browser.

The application below takes any request and displays a timestamp.

require "socket"

# Connection creates the socket and accepts new connections
class Connection

  attr_accessor :path

  def initialize(path:)
    @path = path
    File.unlink(path) if File.exists?(path)

  def server
    @server ||=

  def on_request
    socket = server.accept

# AppServer logs incoming requests and renders a view in response
class AppServer

  attr_reader :connection
  attr_reader :view

  def initialize(connection:, view:)
    @connection = connection
    @view = view

  def run
    while true
      connection.on_request do |socket|
        while (line = socket.readline) != "\r\n"
          puts line 


# TimeView simply provides the HTTP response
class TimeView
  def render
%[HTTP/1.1 200 OK

The current timestamp is: #{ }

end '/tmp/socktest.sock'), view:

Now if I fire up nginx as well as my script, I can go to localhost:2048. Requests are sent to my app. And responses are rendered by the browser. Pretty Cool!

HTTP requests are logged to STDOUT by our simple app server HTTP requests are logged to STDOUT by our simple app server

And here is the glorious fruit of our labors. Behold! A Timestamp!

The server returns a timestamp which is displayed in the browser The server returns a timestamp which is displayed in the browser

Level up your software development career

Join our community of kick-ass developers as we learn engineering, DevOps, cloud architecture, and bootstrapping remote software companies. Tell me more →

We're Honeybadger. We'll never send you spam; we will send you cool shit like exclusive content, memes, and special swag.