Deploying Go with Apache? (using FastCGI)

Nowadays, if you want to deploy a web service you’ll probably either run it as a service on a Linux server somewhere, throw it in a Docker container, put it in Kubernetes, or run it on some PaaS somewhere. That works, and it can be easy! But we seem to have forgotten an even simpler way to deploy web services – CGI! (or, even better, FastCGI)

CGI, or the Common Gateway Interface, defines a way for a web server to start a process and proxy an HTTP request through to that process. It defines environment variables based on the request headers, path, method, etc., sends the request body through stdin, and receives the response body from stdout. Classically, CGI programs were written in scripting languages like Perl. There are also quite a few written in the likes of C. Apache is the primary web server used for running CGI programs nowadays, with the module mod_cgid.

You can easily write a modern web service that uses CGI by writing it in Go. The net/http/cgi package lets you serve any Go HTTP request handler using CGI! Converting an existing web service from being an HTTP daemon to a CGI program is as simple as changing the entrypoint:

import (
  "net/http"
  "net/http/cgi"
)

func handle(rw http.ResponseWriter, req *http.Request) {
  rw.WriteHeader(204)
}

// your existing http entrypoint
func mainHTTP() {
  http.ListenAndServe(":8080", http.HandlerFunc(handle))
}

// your new cgi entrypoint
func mainCGI() {
  cgi.Serve(http.HandlerFunc(handle))
}

The web server handles running the child process – you just plop an executable into your cgi-bin directory, and the web server will execute it for every request. As you may be able to imagine, this doesn’t scale very well. Launching a new process for every request, with each request effectively being a cold start, produces quite a bit of load.

That’s where FastCGI comes in. FastCGI is a protocol which allows multiple requests to be served by one process. There are two ways you can use FastCGI: running a daemon which your web server communicates to through TCP/UNIX socket, or having your web server manage your service process(es) and communicate via stdin/stdout. My preferred method is the latter – it works just like CGI, but with better performance!

In Go, just use the net/http/fcgi package to make your existing service use FastCGI:

import (
  "net/http"
  "net/http/cgi"
  "net/http/fcgi"
)

func handle(rw http.ResponseWriter, req *http.Request) {
  rw.WriteHeader(204)
}

// your existing http entrypoint
func mainHTTP() {
  http.ListenAndServe(":8080", http.HandlerFunc(handle))
}

// your cgi entrypoint
func mainCGI() {
  cgi.Serve(http.HandlerFunc(handle))
}

// your fastcgi entrypoint
func mainFastCGI() {
  // nil means the web server is on stdin/stdout
  fcgi.Serve(nil, http.HandlerFunc(handle))
}

Apache has a module called mod_fcgid which operates much like mod_cgid. Once configured, you can basically use FastCGI the same way you’d use CGI. With Debian’s default FastCGI configuration, binaries with the .fcgi extension are treated as FastCGI binaries.

I personally like to install the Go binaries into /usr/lib/cgi-bin. I then use Alias to mount the service on a path:

<VirtualHost *:80>
  ServerName somewebsite.example.com
  Alias / /usr/lib/cgi-bin/mywebsite.fcgi
  Include conf-available/serve-cgi-bin.conf
</VirtualHost>

For deploying updates, I just build a Debian package with nFPM and install it. I can even bundle the site’s Apache config in with that Debian package.

In my opinion, setting up a web service like this is a lot simpler than deploying a persistent service, setting up containers, or anything else of the sort. It only works this easily for languages that compile to a single executable, like Go, though.


Posted

in

by

Comments

2 responses to “Deploying Go with Apache? (using FastCGI)”

  1. Juan# Avatar
    Juan#

    Have you measured that FastCGI is faster than CGI? By how much? Linux is doing a lot of caching for you and you’ll likely be surprised.

    1. Piper Avatar

      I haven’t! I bet it isn’t too bad, but there are other concerns than just raw processing performance. For example, with FastCGI shared processes, you can have a persistent database connection pool so you (1) don’t have too many concurrent database connections and (2) aren’t constantly churning through connections.

Leave a Reply

Your email address will not be published. Required fields are marked *