Hosting daemon apps on Forge

2nd February 2020

I am a huge fan of Laravel Forge. It does all the things I don't like doing, for me. It's affordable. It works with Digital Ocean.

Thing is...I don't only make Laravel applications. I've been building many Next apps, recently, and that means configuring my servers to maintain a daemon. It also means I need to send the usually Nginx traffic to the daemons, and restart the daemons when I have new code to deploy.

Last week, I set up another Next app: Use Noto. I think I've reached a point where I can document the process, so others can try non-PHP apps in Forge.

Creating a daemon

Go to the server where you've created the site → Daemons:

Creating a new daemon

You probably have some way to start the daemon from NPM or Composer. If not, learn how to define custom scripts in NPM or Composer, and add them.

I find it helpful to run these commands through SSH, on the server, to make sure they run in a production environment. Otherwise, the daemon might get stuck in a restart loop, and you'll be none the wiser.

Chances are your daemon doesn't run on port 80. By default, Forge blocks most traffic to non-HTTP and non-SSH ports. You could add your custom port to the firewall; but it's quicker just to forward requests from Nginx to your app.

Forwarding requests to your app

If you go into your site, there's a "Files" button, at the bottom of the screen. Edit the Nginx configuration, and you'll see config that resembles this:

Server config

There'll probably be config above and below, but this section is all we need to change. By default, Forge sets sites up to use PHP-FPM. This is how Nginx knows what to do, when a new "PHP request" comes in...

We can swap this out for code resembling:

location / {
    proxy_set_header   X-Real-IP $remote_addr;
    proxy_set_header   Host      $http_host;
    proxy_pass         http://127.0.0.1:3000;
}

location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt  { access_log off; log_not_found off; }

access_log off;
error_log  /var/log/nginx/use-noto.assertchris.io-error.log error;

Your port may be different, so change this new config to reflect that. If everything is correctly configured, and your server is running, you should now be able to see it in a browser.

Restarting the daemon

Forge uses Supervisor to manage daemons. When the daemon crashes, Supervisor is configured to start it up again. This is desirable behaviour.

Unfortunately, Supervisor isn't configured to restart the daemon if the site code changes. We need to restart it ourselves, and there's no elegant way to do that.

Supervisor has a concept called named groups, and it also has a CLI command to restart named groups; but it seems Forge doesn't cater nicely to this functionality. I'd love to tell you to run supervisorctl restart example-app in your deploy script, but there's no way this will work unless you manually change the Supervisor configuration...

These days, I tend to use a combination of bash commands, to find and kill the daemon. This is what the script looks like, for Use Noto:

kill $(ps -afx | grep '[n]ext' | grep '[u]se-noto' | awk '{print $1}')

Use Noto is built on Next, so I filter all the processes by [n]ext. The brackets make it so that the grep command isn't in the same list that it returns. Then, I narrow the list down by the folder name in which it's running: [u]se-noto.

awk takes $1 (which is the first column in the returned list of processes), and prints it as the result of the $(...) section. kill uses this PID to kill the daemon.

You'll need to change [n]ext and [u]se-noto to match your project. I find it best to prototype this command by running just what's inside $(...) – making sure the only returned line is the daemon process you're trying to kill. You can use as many pipes into grep as you need, to filter to just the intended process.

My whole Deploy Script, for Use Noto, looks like this:

cd /home/forge/use-noto.assertchris.io
git pull origin master
npm install
npm run build
kill $(ps -afx | grep '[n]ext' | grep '[u]se-noto' | awk '{print $1}')

If your project is Python-based, you'll probably switch npm out for pip. If it's ReactPHP-based, you'll probably use composer commands in there...

Would you like me to keep you in the loop?

I write about all sorts of interesting code things, and I'd love to share them with you. I will only send you updates from the blog, and will not share your email address with anyone.