DEV Community

Cover image for Building an interactive Raspberry Pi dashboard with Laravel, Grafana, and Docker
Andrew Schmelyun
Andrew Schmelyun

Posted on

Building an interactive Raspberry Pi dashboard with Laravel, Grafana, and Docker

If you'd like to skip the following article and just check out the code, you can view everything on this GitHub repository: github.com/aschmelyun/laravel-grafana-dashboard

Feel free to add any questions or bugs you might have to the issues section, and I'll be sure they get answered.

Backstory

I'm really interested in electronic engineering, specifically using it to record data and analytics around my house. I've been monitoring the temperature and humidity on my back porch for over a year using a Raspberry Pi Zero and a DHT22 sensor, pushing the data every minute to a more powerful Raspberry Pi 3 Model B in my living room.

Before last weekend, that larger Raspberry Pi was running Apache with an older version of PHP. In the default /var/www/html directory was a Laravel 5 application that would grab this data and store it in an (also outdated) MySQL database. The front-end was a basic Vue-powered chart showing the last 24 hours of temperature and humidity, as well as the current values.

Picture of a phone with current temperature, humidity, and a graph of the last 24 hours for both

It worked, but I wanted to add more to it. I recently purchased some NodeMCU ESP8266 modules with the goal of monitoring my power usage and indoor air quality. I had kind of thought about ways to deal with new graphs, or putting together some kind of slideshow design, when I ran across Grafana on a Reddit post.

Usually it's used in combination with some kind of server monitoring software, displaying graphs of CPU/RAM usage, core temps, etc. However, I thought it would be a perfect plug and play solution for displaying these data points I've accumulated over time, all I'd need to do is build some simple MySQL queries to pull the data out that I needed. Plus, it looked pretty great out of the box, and was heavily customizable.

Screenshot of a Grafana dashboard showing multiple graphs and charts

So, I decided to scrap everything that was in the Raspberry Pi 3 and start from scratch. The only thing I kept was the data I'd been recording for a year, but everything else was based off a fresh install of Rasbian.

At the start of this project, I honestly didn't think or know if my Raspberry Pi 3 could handle running a Docker network. It's not exactly slow, but not super beefy either, and with the processor being in the ARM family I thought that might have presented some problems. Putting that aside though, the reason that I really wanted to move in this direction was upgradability and containerization.

I love working with Docker because I can upgrade the services that my app runs on without having to touch what's running on the actual hardware. No upgrading PHP, MySQL, or Linux itself in fear that your application might have a compatibility issue afterwards.

So, let's get started!

Setting Up Docker

The first thing we need to do is install the Docker package and requirements for docker-compose on our Raspberry Pi. Open up a terminal and run the following commands:

sudo apt-get update && sudo apt-get upgrade
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
Enter fullscreen mode Exit fullscreen mode

The above commands will update your system's package list as well as any existing services. Then, we download the convenient Docker installation file to our current directory and run it with sh.

