CM3588-NAS project

2025-03


Background and technology selection

While I have remote backups of all my data, last year I finally grew tired of not having any local data redundancy, and set on constructing a real NAS server. Additionally, my previous shell server was getting really old and its manufacturer-side OS support had ceased a good while ago, so it was time to find something that could still be updated and where I could move my users.

I didn't want spinning disks and wanted low power usage and small size. When black friday sales in Verkkokauppa.com offered 4TB Samsung disks at ~€250, I snatched four and started looking for a mainboard to use them with. A couple of options were available, but eventually I settled on the CM3588 from FriendlyElec, because it had a beefy ARM processor and four NVME slots for $210.

The board actually arrived before christmas and I flashed and installed it during the holidays. Having never manually created RAID arrays before, I botched it once and had to recreate. I also forgot to update the initrd image after finishing the array sync, which caused any information of the array to be lost and made the system halt on boot. After unattaching everything and dragging it back beside an actual monitor, the computer booted into developer mode and the array could be recovered with a bit of manual fiddling, albeit with the kernel-invented (random?) label md127 instead of md0.

Once the server was up and running, it was time to build a proper case. For aesthetics, of course, but also because the CPU and the NVMEs could overheat without any active cooling. I drew a detailed blueprint and got to work.

Barebones board in a cardboard box
Blueprints

Fabrication

Both the lid, base and sides are cut from an edge-glued 18mm 20cm wide pine board. The lid and base use the full width of the board, while the sides are portioned from a section sliced approximately in half lengthwise, and trimmed further to decrease the total height of the case.

The sides are miter-glued with a 45-degree angle and then glued to the base. Because I more or less winged the dimensions, the circumference of the sides ended up being a smidgen larger than that of the base, but I assumed I could plane away a hair of their thickness and have the sides and base match exactly. Not a bad plan, but I ended up setting the plane to cut too deep, which caused it to bite hard into a knot, ripping it clean off and flinging it across the room. Nothing a bit of glue and sawdust could not fill, though, and the result didn't end up looking too bad. Planing also slighly frayed the ends of one of the sides, but those were hidden with a belt sander.

The lid had some specific functionality I wanted to implement – it had to have a magnet-removable filter screen, which would keep out dust. For this purpose, I ended up buying a ventilation grating of the proper size and a set of tool holder magnets, which ended up being both too big and far too strong. Luckily, my wife had the sense to buy some whiteboard magnets instead, which were just right. The ventilation grating was made from copper, which is not magnetic, but I flattended the screw insets from the inside and glued large steel washers in the corners for a proper grip. Drilling magnet inlays was relatively easy, and being sufficiently careful, I managed to set them almost perfectly flush with the surface.

Lid magnet inset drilling
Lid magnet inset holes
Magnet installed

A 50-micron steel netting was later glued on top of the sparse netting originally present in the grating, so that it would contain typical particle sizes. With everything made from metal, it would also be easy to just rinse the complete filter under a faucet.

In order to snap the ventilation grating firmly at just the right spot, I also chiseled a groove that its edges could be laid into. Some orientations ended up working better than others, but the main function was to avoid any airflow from the sides.

Filter grating
Lid filter groove planning
Lid filter groove after some chiseling

The center hole in the lid could have been done painstakingly through drilling the center away and filing the edges, but a kind Hacklab soul provided guidance in using the CNC mill, saving me a considerable amount of time.

CNC mill program
CNC mill in operation

After finishing with the CNC, I tried to clean up the the lid by sanding it a bit, but didn't take into account how soft the magnets actually were. The sander first separated metal dust and then ground it between the wood fibers, fouling the surface. As the magnets were glued, it was no longer possible to plane the surface away, which made this error permanent.

Furthermore, laquering the lid twisted it quite badly. However, this time it was possible to at least attempt to plane away the twist from the bottom side. Even though I only tried to slice a little bit at at time, once the curve was finally hidden, none of the angles of the lid were square anymore. The end result was some type of a trapezoid prism, but probably not obvious enough to be distracting from a distance.

