Cover image for Run your Node.js application on a headless Raspberry Pi

Run your Node.js application on a headless Raspberry Pi

bogdaaamn profile image Bogdan Covrig Updated on ・9 min read

Recently I've got a little piece of hardware (Raspberry Pi 2 Model B) on my desk. Rather than have it sit on its ass all day, I got a little Node.js application up and running through Nginx.

Get that Raspberry Pi up and running

A headless install doesn't require any kind of extra hardware (such as screens or keyboard), so everything that you need is the Raspberry Pi itself, a microSD (or SD, depending on your hardware) card and an internet connection (wired or wireless, again depending on your Raspberry).

1. Get the Raspbian Stretch image

Raspbian is the most optimized OS for Raspberries and I use it when I need a minimum and fast setup. Just go the official website and download the latest version of Raspbian Stretch Lite.

Download Raspbian Stretch Lite

2. Burn that image

Insert your card in your PC and burn the Raspbian image on it.

I followed these instructions (Linux/Windows also available) because I prefer using my terminal, but Etcher (a graphical alternative on steroids) is also available on all platforms.

3. Enable headless SSH connections

SSH is not enabled by default in Raspbian, so you will have to do it before you boot the card for the first time.

After the installation, go to boot/ and create a file called ssh (no extension).

touch ssh

4. Boot that Raspberry Pi

Insert the card, the power source, and the network cable. After the green LEDs stop blinking, your Raspberry Pi is good to go!

My Raspberry Pi good to go!

5. Find that Raspberry Pi

So you have power, network and an OS, no keyboard, no screen. How do you connect to the Raspberry Pi?

In order to SSH into it, you will have to find it in your network first. Supposing that you connected the Raspberry to your home network, you can see all the devices (MAC and IP addresses) in your LAN from the ARP table. Simply run in your terminal (working on all platforms)

arp -a

and you will find your Raspberry Pi right there.

