When I was first introduced to Docker, I was excited to see how much easier this can make developing eg. Java applications. Setting up the whole environment was brought down to a set of simple commands allowing me to spin it up without any fuss. But after spending some time with Go, which compiles to static binary files, I was wondering if I really need Docker containers to run them efficiently? This led me to a little experiment with "launchd".

What is launchd

It is a macOS service management framework responsible for starting and stopping daemons, applications, processes, and scripts. It is responsible for booting the system as well as loading any services, both owned by the OS and by the users. To show how important it is, you can verify that it has the PID 1, which means that it's an ultimate alpha-process:

$ ps aux | grep launchd
...
root                 1   (...) /sbin/launchd
...

All the interaction with launchd has to be done using its friend launchctl. In fact, if you try to use launchd directly, you get an error:

$ launchd
launchd cannot be run directly.

The most important commands that we will use with launchctl are:

  • launchctl list returns a list of all running services
  • launchctl load <config-file.plist> adds a configuration from file to the services list
  • launchctl unload <config-file.plist> removes a service from the list

Note that loading the file does not necessarily mean that the application configured there will start. This happens only if the configuration says that the app must be up (KeepAlive or RunOnLoad properties).

Agent application

In order to show how launchd can make sure your app is up and running at all times, we should start by actually making one. We will create a tiny HTTP server in Go that will have two handlers.

The first one will print out a greeting (because it's polite to greet your users) alongside with a time at which the server was started. This will allow us to verify later on if the app was actually restarted:

http.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
    fmt.Fprintf(rw, "Hello from launchd agent! I started at %v!", start)
})

The second endpoint is an evil one, as we will use a hole in an otherwise robust architecture of HTTP servers in Go. As you may already know, by default any time a panic (terminal error for those less Go-savvy) happens inside an HTTP server, the language takes care of it (prints the stack trace, fails the request) and allows the whole app to continue to work. Unless that panic happens in a separate goroutine...

http.HandleFunc("/panic", func(rw http.ResponseWriter, req *http.Request) {
    go panic("can't stop me")
})

Thanks to our hackerish background, we now have a server that can totally crash and is on our mercy:

(server)
./launchd-app 
Starting HTTP server at :9999
(client)
$ curl localhost:9999
Hello from launchd agent! I started at 2018-08-26 22:51:24(...)!

(client)
$ curl localhost:9999/panic
(server)
panic: can't stop me
goroutine 9 [running]:
panic(0x1237b40, 0xc420010bb0)
...

$ curl localhost:9999
curl: (7) Failed to connect to localhost port 9999: Connection refused

Agent configuration

In order to run the application through launchd we first need to define its configuration in a XML file describing an agent. The configuration looks as follows:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
    <dict>
        <key>Label</key>
        <string>com.mycodesmells.launchd-app</string>
        <key>Program</key>
        <string>/Users/slomek/go/src/github.com/mycodesmells/misc-examples/launchd/app/launchd-app</string>
        <key>KeepAlive</key>
        <true/>
    </dict>
</plist>

There are three basic ingredients that make this configuration do the magic:

  • Label is an identifier of the agent and by convention, it's build using a reverse domain notation. Since it's built for MyCodeSmells.com, I'm naming it com.mycodesmells.launchd-app.
  • Program is a full path to the executable
  • KeepAlive indicates that we with launchd to restart the application as soon as it realizes that it crashed/exited.

If you want the application to run on system load, you should put it in ~/Library/LaunchAgents directory. Then, in order to load it during your current session run:

launchctl load com.mycodesmells.launchd-app.plist

If there are no errors, the list of running services should include our own:

$ launchctl list | grep mycodesmells
2691    2       com.mycodesmells.launchd-app

Now, you should be able to call the server's endpoints now:

$ curl localhost:9999
Hello from launchd agent! I started at 2018-08-26 22:32:24(...)!

Let's try to break things:

$ curl localhost:9999/panic
# server crashes

$ curl localhost:9999
Hello from launchd agent! I started at 2018-08-26 22:37:49(...)!%

It works!

Summary

Maybe for Go, you don't always need Docker? If you have small apps running on some cloud provider's VM, the overload is big and this could be done using tools the OS already provides.

For the record, I'm not saying that this solution is better than Docker, Kubernetes or whatever you use for your application orchestration. I'm just saying that there is an alternative, and maybe this fits your needs?

The full source code of the more detailed example is available on Github.

Read more