CoreOS — Run Your Node.js App on Cluster

During the last weeks, we’ve learned the fundamentals of three important components of the CoreOS ecosystem. Additionally, you’re able to manage key-value-pairs within etcd and services within our cluster using fleet featuring systemd.

This gives us all the required knowledge to finally make the step from just running hello-world units on the cluster. This post will show you how to create Node.js application inside a docker container and run it on your CoreOS cluster. Ready? Yes!

CoreOS Series Overview

Create the Docker App Container

There are multiple ways to create a Docker container. You’ve probably heard of DOCKERFILE’s and creating a container from the command line. This guide will follow along the command line to make the container creation more accessible. Of course, you can create your container by using a DOCKERFILE.

As the base image, we use Ubuntu 14.04. That’s an LTS release and will definitely be supported for quite some time. We’re going to create the initial docker container with the following command:

docker run -i -t ubuntu:14.04 /bin/bash  

Let’s get things straight and explain the command with parameters from above:

  • run: tells docker to start a container with the following parameter (if existing) based on the given base image
  • -i: tells docker to start the container in interactive mode and makes STDIN of the container available
  • -t: tells docker to create a pseudo-TTY session. This gives us access to the terminal of the docker container running Ubuntu
  • ubuntu:14.04: this is actually a combination of repository:image. We’re using the ubuntu repository with base image 14.04. Have a look at the Ubuntu Docker Hub repository for other images.
  • /bin/bash:: the command we want to execute inside the container. Since we’re going to install tools, we want access to the command line and spawn a shell session

Where to run the command?

That’s a good question! You can run this command from within one of your CoreOS machines. Since CoreOS heavily relies on docker, it comes directly with the operation system. Of course, you can run the command on your local machine. If you create the docker container locally, make sure you’ve docker installed.

Alright, let’s jump right into the output happening once you submit the command to either a CoreOS or your local machine.

$ docker run -i -t ubuntu:14.04 /bin/bash
Unable to find image 'ubuntu:14.04' locally  
14.04: Pulling from library/ubuntu  
0bf056161913: Downloading [>                                                  ] 539.8 kB/65.67 MB  
1796d1c62d0c: Download complete  
e24428725dd6: Download complete  
89d5d8e8bafb: Download complete  

The defined docker base image gets pulled from the Docker Hub if it isn’t already available locally. The download may take some time, be patient while waiting for the base image download to finish.

Once the download finished, you’ll be directly within the terminal session of the docker container.

$ docker run -i -t ubuntu:14.04 /bin/bash
Unable to find image 'ubuntu:14.04' locally  
14.04: Pulling from library/ubuntu  
0bf056161913: Pull complete  
1796d1c62d0c: Pull complete  
e24428725dd6: Pull complete  
89d5d8e8bafb: Pull complete  
Digest: sha256:d3b59c1d15c3cfb58d9f2eaab8a232f21fc670c67c11f582bc48fb32df17f3b3  
Status: Downloaded newer image for ubuntu:14.04  
root@999e20f43569:/#  

Now that we have access to the Ubuntu shell, we can use the default package manager apt to update the repositories and install curl and Node.js. We use the Nodesource packages which need to be fetched with curl before we’re able to install Node.js. We use the current LTS release 4.x of Node.js. Fire the following commands within your docker container to get Node.js installed.

apt-get update  
apt-get install -y curl  
curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -  
apt-get install -y nodejs  

Verify if everything is alright by checking the versions of Node.js and NPM.

$ node -v
v4.2.3  
$ npm -v
2.14.7  

Alright, we’re good to go and create the actual app.

Create Node.js App in Docker Container

The goal of this tutorial is to create and run a Node.js application within our CoreOS cluster. That requires us to create a basic Node.js server which at least responds to every request with a message. Let’s get a step ahead and read a value from etcd and reply to requests with a view showing the value (if existing) or a default message (if not set yet). Agreed? Agreed! :)

The following code snippet is the content of our server.js file which represents our Node.js app. Of course you can run more complex apps this way. This example is just for illustration purposes and shows the fundamentals to get a Node.js submitted and running on a CoreOS cluster.

We put the package.json and server.js files into the /src folder. This way, we have a specified folder to install for the dependencies and know from where to start the server when submitting and starting it on the cluster.

server.js

const http = require('http')  
const Etcd = require('node-etcd')  
const etcd = new Etcd('172.17.8.103', '4001')

const hostname = '0.0.0.0' // this will expose the Node.js server beyond localhost … avoid that in production!  
const port = 3000

http.createServer((req, res) => {  
  etcd.get(req.url.slice(1), function (err, result) {
    res.writeHead(200, { 'Content-Type': 'text/plain' })
    if (err) {
        console.log(err)
        res.end('No value for key "' + req.url.slice(1) + '" available in etcd')
    } else {
        res.end('Got value for key "' + req.url.slice(1) + '" from etcd: ' + result.node.value)
    }
  })
}).listen(port, hostname, () => {
  console.log('Server running at http://${hostname}:${port}/')
})

