- Home
- Learn Linux
- Learn Electronics
- Raspberry Pi
- Programming
- Projects
- LPI certification
- News & Reviews
Welcome to the companion guide for Part 2 of the CM4 Home Server series. Below you will find the configuration files and commands used in the video to set up your NVMe-powered server.
Note: If you haven't configured your NVMe boot yet, check out Part 1 to set your EEPROM boot and install the Operating system
Install Samba using:
sudo apt install samba samba-common-bin
If you do want to use password authentication then you should used the smbpasswd command. If you are using guest access you can just add the following.
Append the following to /etc/samba/smb.conf for guest-accessible, read-only shares.
[tv-film]
path = /extdata/tv-film
browseable = yes
read only = yes
guest ok = yes
force user = nobody
[music]
path = /extdata/music
browseable = yes
read only = yes
guest ok = yes
force user = nobody
The Docker website provides a handy script which can be used to install Docker and it's dependencies.
curl -fsSL https://get.docker.com -o get-docker.sh
sudo sh get-docker.sh
After installing you can add permission using:
sudo usermod -aG docker $USER
Test it is setup correctly using:
docker run hello-world
Portainer provides a web interface which makes managing docker containers a breeze. Install using.
mkdir -p ~/docker_data/portainer
docker run -d -p 8000:8000 -p 9443:9443 --name portainer \
--restart=always \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/docker_data/portainer:/data \
portainer/portainer-ce:latest
To connect open a web browser and enter your IP address followed by port 9443. E.g.
http://homeserver.local:9443
You will likely get a warning about the use of a local TLS certificate which you can ignore for use on a local home network. If however you are setting it up for external access then you will want to register a certificate.
Login to portainer and setup a username and password as soon as possible to ensure that nobody is able to intercept the setup process!
In Portainer, create a new stack named "monitor" and paste the following YAML configuration:
version: '3.8'
services:
glances:
image: nicolargo/glances:latest
container_name: glances
restart: always
environment:
- "GLANCES_OPT=-w"
ports:
- "61208:61208"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- /:/host:ro
network_mode: host
pid: host
This stack sets up a lightweight web server to host your custom home dashboard. In the video I use a two step process, but you can setup both Nginx and PHP in one step. Create the file ~/webserver/nginx/default.conf
server {
listen 80;
server_name localhost;
# This must match the folder path inside the container
root /usr/share/nginx/html;
index index.php index.html index.htm;
location / {
try_files $uri $uri/ =404;
}
# Pass PHP scripts to the PHP-FPM container
location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
Then using portainer choose "Add Stack" and use the following YAML config.
services:
web:
image: nginx:latest
container_name: nginx_server
restart: unless-stopped
ports:
- "80:80"
volumes:
- /home/$USER/webserver/html:/usr/share/nginx/html:ro
# Mount the custom config file we just made
- /home/$USER/webserver/nginx/default.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- php
php:
image: php:fpm
container_name: php_server
restart: unless-stopped
volumes:
# PHP needs access to the exact same files as Nginx to process them
- /home/$USER/webserver/html:/usr/share/nginx/html
Remember to replace ${USER} with your actual username if not using environment variables.
Whilst I usually create my web pages by hand in this case I used Gemini to create the page for me. This is the prompt I used:
Please create a dynamic web page powered by PHP.
The page should create large click through images which can be used to access local resources on my home server. This should look similar to how a media server provides large buttons for the media.
Incorporate a background image “background.png” which should be scaled to completely fill the page but maintain the image aspect ratio.
Have a clear title in a large font across the top of the page. This should say “Home Server”
The main part of the page should then be a grid view displaying links to the different services. Each will have a thumbnail image. The services should be loaded from a json formatted config file services.json
The json file is structured as a list of entries. Each entry has a “name” - which should be used as the title when hovering over the image, a “url” which is accessed when clicking on the image and an “image” which provides the thumbnail image. The images should be taken from the image directory.
Please update the background.png location to the images directory.
Please can you set all the other images to 1:1.
This is the PHP web page that it created.
<?php
// Load services from the JSON file
$services_json = file_get_contents('services.json');
$services = json_decode($services_json, true);
?>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Home Server Dashboard</title>
<style>
body {
margin: 0;
padding: 0;
font-family: 'Segoe UI', Roboto, Helvetica, Arial, sans-serif;
color: white;
background: url('images/background.png') no-repeat center center fixed;
background-size: cover;
min-height: 100vh;
}
.overlay {
background: rgba(0, 0, 0, 0.4);
min-height: 100vh;
padding: 60px 20px;
box-sizing: border-box;
}
h1 {
text-align: center;
font-size: clamp(2.5rem, 8vw, 4.5rem);
margin-bottom: 60px;
letter-spacing: 2px;
text-shadow: 0 4px 10px rgba(0,0,0,0.8);
}
.grid-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 40px;
max-width: 1400px;
margin: 0 auto;
}
.service-card {
position: relative;
aspect-ratio: 1 / 1;
overflow: hidden;
border-radius: 20px;
box-shadow: 0 10px 30px rgba(0,0,0,0.6);
transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);
background: rgba(255, 255, 255, 0.1);
backdrop-filter: blur(5px);
}
.service-card:hover {
transform: translateY(-10px) scale(1.03);
box-shadow: 0 20px 40px rgba(0,0,0,0.8);
outline: 4px solid rgba(255, 255, 255, 0.5);
}
.service-card img {
width: 100%;
height: 100%;
object-fit: cover;
display: block;
}
@media (max-width: 600px) {
.grid-container {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
gap: 20px;
}
}
</style>
</head>
<body>
<div class="overlay">
<h1>Home Server</h1>
<div class="grid-container">
<?php if ($services): ?>
<?php foreach ($services as $service): ?>
<div class="service-card">
<a href="<?php echo htmlspecialchars($service['url']); ?>"
title="<?php echo htmlspecialchars($service['name']); ?>">
<img src="images/<?php echo htmlspecialchars($service['image']); ?>"
alt="<?php echo htmlspecialchars($service['name']); ?>">
</a>
</div>
<?php endforeach; ?>
<?php else: ?>
<p style="text-align:center;">No services found. Check your services.json file.</p>
<?php endif; ?>
</div>
</div>
</body>
</html>
In future I'll be adding details of how to install the JellyFin media server to provide a easy to use server for videos and music. Please Subscribe to the PenguinTutor YouTube Channel to get notified of future updates.