fritz.box ( on en1 ifscope [ethernet]
raspberrypi.fritz.box ( on en1 ifscope [ethernet]

In my case, fritz.box is my router and right after is my Raspberry Pi. So from now on, I will connect to it through address.

More about ARP tables and how you should find your devices there.

6. Finally SSH into that Raspberry Pi

The default credentials are

username: pi
password: raspberry

SSH into the Raspberry Pi. On Mac or Linux you can just simply run

ssh pi@

while on Windows are a few alternatives such as Putty or the default config on Cmder.

Get your Node.js application up and running

You are in! You should get your Node.js application up, so the following steps are run through SSH, on your Raspberry Pi.

1. Install Node.js on Raspbian

There are a lot of ways to install Node.js on a Linux machine, but I always follow NodeSource's instructions, being the safest way I ever did.

For Node.js v11.x I ran

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

Anyways, curl -sL https://deb.nodesource.com/setup_11.x | bash - will provide more instructions if you need more tools or add-ons.

Check if Node.js and npm are installed properly.

$ node -v

$ npm -v

For other versions or troubleshooting take a look to NodeSource's comprehensive docs. Raspbian is a Debian based OS, so look for Debian instructions.

GitHub logo nodesource / distributions

NodeSource Node.js Binary Distributions

NodeSource Node.js Binary Distributions



This repository contains documentation for using the NodeSource Node.js Binary Distributions via .rpm, .deb and Snap packages as well as their setup and support scripts.

If you are looking for NodeSource's low-impact Node.js performance monitoring platform, please get started here.

Please file an issue if you are experiencing a problem or would like to discuss something related to the distributions.

Pull requests are encouraged if you have changes you believe would improve the setup process or increase compatibility across Linux distributions.

Table of Contents

Debian and Ubuntu based distributions

Available architectures:

NodeSource will continue to maintain the following architectures and may add additional ones in the future.

  • i386 (32-bit)—not available for Node.js 10 and later

If you choose to write or paste the code, quickly install vim, it will make our lives easier and later I will walk you through, don't worry.

sudo apt-get update
sudo apt-get install vim -y

2. Get your Node.js app

Write, copy-paste or clone the Node.js application. For testing purposes, I created app.js file.

cd ~
vim app.js

I pasted the following boilerplate

const http = require('http');

const PORT = 3000;

const server = http.createServer((req, res) => {
  res.statusCode = 200;
  res.setHeader('Content-Type', 'text/plain');
  res.end('Hello dev.to!\n');

server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}.`);

If vim is too overwhelming you can try to use other ways as nano. But just to be sure, this is a really quick follow-up:

  1. Open (or create) the file with vim app.js.

  2. Now, vim is in the normal mode and it's waiting for your command. Press i to go in the insert mode, this will allow you write code.

  3. Type or paste your code now, exactly how you'd do it in your editor.

  4. If you're done writing, press esc so you go back to the normal mode so you can command vim to save and exit.

  5. Usually, vim commands start with :. Gently press : followed by w for writing and q for quitting. You can actually see the commands that you're typing on the bottom of your terminal. Press enter to acknowledge the commands.

  6. Taa-daaaaa. app.js is saved.

If you want to do more crazy tricks with vim, follow-up this beginner guide and you will see that vim is not that bad.

3. Finally run the Node.js application


$ node app
Server running at on port 3000.

and your Node.js app will run on localhost:3000/.

Because none of the ports are opened by default, you can test the app only from your Raspberry Pi. Open a new tab of the SSH connection and run

curl localhost:3000

and you should get

Hello dev.to!

4. Install PM2

Of course that you want your application daemonized (in background) and of course that you want your application to start when the system is restarting. PM2 will provide all of this.

Stop your Node.js application (ctrl + C) and proceed to installation.

We will use npm to install PM2 globally -g.

sudo npm install -g pm2

Start the application with PM2

To start app.js with PM2 run

pm2 start app.js

and you should see

[PM2] Starting /home/pi/app.js in fork_mode (1 instance)
[PM2] Done.
│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu │ mem       │ user │ watching │
│ app      │ 0  │ N/A     │ fork │ 738 │ online │ 0       │ 0s     │ 0%  │ 21.8 MB   │ pi   │ disabled │

Now app.js is daemonized running. You can test it as we did before with curl localhost:3000.

Bonus: if the app crashes, PM2 will restart it.

PM2 startup

The pm2 startup command will generate a script that will lunch PM2 on boot together with the applications that you configure to start.

pm2 startup systemd

will generate

[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u pi --hp /home/pi

Copy the generated command and run it.

sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u pi --hp /home/p

This created a system unit that will start PM2 on boot. When the system will boot PM2 will resurrect from a dump file that is not created yet. To create it run

pm2 save

This will save the current state of PM2 (with app.js running) in a dump file that will be used when resurrecting PM2.

That's it! Your application is currently running and in case of a restart, it will start when the system boots.

PM2 daemon

You will be able to check anytime the status of your application with pm2 list, pm2 status or pm2 show.

$ pm2 list
│ App name │ id │ version │ mode │ pid │ status │ restart │ uptime │ cpu  │ mem       │ user │ watching │
│ app      │ 0  │ N/A     │ fork │ 451 │ online │ 0       │ 96m    │ 0.2% │ 31.8 MB   │ pi   │ disabled │
$ pm2 show app
│ status            │ online                           │
│ name              │ app                              │
│ version           │ N/A                              │
│ restarts          │ 0                                │
│ uptime            │ 97m                              │
│ script path       │ /home/pi/app.js                  │
│ script args       │ N/A                              │
│ error log path    │ /home/pi/.pm2/logs/app-error.log │
│ out log path      │ /home/pi/.pm2/logs/app-out.log   │
│ pid path          │ /home/pi/.pm2/pids/app-0.pid     │
│ interpreter       │ node                             │
│ interpreter args  │ N/A                              │
│ script id         │ 0                                │
│ exec cwd          │ /home/pi                         │
│ exec mode         │ fork_mode                        │
│ node.js version   │ 11.10.0                          │
│ node env          │ N/A                              │
│ watch & reload    │ ✘                                │
│ unstable restarts │ 0                                │
│ created at        │ 2019-02-17T14:14:35.027Z         │

There is a lot of greatness within PM2 that you can use, read more about logs and processes below.

Make use of a Reverse Proxy

As I mentioned before, none of the ports on your devices are publicly open yet, so you cannot access your Raspberry Pi from the outer world. There are a ton of reason why you should or shouldn't use a reverse proxy for your Node.js application. Because of scalability and security reasons (and also is really simple to setup and manage), I will use Nginx as a Reverse Proxy Server for this application.

0. Don't use a Reverse Proxy :(

If you plan to use a Reverse Proxy don't follow this step otherwise you will mess up the ports (like having 80 and 3000 opened at the same time).

An uncomplicated way to go without a Reverse Proxy is to use ufw to allow some of the ports to allow incoming traffic. But note that this might be a big security flaw.

Install it by running

sudo apt-get install ufw

A quick sudo ufw status verbose will show us that ufw is currently inactive. Before you enable it, you should allow all the SSH traffic to your device, so the connection is not disturbed.

$ sudo ufw allow ssh
Rules updated
Rules updated (v6)

Now you can enable it.

sudo ufw enable

Another quick sudo ufw status verbose will show that all incoming SSH traffic is allowed. All the outgoing traffic is allowed, so don't worry about it. Now just go on and allow connections on 3000, the port of your application.

sudo ufw allow 3000

Now you can access from the outside of the world! You can type your device's address followed by the port in your browser.

Outer world

1. Install NGINX

I used Nginx as a Reverse Proxy Server to redirect all the traffic to/from port 80 to my application, on port 3000. Install Nginx running

sudo apt update
sudo apt install nginx

After the installation, Nginx will be running right away. The default port opened is 80 and you can test it by browsing to your Raspberry's address.

Welcome to Nginx, dev.to

2. Configure the Reverse Proxy Server

There is a lot to say about Reverse Proxies, but we will stick to basics now.

You will edit the default configuration (that serves the HTML page that you saw in your browser) to make the proper redirects.

sudo vim /etc/nginx/sites-available/default

If you are not familiar to Nginx, /etc/nginx/sites-available/default is a long, confusing file. I will get rid of all the comments so you can see it better.

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
            # First attempt to serve request as file, then
            # as directory, then fall back to displaying a 404.
            try_files $uri $uri/ =404;
            # proxy_pass http://localhost:8080;
            # proxy_http_version 1.1;
            # proxy_set_header Upgrade $http_upgrade;
            # proxy_set_header Connection 'upgrade';
            # proxy_set_header Host $host;
            # proxy_cache_bypass $http_upgrade;

You will need the basic configuration, therefore leave it be. You will make changes to location / { block.

Uncomment the commented section inside that block, change the port to 3000, get rid of the first lines and that exact configuration is a Reverse Proxy (or just copy the following code).

server {
        listen 80 default_server;
        listen [::]:80 default_server;

        root /var/www/html;

        index index.html index.htm index.nginx-debian.html;

        server_name _;

        location / {
            proxy_pass http://localhost:3000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection 'upgrade';
            proxy_set_header Host $host;
            proxy_cache_bypass $http_upgrade;

Check for syntax errors in Nginx with

sudo nginx -t

and finally restart the Nginx server.

sudo systemctl restart nginx

Test it out by browsing to your Raspberry's address.

Finally the needed outer world

Done! All the requests to your Raspberry's address will be redirected to your Node.js application.

Note that this is a basic configuration of Nginx, you can discover more about other features here.


Now you are done! So you got a Node.js application running (daemonized) on a headless Raspberry Pi that deals with requests through an Nginx Reverse Proxy Server.

Hopefully, this was a comprehensive enough guide, but I am open to discussions and questions below. Let us know what you've experienced or what other alternatives you found on the way.


Editor guide
chrisakers profile image
Chris Akers

Thanks for this, Bogdan. This was very easy to follow and clear. Although I ran into an issue that caused me to pull out my hair for several hours. I was following your example exactly. Everything was working well up to the point when I tried to make a request to the server from outside the Raspberry Pi. I went the ufw route and allowed port 3000. But my laptop on the same LAN could not get a response from the node server. (The same laptop was connected over SSH on port 22.) Running a port scan showed that only 22 was open. I tried many things including disabling/enabling ufw, rebooting, making ufw default allow, disabling ufw and setting iptables directly to the most permissive and simple set of allow rules, etc. Next on the hit list was the router. I went through all the router settings with a fine tooth comb. No help there.

I'm a web developer so I'm super comfortable with the node stuff, but the networking stuff is a bit out of my comfort zone. So I was sure that's where the problem was. There must be some firewall issue somewhere, right?

No. It turns out it was the node server all along. The example code passes the hostname to server.listen. So node is only responding to requests with that exact combination of hostname and port. Trying to hit it using a different hostname from outside the Pi failed every time. Once I changed to using the hostname-less listen method then it worked immediately and the port showed as open when running the port scan. The change:

server.listen(PORT, () => {
  console.log(`Server running on port ${PORT}.`);

It goes to show that sometimes the issue is where you least expect it. I hope this helps someone else if they run into this issue.

bogdaaamn profile image
Bogdan Covrig Author

Great catch Chris! I spend some time taking a look to the documentation, see if I missed the parameters combination, but it seems indeed that it takes the exact combination of hostname and port.

Interesting to take a look at, I expected if you use the reverse proxy to get trough, right?

chrisakers profile image
Chris Akers

Yes, I imagine that the reverse proxy would have worked properly since the hostname would match in that scenario. But I followed the "0. Don't use a Reverse Proxy :(" section since I was only making something for use in my LAN. The assertion at the end of that section is "Now you can access from the outside of the world! You can type your device's address followed by the port in your browser." This is not true since using the device's address will not match the hostname. :(

Thread Thread
bogdaaamn profile image
Bogdan Covrig Author

That's right, yeah. Thank you for the heads up, I've edited the code to a more general case

emh333 profile image
Ethan Hampton

This is great, thanks! Slightly off topic but do you have any experience with constantly running a Pi as a server 24/7 and any issues that might arise? I was thinking of doing something like this for my local network but was curious if there were any other things I might do to increase stability and lifespan. Again, thanks for the article!

lundeee profile image

My Pi 2 is running for more than 3 years non stop. It is constantly measuring and saving enviromental data and serving simple page to check that data.

dvddpl profile image
Davide de Paolis

this sounds similar to something i'd like to do. i'd like to use RasbPi + DHT11 Temperature and Humidity Sensor Module and send data to AWS Dynamo DB and then have a React APP displaying that info. could you please tell me more about your project?

Thread Thread
lundeee profile image

Hey. Goal of my project was just to level the thermostats on the first and second floor of the house. So i can be dad and yell "Don't touch the thermostat" :) I have DHT11 also and used Adafruit_DHT python module. Values are written to plain CSV and i used node.js and chart.js to display it. I use raspery for other thing now, but i left daemon running.

bogdaaamn profile image
Bogdan Covrig Author

Since I started to write this article (one or two weeks back) I didn't unplug it at all. I actually started to test some of my real work on it (more robust APIs, databases, a lot of routes, automatic Postman tests) and never failed me. It uses an unnoticeable part of my home network bandwidth and almost no power at all, so comes in handy to have one around. Unfortunately, I don't know what sort of issues might arise if you use a lot of traffic and I did not monitor the stability. But I will keep you posted in case something goes wrong and maybe I will setup some benchmarking in the following days.

bigorangemachine profile image
Brian Dinga

I made a CSS indexer. I used PhantomJS. There was some incidents of PhantomJS sub tasks not exiting which left some tasks hanging.

It depends on what you are doing. For the most part PM2 will cover what you need.

artemis profile image

Two points:

  • I'd recommend using the Lite image, as it's much lighter, especially when you want to run a headless server (Edit, just re-read it, and I missed the Raspbian Lite part)
  • I'd also recommend directly using SystemD as daemon manager to manage the nodejs process, especially since a systemd file isn't much harder to write, and doesn't require any external dependency/software.
bogdaaamn profile image
Bogdan Covrig Author

Thanks for the tip! Didn't have time before to check SystemD, since PM2 seemed the easiest out of the box way (that also uses SystemD), but will definitely check asap.

dmitryvdovichencko profile image
Dmitry Vdovichenko 💻🤘

Great article)) Thanks for this nice and useful tutorial)) I've mentioned that in the first step when I install NodeJS I should use

curl -sL https://deb.nodesource.com/setup_11.x | sudo bash -

instead of

curl -sL https://deb.nodesource.com/setup_11.x | bash -
dvddpl profile image
Davide de Paolis

this post is awesome. exactly what i needed to get started with RaspPi. I am planning a pet project with RaspPi (and humidity modules) and AWS stack to gather data and store it to DB and then have a React APP to display it from anywhere!

bogdaaamn profile image
Bogdan Covrig Author

That's exactly the kind of cool things that you can do with a Pi! Keep me posted.

mbariola profile image
Massimiliano Bariola

I install pm2 as instructed, but when I try to pm2 start app.js I get a pm2: command not found error. I see no errors during pm2 installation. I had to add

export PATH=$PATH:/opt/nodejs/lib/node_modules/pm2/bin
to my .bashrc
then source .bashrc

to make it work on npm 6.7.0 / 6.9.0, latest version of raspbian

bogdaaamn profile image
Bogdan Covrig Author

Thanks for the heads up! Duly noted.

htalat profile image
Hassan Talat

Nice tutorial! What database can be setup on the pi?

bogdaaamn profile image
Bogdan Covrig Author

You can setup any database that can be setup on Debian.

htalat profile image
Hassan Talat

oh really? i was trying to setup mongodb on my pi3 once. Ran into some issues. will try again

paulc_creates profile image
Paul Caoile

Thank you for this. I will have to try this out since I have few RasPi waiting to be used for good. Do you have recommendations on ufw rules to make the connection more secure?

bogdaaamn profile image
Bogdan Covrig Author

I would allow just the traffic from/to the ports that my API is using.

paulc_creates profile image
ben profile image
Ben Halpern

This looks like a fabulous guide. Insta-bookmark.

bassemmohamed profile image
Bassem Ibrahim👨‍💻🇪🇬

Great post! I am wondering tho can i then access this node server from outside the local network, i assume i will have to do some configuration on my router.

bogdaaamn profile image
Bogdan Covrig Author

Haha, I was thinking the same a few days back. You should be aware that you might not be allowed since the traffic is monitored by your internet provider.

Oopsie, iliegal

This is the source for further reading. My provider states only The Customer is not permitted to rent the Service to a third party, to sell it or to make it available in any way, unless otherwise agreed. which might not be the case though.

But, yes, I assume that you have to setup a sort of domain name for your public IP or so.

bassemmohamed profile image
Bassem Ibrahim👨‍💻🇪🇬

Will definitely investigate further, would be very cool to setup up stuff for personal use. Thanks, keep up the good work 👍👍

lfkwtz profile image
Michael Lefkowitz

Give ZeroTier a look, makes it very easy but still secure.

bushibot profile image

I have hopefully easy question, I was following this to get node app to run background, but it requires a variable and the whole thing seems to fail when deomonizing it.
I'm trying to pass 'node resources/app/main.js --dataPath=$HOME/foundrydata' through PM2 but it seems to break.

bogdaaamn profile image
Bogdan Covrig Author

yep, in order to pass node arguments to pm2 you have to use the --node-args argument. like

pm2 start resources/app/main.js --node-args="--dataPath=$HOME/foundrydata" 

Or you can add it in the pm2 JSON file. You can read more in the tutorial or in the pm2 docs.

danilloalvis profile image
Danilo Torquato

Great article, it helped me a lot, but I had a problem, I'm not able to upload files on node.js, on windows and MacOS I can upload files, but on raspberry not work

ujjwalkr profile image
Ujjwal Kumar

btw do i really need the network cable if i have wireless connection??

bogdaaamn profile image
Bogdan Covrig Author

No, my Pi didn't have wireless connection, therefore I used the cable