Initialize a Node.js with NPM inside your docker container using npm init. Tell NPM to use the created server.js file as the entry point for your app. Then install the node-etcd dependency via npm i -S node-etcd. Afterward, try to run the app and if everything went well, you should see the log that your server started at 127.0.0.1 using port 3000.

$ node server.js
Server running at http://127.0.0.1:3000/  

Commit & Push the Container Image

Our docker container is prepared and the basic Node.js application is running like a charm. The next thing on the list is the to commit and push the container image to your Docker Hub repositories. To commit our container, we need the container ID. Use docker ps -l to show the container on your machine.

$ docker ps -l
CONTAINER ID        IMAGE               COMMAND             CREATED             STATUS                       PORTS               NAMES  
999e20f43569        ubuntu:14.04        "/bin/bash"         31 minutes ago      Exited (130) 7 seconds ago                       cranky_bhabha  

The important information is in the column of CONTAINER ID. In the example above it’s 999e20f43569. To create the same container again on another machine, we commit and push this container to the Docker Hub. Docker’s commit command uses the following structure:

docker commit <container id> <your-docker-hub-username>/<image-name>  

Putting our actual values in place results in the following command and console output.

$ docker commit 999e20f43569 marcuspoehls/node-etcd
ad33048afe08911238d082bf68727fdb5465298910d0650eb151bba1d7c68c26  

Changes are committed, we can finally push the container image. The upload and distribution process may take a while. Don’t get nervous if new machines occur while pushing the image, it’s the usual procedure :)

$ docker push marcuspoehls/node-etcd
The push refers to a repository [docker.io/marcuspoehls/node-etcd] (len: 1)  
ad33048afe08: Pushed  
89d5d8e8bafb: Pushed  
e24428725dd6: Pushed  
1796d1c62d0c: Pushed  
0bf056161913: Pushing [=========>                                         ] 37.04 MB/187.7 MB  

That’s it, our Node.js container is available via Docker Hub and we can move on to create the unit file which pulls the docker container onto the machine which will host our app.

Create Unit Files

The docker container is ready for deployment and pushed to Docker Hub. That means, we can create our unit files for deployment. We create two unit files: first, the one called node-etcd which boots the docker container and finally starts the Node.js server inside the container. Second, we create a little helper unit file that announces our app service to etcd.

Unit File: node-etcd Docker Container

The unit file describes itself. Just let me pick some important parts and explain them in more detail so everyone is on the same page and knows what’s going on here.

The [Unit] block just defines that we want this unit requires etcd and docker to run and also the dependency on our discovery service. Within the [Service] block, we tell fleet to remove all previously created containers with the name node-etcd%i where %i is the placeholder for the port number which we’ll pass during service start. Also, we execute docker run … including the commands to start our Node.js server inside the docker container. If the dependencies are not installed already, we’re going to install them and afterward kick off the server.

[Unit]
Description=Node.js app inside a docker container reading a value from etcd  
After=etcd2.service  
After=docker.service  
Requires=node-etcd-discovery@%i.service

[Service]
TimeoutStartSec=0  
KillMode=none  
EnvironmentFile=/etc/environment  
ExecStartPre=-/usr/bin/docker kill node-etcd%i  
ExecStartPre=-/usr/bin/docker rm node-etcd%i  
ExecStartPre=/usr/bin/docker pull marcuspoehls/node-etcd  
ExecStart=/usr/bin/docker run --name node-etcd%i -p %i:3000 \  
    -P -e COREOS_PRIVATE_IPV4=${COREOS_PRIVATE_IPV4}        \
    marcuspoehls/node-etcd                                  \
    /bin/sh -c "cd /src && npm i && node server.js" -D FOREGROUND
ExecStop=/usr/bin/docker stop node-etcd%i

[X-Fleet]
Conflicts=node-etcd@*.service  

Stopping the service will tell Docker to stop the container. Additionally, we have an [X-Fleet] constraint that tells fleet to put this unit only once on a machine.

Unit File: node-etcd-discovery for Service Discovery

This little helper just announces our Node.js app to the rest of the cluster members and spreads the word once we launch the node-etcd service. Since it’s a requirement for node-etcd, it will be launched simultaneously. We don’t need to worry about starting the node-etcd-discovery service, because fleet does the work for us and starts it aside node-etcd.

[Unit]
Description=Announce node-etcd@%i service  
BindsTo=node-etcd@%i.service

[Service]
EnvironmentFile=/etc/environment  
ExecStart=/bin/sh -c "while true; do etcdctl set /announce/services/node-etcd%i ${COREOS_PUBLIC_IPV4}:%i --ttl 60; sleep 45; done"  
ExecStop=/usr/bin/etcdctl rm /announce/services/node-etcd%i

