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.
Leave a Reply