Assembly

After laquering, it was time for assembly. The holes for connection ports were drilled, chiseled and filed to the right size and the power supply was inserted. Free-hanging items were hot-glued onto the inner walls of the case and the actual connecting holes were sealed with regular wood glue.

Case after gluing
Filing the connection ports
Arranging the cables

Several steps here are not shown in the photographs because I forgot to document them, but the bottom of the case was marked from under the location where the board would sit. Fourteen holes were drilled in square pattern to act as airflow exit holes. Wooden risers for the whole board were made from 1cm round stock, and their ends were inset with regular motherboard studs. These risers were glued to the bottom of the case and the board was screwed onto them like a typical motherboard, albeit with more clearance so that airflow under the board would be unimpeded.

Because apparently nothing can just work without extra work, I had selected two different screw pitches for the aforementioned risers by accident, and any screws I had lying around for the coarser pitch were simply too large to fit through the board mounting holes. Manually filing the threads shallower allowed them to fit, while still attaching to the thread firmly enough. Acceptable, I guess.

It was tricky to appraise what exact kind of connectors would actually translate correctly from the board to the case holes. I had ordered multiple angled RJ45 male-female connectors for this purpose, and naturally the first one I glued in place was of the wrong handedness when considering the final board orientation. Fortunately it was impossible to make this mistake with the power supply cable, so it was packed beside the power brick itself. I allowed my wife do this because she is much better at organizing things neatly.

Attaching NVME heatsinks
PWM controller top
PWM controller bottom

In addition, while the board provided 12V from a row connector direcly beside the power input, this could not be used as-is for noise reasons. As such, a PWM controller was installed to drive the fan. Being lazy, and because the controllers were cheap, I ordered a ready-made part, and then reordered another because I apparently couldn't read and had ordered a model with a temperature switch instead of a potentiometer-driven speed selector. Soldering was needed, because the controller didn't have any wiring of its own, but fortunately the 3.5mm row connector had screw terminals.

The PWM controller was shoddily hot-glued to the case wall and then immediately reheated, removed and reattached because it was in a position which prevented me from accessing the board mounting screws.

When attaching the lid closing mechanisms, I squished the sealant with clamps before installing the hinges and latches, hoping that the box would hold tighter shut this way. The brass screws that came with the hinges were too short and flimsy to securely hold the clasps in place, and since I was afraid that the lid would crack without pilot holes, for which I didn't have a thin enough drill bit anyway, they didn't properly bite into the wood either. The locking mechanism didn't quite end up satisfying or snappy enough for my tastes, so I pre-emptively reinforced both the hinges and clasps with some cyanoacrylate, hoping they wouldn't come loose later.

Shallow pilot holes for lid screws
Latches

The case fan was attached to the lid from the underside with 4mm bolts and nuts, and the interface was cushioned with a felt sealant stip, as was the interface between the lid and the case. It was necessary to slightly deepen the attachment holes in the lid for the the bolts to reach underside of the fan. Being an idiot, I drilled too aggressively and punctured the lid with the larger drill bit, which again forced me to improvise with glue and sawdust.

Cyanoacrylate is indeed man's best friend.

Finally, furniture feet were installed on the bottom of the case in order to allow warm air to escape the case from the formerly mentioned holes below the board. The end result can be seen placed on top of the fireplace (which is never in actual use).

Final inside view
Resting place
Resting place closeup

Addendum

Bill of materials

CM3588 32G RAM + 64G EMMC                   $210 + postages
4x Samsung 990PRO 4T NVME                   €1039.96
4x Akasa SSD heatsink                       €53.95
Wood                                       ~€10
Magnets, latches, hinges, filter, feet     ~€52
Cyanoacrylate + sealant felt               ~€23
50-micron steel netting                    ~€10
RJ45 angled brackets (two, one used)       ~€10
Fan temperature controller (not used)      ~€2
Fan manual PWM controller                  ~€2
3.5mm notched row connectors (one used)    ~€2
Heat-shrink tubing (not used)               €8.8
Hacklab membership                          €20
Laquer, wood glue, wires, solder            Found in closet
-----------------------------------------------------------
Total                             more than €1440
Catalog of time used