[X-Fleet]
MachineOf=node-etcd@%i.service  

Once the node-etcd service stops, fleet will remove the announcement from etcd. There is also a [X-Fleet] condition which binds the node-etcd-discovery unit to node-etcd and starts this service on the same machine as node-etcd launches.

Run Node.js App on CoreOS Cluster

This is our final step and we finally see our app going live on the CoreOS cluster. All the preparations distill down to start and run the app inside the docker container which will be pulled once we kick off the unit on our cluster. Let’s do this!

First, we need to submit the node-etcd and node-etcd-discovery services to our cluster using fleetctl.

$ fleetctl submit node-etcd@.service node-etcd-discovery@.service
Unit node-etcd@.service inactive  
Unit node-etcd-discovery@.service inactive

$ fleetctl list-unit-files
UNIT                           HASH      DSTATE     STATE      TARGET  
hello.service                  e55c0ae   loaded     loaded     3781dfef.../172.17.8.101  
node-etcd-discovery@.service   c8c5ae5   inactive   inactive   -  
node-etcd@.service             e48e872   inactive   inactive   -  

Things went smoothly, the services are available to the cluster’s init system and are in inactive state. That’s what we expected because we just submitted them which doesn’t follow any service start.

Let’s get the unit loaded on a server. fleetctl will respect and follow our constraints defined within the [X-Fleet] block in the unit files. Since we defined the node-etcd-discovery service should run aside from the node-etcd service, we expect both services to be loaded on the same machine. If you want to shortcut this procedure, you can just start the node-etcd service and fleet will handle the service loading.

$ fleetctl load node-etcd@3000.service
Unit node-etcd@3000.service inactive  
Unit node-etcd@3000.service loaded on 767d4316.../172.17.8.103

$ fleetctl load node-etcd-discovery@3000.service
Unit node-etcd-discovery@3000.service inactive  
Unit node-etcd-discovery@3000.service loaded on 767d4316.../172.17.8.103  

Yeah, both services got submitted to our machine with trailing IP .103. That’s the intended behavior!

We’re very close to run the Node.js server. Our unit files specify, that the node-etcd-discovery service is bind to the actual node-etcd service. That means, we can just start the node-etcd service and fleet will automatically boot both! Great work fleet developers :)

$ fleetctl start node-etcd@3000.service
Unit node-etcd@3000.service launched on 767d4316.../172.17.8.103

fleetctl list-units  
UNIT                               MACHINE                       ACTIVE        SUB  
hello.service                      3781dfef.../172.17.8.101      inactive      dead  
node-etcd-discovery@3000.service   767d4316.../172.17.8.103      active        running  
node-etcd@3000.service             767d4316.../172.17.8.103      active        running  

That looks great, bot node-etcd* services are in active and running state. Let’s check if we can access the server.

Result

Now that we’ve started our Node.js app on our CoreOS cluster, let’s check whether we can access the app via a browser. Just use the IP address of the server where the service was loaded and started on. In our example, it’s the server with IP address 172.17.8.103. Open the browser and access the Node.js server on port 3000 because we forwarded the port 3000 from the docker container to the host machine.

Node.js app running in CoreOS cluster and reads keys from etcd

Booyah! Our app is running and if you set any key using etcdctl, you can read and display the value using our Node.js app. Set random values using etcdctl and check whether they’re accessible.

Problems

While preparing this guide, we ran into various docker issues and it took some time to figure out what caused the errors. We describe our solution strategies in the following sections.

Can’t Connect to the Docker Daemon

While preparing the docker container directly on our host machine (running OS X), we hat some issues to run docker commands. Every time we wanted to create a new docker container, we just got the following message:

Cannot connect to the Docker daemon. Is the docker daemon running on this host?  

Fix:

We needed to run a boot2docker initialization process. Whatever caused these issues could be fixed by just running the following command:

$(boot2docker shellinit)

Docker Client and Server don’t have the same version

Once we got the problem above fixed, another error message occurred and we began to search for a solution having the version mismatch.

Error response from daemon: client and server don't have the same version (client: 1.21, server: 1.16)  

Fix:

Just update boot2docker on your machine using the following command. It will download and install the images and there’s no need for additional configurations.

boot2docker upgrade  

What Comes Next

Within this post, you’ve learned how to create a docker container including your Node.js app. Further, we’ve committed and pushed the docker container to your Docker Hub account and used it later for deployment on your own CoreOS cluster. Once we had a prepared docker container, we created the unit files to start the app on one of our cluster members.

The next post will cover app scalability. We’re going to run multiple instances of an app on different hosts within the cluster.


Additional Resources

Explore the Library

Find interesting tutorials and solutions for your problems.