Graceful shutdown of NodeJS/Remix Server

Hi! I’m unable to gracefully shutdown a Remix app using the Remix Blues Stack server.ts and Indie Stack start.sh script (workaround suggested by fly.io team for npx prisma migrate deploy release command issues).

The specific issue is that the server never receives the SIGINT signal. I have added the following to the server, which works locally, but is never triggered on deployed VMs.

export function addShutdownHandlers(shutdownFn: NodeJS.SignalsListener) {
  ["SIGINT", "SIGTERM"].forEach((signal) => {
    process.once(signal, shutdownFn);
  });
}

I have verified that a signal is being sent via logs and that it just isn’t be handled/sent to the nodejs running process. I have previously tried adding tini to the Dockerfile and upgrading to yarn:berry, but neither approach worked.

Here are my questions:
1. What is the correct structure for the Dockerfile in order for the CMD/ENTRYPOINT to receive the SIGINT signal?
2. Is there an example NodeJS app/configuration that has graceful shutdown working?


Relevant snippets from files mentioned above

Dockerfile

// ...
ENTRYPOINT [ "./start.sh"]

start.sh

set -ex
npx prisma migrate deploy
yarn start

package.json

"start": "node --require ./tracing.js ./build/server.js",
1 Like

I don’t know about Remix but in my Node apps I simply do something like:

async function closeGracefully () {
	// do whatever you need to do...
	process.exit();
}

process.once('SIGTERM', closeGracefully);

Which AFAIK works as expected.

In the Docker file I usually start the app with something like this:

CMD ["node", "src/index.js"]

Why would it matter how you start your app in Docker to receive the signals? :thinking:

1 Like

Not sure if you’ve found the solution already, but I had a similar issue and was able to solve it with a small change.

Why would it matter how you start your app in Docker to receive the signals? :thinking:

When you start your application via an entrypoint script like yours instead of CMD ["node", "index.js"] like @pier suggested, you must replace the process, otherwise Docker will forward the SIGTERM signals to the entrypoint script process instead of your application process.

Here is how you can solve this on your ./start.sh script:

# your setup commands here as before
#...

# the `exec` command is special because when used to execute a 
# command like running our service, it will replace the parent process 
# with the new process kicked off for our application. 
# Now when Docker sends the SIGTERM signal to the process id,
# the Remix app will trap the SIGTERM and will be able to gracefully shutdown.
exec "yarn" "start"

You can find out more about how exec behaves here.

This resolved the issue for me on an Elixir application. It will most probably help you with Remix as well.