After the installation runs (it'll take a few minutes to fully install and finish up), we need to add a non-root user to the docker group. On Raspberry Pi's the user we'll want to target is pi, so we can run the following command to do that:

sudo usermod -aG docker pi
Enter fullscreen mode Exit fullscreen mode

We can then check that Docker was installed by seeing what version is running on our system:

docker version
Enter fullscreen mode Exit fullscreen mode

Note: If the above throws an error, you may need to restart your Raspberry Pi before trying again.

We're going to be building our container network with docker-compose, and in order to do that we'll need to add some more packages to our system. The following commands will install the dependencies and then use Python's pip package manager to run the docker-compose installation:

sudo apt-get install -y libffi-dev libssl-dev
sudo apt-get install -y python3 python3-pip
sudo pip3 install docker-compose
Enter fullscreen mode Exit fullscreen mode

Okay! That should have all finished up successfully, and now we can move on to our actual Docker configuration.

Configuring Services

Now that we have Docker installed on our Raspberry Pi, we have to add the containers that our system will use for its web server, data retention, and to display the data back to a browser. Let's create a directory on our Raspberry Pi, I have mine at /home/pi/server, and inside of that we'll make a file called docker-compose.yml.

To start off with, we'll need to specify the docker-compose version we're using, and set up a basic network name that'll tie each of these services together. Add this to the top of your docker-compose.yml file:

version: '3'

networks:
  laravel:
Enter fullscreen mode Exit fullscreen mode

This file will let us determine what containers (services) our Docker network will consist of. This tutorial is about Laravel, MySQL, and Grafana, so I think to start off with we'll need at least the following:

  • PHP
  • Nginx
  • MySQL
  • Grafana

For each of the services, a block needs to be created that configures the port that's exposed on the system, the volumes and directories mounted to it, and the actual Docker image it's built off of.

We'll go one by one through these, explaining what's being added. For each of these, add them to the bottom of your docker-compose.yml file:

nginx:
  image: nginx:stable-alpine
  container_name: nginx
  ports:
    - "8080:80"
  volumes:
    - ./src:/var/www/html
    - ./config/nginx.conf:/etc/nginx/conf.d/default.conf
  depends_on:
    - php
    - mysql
  networks:
    - laravel
Enter fullscreen mode Exit fullscreen mode

The above block contains our Nginx configuration. Personally, I like using it better than Apache nowadays, and it especially plays well with Docker setups due to the light-weight nature of the Alpine distro it's attached to.

We're exposing port 8080 on here, which means that if you visit your Raspberry Pi's local IP address on your browser, you'll only be able to access your web server if you navigate to the :8080 port.

We have our src directory linked to the web server root, and a config file (which you can see in the GitHub repo) attached to the directory that Nginx expects it to be in.

mysql:
  image: 459below/mariadb-armv7
  container_name: mysql
  ports:
    - "3306:3306"
  volumes:
    - ./mysql:/var/lib/mysql
  environment:
    MYSQL_DATABASE: homestead
    MYSQL_USER: homestead
    MYSQL_PASSWORD: secret
    MYSQL_ROOT_PASSWORD: secret
    SERVICE_TAGS: dev
    SERVICE_NAME: mysql
  networks:
    - laravel
Enter fullscreen mode Exit fullscreen mode

Our MySQL service sets up a database at the default 3306 port, and adds in a few environment variables to help us configure the credentials of the database that's created.

The image that this is built off of, 459below/mariadb-armv7, should be the only thing that might need to be changed depending on the architecture of your Raspberry Pi. Mine (A 3 Model B) uses the ARM V7 processor. If yours is different, just search for MariaDB with your processor type on the Docker hub and find a relevant image to use instead.

A volume is mounted here as well, attaching from the local mysql directory, which will ensure that our data remains preserved if we need to restart the Docker network or the Raspberry Pi itself.

php:
  build:
    context: .
    dockerfile: Dockerfile
  container_name: php
  volumes:
    - ./src:/var/www/html
  ports:
    - "9000:9000"
  networks:
    - laravel
Enter fullscreen mode Exit fullscreen mode

The above builds out a simple PHP service. However, instead of using an image straight from Docker, it's using a locally-created Dockerfile. We're doing this because we need to install some additional dependencies, which you'll see below.

FROM php:7.4-fpm-alpine

WORKDIR /var/www/html

RUN docker-php-ext-install pdo pdo_mysql
Enter fullscreen mode Exit fullscreen mode

The above is our Dockerfile, the FROM line specifies the image we're building off of, and at the bottom you can see the PDO dependencies that we're requiring during the install.

grafana:
  image: grafana/grafana:latest
  container_name: grafana
  volumes:
    - ./src:/var/www/html
    - ./config/grafana.ini:/etc/grafana/grafana.ini
    - ./grafana:/var/lib/grafana
  ports:
    - "80:3000"
  depends_on:
    - mysql
  networks:
    - laravel
Enter fullscreen mode Exit fullscreen mode

Finally, we have our Grafana service. Building off of the latest Grafana Docker image, we use the 80 port externally to expose it directly when our Raspberry Pi is accessed through a web browser. No need to specify the port in order to see our graphs!

A configuration file is optional, but by default Grafana has an authentication screen. I've attached a basic configuration that removes it, since everything is on a local home network and isolated from the internet. Additionally, just like with the MySQL service, we're using a local grafana directory to keep the data preserved.

You also might notice that we've added the MySQL service to the depends_on option. This is because Grafana needs that in order to function, so it tells Docker to spin that container up first, regardless of the position of the services in our main docker-compose.yml file.

Now, we're good to build our network! Run the following command to get everything up and started:

docker-compose up -d --build
Enter fullscreen mode Exit fullscreen mode

The -d flag tells Docker that we want detached mode, which will keep the containers running until brought down. We use --build to let it know that we'll need to build images off of our local Dockerfile before spinning up the containers.

If everything went well, you should see your services lighting up green, and we can move on to our API!

Saving Data

I've chosen Laravel for this project, because, well, it's my bread and butter. I work in it daily, and love using it for complicated and simple sites alike. I'll be using it headless in this application, purely as an API layer to take data in that I'm recording, and save it to our MySQL database. In reality something way more lightweight could be used, but this is what I've chosen.

Purely for space reasons, I haven't included three other services that I've added to docker-compose.yml that'll be used throughout this tutorial. There's one for composer, artisan, and npm, and if you'd like to continue with the below I'd recommend you get them set up. You can view the details for each in the docker-compose file on the GitHub repository.

Going to our src directory, which is the web server root that we specified in our Nginx service, we can install the framework using our Composer service:

docker-compose run --rm composer create-project laravel/laravel .
Enter fullscreen mode Exit fullscreen mode

Once it's finished up, we can work on our data layout. I'm storing everything (past and future data points) in a single database table. This way, I can add or remove different values easily without having to run migrations each time I add a new monitor.

The table will be called datapoints, and we can create both a model and a migration for it in one step by using the artisan service:

docker-compose run --rm artisan make:model Datapoint --migration
Enter fullscreen mode Exit fullscreen mode

The table is incredibly simple, consisting of three different columns:

Schema::create('datapoints', function (Blueprint $table) {
    $table->id();
    $table->string('name', 156);
    $table->string('value', 156);
    $table->string('cast', 156)->default('string');
    $table->timestamps();
});
Enter fullscreen mode Exit fullscreen mode

We're wanting:

  • A name, something like temperature or humidity or ping. This is the label of our data point to be stored.
  • A value, the representation of our data point, like 76.5 or 250ms.
  • A cast, not necessary right now but maybe useful later, if you want to create a GET method and determine how data should be returned back.

Note: We're specifying 156 for the length on each of these due to a bug in the version of MariaDB that I had to use. Normally a varchar defaults to 255, but it causes an error to be thrown when data is saved. Anything under 160 should be safe.

And the model is even simpler, all we need to do is just add a $guarded property to Datapoint.php in order to easily add data through our controller:

class Datapoint extends Model
{
    protected $guarded = [];
}
Enter fullscreen mode Exit fullscreen mode

That's the only model we'll be using! Before we run this migration though, we'll need to make a quick adjustment to our Laravel app's .env file. In order to connect to our Docker network's database, open it up and change your MySQL settings to the below:

DB_CONNECTION=mysql
DB_HOST=mysql
DB_PORT=3306
DB_DATABASE=homestead
DB_USERNAME=homestead
DB_PASSWORD=secret
Enter fullscreen mode Exit fullscreen mode

Alright, now we can go to our terminal and run docker-compose run --rm artisan migrate, which will create our table that we just built!

The final part of our app is saving the actual data. For that, we need a route to have our monitors POST to. Open up your routes/api.php file and add in the following:

Route::post('/datapoint/create', [
    'as'    => 'datapoints.store',
    'uses'  => 'DatapointController@store'
]);
Enter fullscreen mode Exit fullscreen mode

We could also use the routes/web.php file as long as we add that route to the $except array in Middleware/VerifyCsrfToken.php.

Regardless of what you choose, the next step is to create that controller. You can do this manually, or with artisan, which is my preferred way: docker-compose run --rm artisan make:controller DatapointController.

Our controller needs the single method we just specified, store, which will take our incoming data and parse it out to our database:

public function store(Request $request)
{
    $request->validate([
        'name' => 'required',
        'value' => 'required'
    ]);

    return Datapoint::create([
        'name' => $request->input('name'),
        'value' => $request->input('value'),
        'cast' => $request->input('cast')
    ]);
}
Enter fullscreen mode Exit fullscreen mode

So now, on our monitor boards (or in an app like Postman), we can run a POST to {IP_ADDRESS}:8080/api/datapoints/create, replacing IP_ADDRESS with the local IP of your Raspberry Pi, and your data's name, value, and cast should be added to the MySQL datapoints table.

Screenshot of Table Plus displaying data points from a Laravel app

Now we can finally move on to actually displaying our data!

Customizing Our Display

The first time you navigate to your Raspberry Pi's IP address in a web browser, you'll be presented with multiple options for tying in data. Simply select MySQL, and then add in the same credentials that we used in the Laravel .env file earlier.

  • Host: mysql
  • Database: homestead
  • Username: homestead
  • Password: secret

After connecting, we can start creating our first dashboard! A dashboard is made up of different panels, each one containing a graph, chart, or some other kind of visualization that's created by data brought in through a query pieced together.

Let's create a simple line chart on our dashboard that'll show the value of one specific data point over time. For instance, temperature. The query built for that would look something like this:

SELECT
  $__timeGroupAlias(created_at,$__interval),
  avg(value) AS "temperature"
FROM datapoints
WHERE
  $__timeFilter(created_at) AND
  name = 'temperature'
GROUP BY 1
ORDER BY $__timeGroup(created_at,$__interval)
Enter fullscreen mode Exit fullscreen mode

You can also use the visual builder, which is a little easier and gives you helpful auto-completion for things like column values and property names.

Screenshot of Grafana with the query builder open

What the above represents is a query that:

  • Grabs the value column's data as 'temperature'
  • Filters for just items whose name equals temperature
  • Groups by the $__interval which is calculated based on the spacing of datetimes in the created_at column

These points are then graphed, in real-time, on the chart above. Clicking down on the left-hand menu, you're presented with a slew of options to modify the look and layout of your panel. We're doing a line chart of temperature, so I've added Fahrenheit symbols to the lefthand side and static values to keep the chart between 40 and 110 degrees. It doesn't get much colder or hotter than that here in Florida.

Screenshot of Grafana with the graph editor open

Now if I save, and if I'm getting data sent to my Raspberry Pi's web server on a regular basis, I can visit the Pi's IP address and see my graph populate in real time!

Because of our data structure that we set up earlier, I can add as many, and as different, data points as I want on here to graph. Ping time, internet usage in MB, energy usage in kWH, moisture levels in my garden, CPU temperature in my server, air quality PPM in my house, etc. As long as I give them each a unique name in the datapoints table, I can graph them or display different attributes in my Grafana dashboard using just some simple queries.

Conclusion

Okay, this might have gone on a little long, BUT you now know how to completely set up your own data dashboard with a Laravel-driven API, MySQL database, Grafana frontend, and Docker network, all on a single Raspberry Pi! It's an awful picture, but here's my current setup with the Grafana dashboard using an old iPad on my bookshelf.

Picture of an iPad running Grafana on a bookshelf

The best part is that if you want to add in a new service of your own (like a different web server, or Python for the backend language instead of PHP), it's insanely easy and doesn't require you touching the software on the actual Raspberry Pi at all.

Feel free to clone the same repo that I'm using on my project box at home, play around with it, tweak it as much as you want, and try it out for yourself. If you have any questions or would like to discuss any web development-related topics, I'm always available on Twitter.

Top comments (6)

Collapse
 
m4t5 profile image
Mat S

Hi Andrew,

Thanks for a well written and useful post. I'm using it to connect a pi-w to an old model B which has been logging temperature (heating, ambient) data for my house for about 5 years. I'm a docker newbie though, and the instructions for creating docker-compose.yml omit the services: line and the indents which follow. This is enough to stop it working (even though yamllint didn't complain).

The git repo saved the day and the downloads are nearly done - got to go!

Thanks again

Mat

Collapse
 
aschmelyun profile image
Andrew Schmelyun

Glad the repo helped Mat, I'll have to make some quick edits to those in the article!

Also, if you don't mind me asking, how'd you find this post?

Collapse
 
m4t5 profile image
Mat S

This post was linked from the stack overflow newsletter (The Overflow #20: Sharpen your skills).

I can't get beyond docker-compose up -d --build though. Tried the dockerfile in your repo, tried some other containers, played around with it but it always fails (output below).

Fresh Raspbian 10 on a Pi Zero 1.1, 16Gb sd card. Google didn't help - any ideas?

Many thanks

~/laravel-grafana-dashboard-master $ docker-compose up -d --build

Building php
Step 1/3 : FROM php:7.4-fpm-alpine
---> 62ddc4288d97
Step 2/3 : WORKDIR /var/www/html
---> Using cache
---> 2c1d49e9f256
Step 3/3 : RUN docker-php-ext-install pdo pdo_mysql
---> Running in 5e66cc8f0056
ERROR: Service 'php' failed to build: The command '/bin/sh -c docker-php-ext-install pdo pdo_mysql' returned a non-zero code: 139

Collapse
 
tylerlwsmith profile image
Tyler Smith

Hey this is nifty!

Collapse
 
huncyrus profile image
huncyrus

What's the PI load with this amount of docker?

Collapse
 
roelofjanelsinga profile image
Roelof Jan Elsinga

I'm just starting out in the "smart home" journey and this post is what I needed! Thank you for this 😁