Docker for Devs: Debug Docker Containers with WebStorm

Debug Docker feature image with Docker Whale and words Docker 4 Devs: Debug Docker Containers with WebStorm

If you’re using Docker to host your applications development environment, it will be important to know how to debug your applications source code running in the container. This is the first of a three part series where were going to look at how to debug docker containers running an React.js/Node.js/Express.js application. In this first part were going to look at how to use WebStorm, followed by Visual Studio Code and finally hooking up the good Ol’ fashion node.js debugger.

If you haven’t started, but are interested in hosting your applications development environment inside Docker containers, you’re going to want to check out the recently released Docker for Devs tutorial. If you’re coming directly to this tutorial on debugging docker containers, I’ll be assuming you’re familiar with Docker Compose.

As a bonus, we’re also going to look at nodemon for monitoring server side changes and restarting our server without us having to shutdown and restart containers on ever server-side change.

Getting Started

To easily get up and running, you can use this Github repository to get up and running. Its very similar to the tutorial involving Docker Compose but without the nginx reverse proxy.

Debug Docker Containers

The bottom line is, we need our Docker container to be reachable by an external debugger. In our case, we want to configure WebStorm’s Node.js debugging plugin to connect to our running Express.js application’s debugger that is running remotely in our Docker Container. In doing so, we’ll be in a position to debug docker container with WebStorm or other other EDIs which we’ll see in upcoming tutorials.

Therefore, this is a three part series:

  1. Prepare Docker Compose
  2. Build Images and Run Containers using Docker Compose
  3. Configure WebStorm Node.js Remote Debugger plugin
  4. Connect WebStorm Node.js Remote Interpreter to our application’s debugger

 

Step 1. Preparing Images

 

Step 1a. Create Docker Compose File

No different than how we started using Docker Compose in the Docker tutorial, we’re also going to leverage Docker Compose for creating our network, creating our services and their containers in this debug docker container tutorial.

  1. Create a file in the root of the project and name it “docker-compose.yml
  2. Paste in the following:
    version: "2.1"
    
    services:
    node:
    container_name: hackershall-dev-app
    image: hackershall-dev-i
    build:
    context: .
    dockerfile: ./.docker/node.dev.dockerfile
    ports:
    - "7000:7000"
    networks:
    - hackershall-network
    volumes:
    - .:/var/app
    env_file:
    - ./.docker/env/app.dev.env
    working_dir: /var/app
    
    mongo:
    container_name: hackershall-dev-mongodb
    image: mongo:3.4.1
    build:
    context: .
    dockerfile: ./.docker/mongo.dev.dockerfile
    env_file:
    - ./.docker/env/mongo.dev.env
    networks:
    - hackershall-network
    ports:
    - "27017"
    
    networks:
    hackershall-network:
    driver: bridge

     

What Did We Do?
  • We created a Docker Compose File
  • That created a single network named “hackershall-network”
  • Specified two services: node and mongo
  • and set the services to use the “hackershall-network” network

 

Step 1b. Debug Ports

