Gracefully Shutdown Node Apps
Tags: typescript, node, tutorial, docker
Published: 2023-08-11 11:00:00

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](https://github.com/Yelp/dumb-init) from Yelp.

```Dockerfile
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:

```typescript
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 <a href="https://unsplash.com/@photosimon?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Simon Infanger</a> on <a href="https://unsplash.com/photos/hgdh6IwKi7Y?utm_source=unsplash&utm_medium=referral&utm_content=creditCopyText">Unsplash</a>