Procrastination                          7 months
Case creation, finishing and assembly   15 hours
Software installation and tinkering      ? hours
-----------------------------------------------------------
Total                                    a lot

Hints

It is probably prudent to cover some pitfalls and configuration suggestions for anyone making their own builds using this board.

Image selection

Instead of the manufacturer image, I used the Armbian OpenMediaVault one: https://dl.armbian.com/cm3588-nas/Bookworm_vendor_minimal-omv

The reason for this is, that the manufacturer image does not have a proper option for installation to EMMC. It creates a boot volume, yet hides it, which makes it impossible to actually update the kernel. I.e. it feels like a temporary solution used to demonstrate board capablities instead of something you actually want to use.

MOTD screen

CPU governors

At least in my installation, the processor speed does not seem to be detected correctly and the board runs too slowly out of the box. A manual scipt is needed to set the correct clocks, yet some care has to be taken because not all of the cores run at the same speed. My example script is here, but the governor might also be set to be less aggressive (i.e. "conservative" instead of "ondemand"):

#!/bin/sh
cpupower --cpu 0-3 frequency-set --min 408MHz --max 1800MHz --governor schedutil
cpupower --cpu 4-7 frequency-set --min 408MHz --max 2304MHz --governor schedutil

Init runlevels no longer do anything on debian and derivatives, so you need to setup an one-shot service to fire this every boot:

[Unit]
Description=cpupower script to run in system start
After=network.target

[Service]
Type=oneshot
ExecStart=/path/to/update-cpupower
RemainAfterExit=yes

[Install]
WantedBy=multi-user.target

Save this as /usr/lib/systemd/system/cpupower.service and enable it through the normal systemd commands:

service cpupower enable
systemctl daemon-reload

I felt that I had to mention this here, because while the old init systems and linux runlevels were simple and easy to understand, systemd services continue to be a conundrum.

Temperatures

RAID array

To build the RAID array, an example list of commands is as follows. Run them after wiping any earlier partitions from the disks with your favorite disk partitioner and after creating single whole-disk linux partitions for them:

# Initialize the array
mdadm --create /dev/md0 --level=5 --raid-devices=4 /dev/nvme0n1 /dev/nvme1n1 /dev/nvme2n1 /dev/nvme3n1

# Check creation process, wait for this to finish
cat /proc/mdstat

# Once finished, create filesystem
mkfs.ext4 -F /dev/md0

# Create mount point
mkdir /mnt/archive

# Save array config
mdadm --detail --scan | sudo tee -a /etc/mdadm/mdadm.conf

# Save mount configuration to fstab
echo '/dev/md0 /mnt/archive ext4 defaults,noatime,commit=60 0 1' | sudo tee -a /etc/fstab

# Update initrd image so that the array is actually available on boot
update-initramfs -u

NOTE: Regarding these commands, many online guides give misleading or dangerous fstab mount options. For example, even though the "discard" option was originally advertised as being required to take advantage of SSD TRIM features, this is no longer needed. The disk controller does a better job at it than the kernel, and using "discard" may actually damage your disk in the long run.

Networking configuration

By default, Armbian distros create a br0 bridging interface and enable IPV6 privacy features which regenerate the V6 IP every 24 hours. These are fine for desktop use, but machines that act as servers shouldn't try to avoid being found.

Make a backup copy of /etc/netplan/armbian.yaml and alter its configuration to something like this:

network:
  version: 2
  renderer: networkd
  ethernets:
    eth0:
      dhcp4: yes
      dhcp6: yes
      ipv6-privacy: no

Your interface name may be different. The configuration may be tested with netplan try, but make sure you know the MAC address of the interface and are otherwise prepared. For me, the IP changed and any routing rules became invalid at the same time, so the connection was cut and the server became inaccessible. I had to detach the board one more time so I could plug in a keyboard and display to gain local access.


<return>