Currently, we only have the port 7000 of the container mapped to our local host post 7000. We are using this port to connect to the running application in our container (e.g. http://localhost:7000).

We want to be able to attach WebStorm to a running node.js debugger in our application’s Docker container. To do this, we need to open up the port that our Express.js application debugger will be listening on.

We could specify any port to use by specifying a port along with the –debug flag, when we start our express.js application.

e.g.

node --debug=<custom_port>

 

However, to keep it as simple as possible, we’re going to stick with the default port that node.js debugger will listen on which 5858.

In order to make it available through our container do the following:

  1. Inside the docker-compose.yml file, locate the “ports” property, under the “node” service
  2. add an additional port mapping 5858 to local host port 5858:
    ports:
      - "7000:7000"
      - "5858:5858"
    

     

What Did We Do?
  • We have mapped the port 5858 on the container to our local host port 5858
  • If the node.js debugger for our application is listening on port 5858, we’ll be able to connect to it from our host machine.

 

Step 1c: Updating Dockerfile

When setting up port access to a container there are two parts. Exposing the port on the container and mapping the port from the container to the local host. I thought it might be easier in the overall approach to wire up the port mapping first.

NOTE: If you missed the Docker tutorial, let me point out that we have a “production” image which plays the role of base image for our development image defined in the node.dev.dockerfile located inside the “.docker” directory off the project root.

 

We now need to expose port 5858 from our express.js application image which we’ll specify in the application development Dockerfile:

  1. Open the file “node.dev.dockerfile” in the .docker directory off the root
  2. Lets add a new environment variable and its value, locate the ENV line and update with:
    ENV NODE_ENV=development
  3. Update to:
    ENV NODE_ENV=development DEBUG_PORT=5858
  4. Before the CMD command, add the following:
    EXPOSE $DEBUG_PORT

    Result:

    FROM maxmccarty/hackershall-prod-i
    
    ENV NODE_ENV=development DEBUG_PORT=5858
    
    EXPOSE $DEBUG_PORT
    
    CMD ["./.docker/node_scripts/initialize.sh"]

     

What Did We Do?
  • Created a new environment variable DEBUG_PORT
    We could have simple exposed Port 58585 (EXPOSE 5858) but we took the liberty to use environment variables
  • Used the EXPOSE command to expose the value of $DEBUG_PORT (5858)
NOTE: If you’re wondering where the port 7000 is being exposed since we saw that we are mapping it in the docker-compose.yml file, for those of you that did the tutorial, we have this mapped in the production Dockerfile node.prod.dockerfile already.

 

Step 1d: Updating Our NPM Scripts

We still have one more step to complete to be able to attach WebStorm to a node.js debugger and that is starting our application’s node.js debugger.

  1. Open the package.json file in the project root
  2. Locate the NPM script:
    "start:server": “babel-node server/server.js,
  3. Add the –debug flag after babel REPL
    "start:server": "babel-node --debug server/server.js",

     

What Did We Do?
  • On our NPM script to start our application,
  • we specify the –debug flag in order to start the node.js debugger to be listening

 

Step 1e: Enabling nodemon

While we already have everything in place to create our application development environment images, lets add nodemon into the mix.

NOTE: If you’re not familiar with nodemon, it’s simply a utility that will monitor your application server files and restart your node.js server when there are changes.  Imagine, every server side change (e.g. route add or update) would normally require you stopping your running application contain and restarting it.  The same would be if we weren’t using Docker, you would be required to shutdown and restart your express.js application on any change.

 

  1. Going back to the same NPM script we edited in Step 1c, lets make the small change of adding nodemon:
    "start:server": "nodemon --debug server/server.js --exec babel-node",

 

What Did We Do?
  • We added the nodemon utility to run our node.js application
  • and specified nodemon to execute the babel-node CLI using the —exec option

 

Step 2: Build Images and Run Containers using Docker Compose

With our dockerfile’s updated to expose the debugger port 5858 from our application container, along with the port mapping to our local host, we’re ready to create the images and generate running containers of our application.

Step 2a: Build Images

  1. To build the specified Docker Compose images for application and database run the command:
    docker-compose build
What did we do?

In that single command of BUILD docker-compose:

  • pulled any dependent images
  • and built specified images in the docker-compose.yml file.

 

Step 2b. Generate Containers

  1. To generate running containers from our build Docker images, run the following command
    docker-compose up
  2. Observe the log messages in the WebStorm terminal that the debugger is listening on port 5858:


    Debug Docker Container console message reading Debugger listening on 5858
What did we do?

Using the UP docker-compose:

  • Create the specified network.
  • Generated the node and mongo specified containers
  • Mongo scripts were ran, admin and various operation accounts created.
  • MongoDB and Node service was brought online.

Step 2c. Verify Running Application

  1. From your browser navigate to http://localhost:7000 to verify the application is running and accessible:View of the running application in container for debug docker container post

 

Step 3: Configure WebStorm Node.js Remote Debug plugin

We now have a running container of our application exposing the port 5858, a mapping of 5858 from the container to our local host port 5858 and our application running with the node.js debugger listening.

Before we can use WebStorm to debug docker container, and actually attach to the listening node.js debugger in the container, we have to configure WebStorm’s node.js remote debug plugin so that it knows what address (localhost) and port (5858) the debugger is listening on.

Step 3a: Installing / Enabling Node.js Remote Interpreter Plugin

Depending on your OS, you’re steps might very.

  1. Open WebStorm preferences
  2. Click on Plugins
  3. Locate the Node.jsView of WebStorm plugin screen
  4. If you don’t have it installed you can click on “Install JetBrains Plugins from the same screen and consult this WebStorm documentation.
  5. Ensure its enabled by checking the box to the right.

 

Step 3b. Creating Debug Configuration

With the plugin installed and enabled, we need to setup a configuration for this interpreter to connect to our application’s debugger listening on port 5858.

  1. Off the main WebStorm menu click on Run -> Edit Configuration
  2. Click on the “+” top left to open the “Add New Configuration” dropdown
  3. Select “Node.js Remote Debug” from the menuView of screen to add a custom WebStorm configuration
  4. Provide a name
  5. Specify “Localhost” for the Host field
  6. Verify port is at the default 5858View of form for Node.js Remote Interpreter configuration in WebStorm
  7. Click “Apply” then “OK”

 

Step 4: Connect WebStorm Node.js Remote Interpreter to our application’s debugger

Remember, our application container is running, we now have setup the configuration for WebStorm’s Node.js Remote Interpreter with the information it needs to know where to connect to the listening debugger in our apps container.

We just need to specify the configuration we want to use and set some breakpoints and debug docker.

Step 4a. Select Debugger Configuration

  1. Locate on the top right of the main WebStorm window a dropdown with Configurations
  2. Select the one you created and named in the previous step 3b.View of selecting the custom configuration to use when debug docker container from WebStorm

 

Step 4b. Verify Running Application

We verified the application was running back at the end of Step 2. We should now be able to setup breakpoints and trigger them from within WebStorm to debug docker container:

  1. From the project root, open server -> routes -> timelineRoutes.js
  2. Place a breakpoint on line 20 by clicking to the right of the line number
    const {startDate, endDate} = req.query;

    View of added breakpoint to code in WebStorm

 

Step 4c. Start the Debug

  1. Click on the green Debug “Bug” icon from the same top right corner of the main WebStorm: window:View of where the Bug Icon for WebStorm is to start a debug session
  2. Refresh or navigate back to http://localhost:7000 which will trigger a call to the timeline route for getting timeline events which is part of the initial request.
  3. Back in WebStorm we can see the breakpoint triggered and if we step over the current breakpoint also observe information on the state of the request:View of an actual debug session when breakpoint hit in WebStorm
What did we do?
  • We created a debugging configuration for the remote interpreter plugin that would allow it to connect to the listening debugger in our app’s Docker container.
  • Debug Docker container application using the WebStorm’s debugging capabilities.
  • Set breakpoints in WebStorm on specific code that will be triggered.

 

BONUS

As part of this setup, we also configured our starting NPM script to launch nodemon so that we could make changes to our server and nodemon will restart our server with out us having to shutdown and restart the app container whether doing feature enhancement or a debug docker scenario.

  1. Notice that when are application started we got a message that nodemon was monitoring in our console:
    hackershall-dev-app | [nodemon] 1.11.0
    hackershall-dev-app | [nodemon] to restart at any time, enter `rs`
    hackershall-dev-app | [nodemon] watching: *.*
    hackershall-dev-app | [nodemon] starting `babel-node --debug server/server.js`
    
  2. Make a small change to the any file, for example add an additional empty line in the timelineRoutes.js file.
  3. Save the file
  4. Observe the nodemon related log messages in WebStorm’s terminal:View of nodemon terminal logs when restarted in WebStorm console/terminal

Nodemon is monitoring for any changes and will automatically restart the server for us. This is exceptionally handy when dealing with live editing changes when working on features that require server-side changes.

Conclusion

When you realize how to facilitate the core components required to debug docker containers, there isn’t much to it. It really boils down to exposing the proper ports on the container, creating a port mapping between the container and the host, and configuring WebStorm to know where to connect its debugging tools and that exactly what we did.

 

About the author

Max McCarty

Max McCarty is a software developer with a passion for breathing life into big ideas. He is the founder and owner of LockMeDown.com and host of the popular Lock Me Down podcast.