Gracefully Shutdown Node Apps
Thomas Pegler
Posted on August 11, 2023
This article will be primarily written for Dockerized Node apps, but the final stage can definitely be used for any Node apps because it makes the teardown process nicer and more informative.
Getting started
First up is installing an init service, I've personally been using dumb-init from Yelp.
ENTRYPOINT [ "dumb-init", "node", "app.js" ]
With that, you get all signals properly injected into the environment (SIGINT
, SIGTERM
, SIGHUP
etc.) so that you can handle them yourself. This is useful because without them, the process just dies, all connections are terminated and this can leave connected services hanging, data correctness issues and a terrible user experience because they're just kicked.
Here's a simple setup similar to what I use for handling these a bit more gracefully:
import cors from 'cors';
import express, {
Express, Request, Response,
} from 'express';
import helmet from 'helmet';
import { createServer } from 'http';
/**
* Main Express src class.
*
* Create a new instance and then run with await instance.start().
*/
class Server {
app: Express;
config: Config;
token?: string | null;
constructor() {
// Get and set environment variables
this.app = express();
this.config = getConfig();
}
// eslint-disable-next-line class-methods-use-this
teardown() {
disconnectFromDatabase()
.then( () => console.log( 'Disconnected from Mongo servers.' ) )
.catch(
( e: Error ) => {
console.error( `Received error ${e.message} when disconnecting from Mongo.` );
},
);
}
async start() {
this.app.use( express.urlencoded( { extended: true, limit: this.config.fileSizeLimit } ) );
this.app.use( express.json( { strict: false, limit: this.config.fileSizeLimit } ) );
this.app.use( express.text() );
this.app.use( helmet() );
this.app.use( cors() );
this.app.use( limiter );
const server = createServer( this.app ).listen( this.config.port, () => {
console.log(
`🚀 Server ready at ${this.config.host}`,
);
} );
server.timeout = this.config.express.timeout;
process.on( 'SIGINT', () => {
console.log( 'Received SIGINT, shutting down...' );
this.teardown();
server.close();
} );
process.on( 'SIGTERM', () => {
console.log( 'Received SIGTERM, shutting down...' );
this.teardown();
server.close();
} );
process.on( 'SIGHUP', () => {
console.log( 'Received SIGHUP, shutting down...' );
this.teardown();
server.close();
} );
}
}
const server = new Server();
server.start()
.then( () => console.log( 'Running...' ) )
.catch( ( err: Error | string ) => {
console.error( err );
} );
That's essentially it. I didn't include a few of the methods but if you connect to a database or AMQP service or some other service, it's good to properly close those connections off to ensure that any read/write/push/whatever is completed before forcefully stopping the application.
Header by Simon Infanger on Unsplash
Posted on August 11, 2023
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.