DEV Community

Cover image for Gracefully Shutdown Node Apps
Thomas Pegler
Thomas Pegler

Posted on

Gracefully Shutdown Node Apps

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" ]
Enter fullscreen mode Exit fullscreen mode

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 );
    } );

Enter fullscreen mode Exit fullscreen mode

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

Top comments (0)