This post describes the process I uses to install the Unifi Controller software on a Raspberry Pi. There were a few unusual steps required to address some issues of the particular version I was using. Hopefully an upgrade is availalbe soon which would eliminate those additional manual steps.

Versions

Java: Oracle Java 8
Raspbian: 2016-02-09-raspbian-jessie-lite
Unifi Controller: 4.8.12
RPi MAC: b827.eb43.5384

Prepare SD Card

Flash the image

Insert the SD card and list the images.

diskutil list

Identify the disk (not partition) of the SD card, i.e. disk2. Unmount the SD card by using the disk identifier (assuming this is disk2).

diskutil unmountDisk /dev/disk2

Copy the data to the SD card.

sudo dd bs=1m if=2016-02-09-raspbian-jessie-lite.img of=/dev/rdisk2

Since dd does not output anything by default, it is possible to query its progress by sending Ctrl-T.

Use serial console

Upon successful flashing of the SD card it should be automatically mounted. A minor edit is needed to support serial console instead of keyboard and monitor.

Edit /Volumes/boot/cmdline.txt and change

console=ttyAMA0,115200 console=tty1

to

console=tty1 console=ttyAMA0,115200

Eject SD card

Finally eject the SD card.

sudo diskutil eject /dev/rdisk2

First Boot

Insert the SD card into the Raspberry Pi. Attach the serial console cable. Attach power via the micro USB port.

Start the serial console on the Mac.

screen /dev/tty.usbserial 115200

Login using the default credentials of username: pi and password: raspberry.

Resize the root partition

Determine the partitions we have.

pi@raspberrypi:~$ ls -al /dev/mm*
brw-rw---- 1 root disk 179, 0 Feb  9 16:06 /dev/mmcblk0
brw-rw---- 1 root disk 179, 1 Feb  9 16:06 /dev/mmcblk0p1
brw-rw---- 1 root disk 179, 2 Feb  9 16:06 /dev/mmcblk0p2

Determine the partition we need to extend using mount and df. Most likely the partition to extend will be /dev/mmcblk0p2.

Examine the disk using fdisk.

pi@raspberrypi:~$ sudo fdisk -c -l /dev/mmcblk0

Disk /dev/mmcblk0: 15.6 GiB, 16777216000 bytes, 32768000 sectors
Units: sectors of 1 * 512 = 512 bytes
Sector size (logical/physical): 512 bytes / 512 bytes
I/O size (minimum/optimal): 512 bytes / 512 bytes
Disklabel type: dos
Disk identifier: 0x34e4b02c

Device         Boot  Start     End Sectors  Size Id Type
/dev/mmcblk0p1        8192  131071  122880   60M  c W95 FAT32 (LBA)
/dev/mmcblk0p2      131072 2848767 2717696  1.3G 83 Linux

Now remove the partition using fdisk and recreate with larger size.

pi@raspberrypi:~$ sudo fdisk -c /dev/mmcblk0

Command (m for help): d
Partition number (1,2, default 2): 2

Partition 2 has been deleted.

Command (m for help): n
Partition type
   p   primary (1 primary, 0 extended, 3 free)
   e   extended (container for logical partitions)
Select (default p): p

Using default response p.
Partition number (2-4, default 2): 2
First sector (2048-32767999, default 2048): 131072
Last sector, +sectors or +size{K,M,G,T,P} (131072-32767999, default 32767999): 

Created a new partition 2 of type 'Linux' and of size 15.6 GiB.

Command (m for help): w
The partition table has been altered.
Calling ioctl() to re-read partition table.
Re-reading the partition table failed.: Device or resource busy

The kernel still uses the old table. The new table will be used at the next reboot or after you run partprobe(8) or kpartx(8).

Confirm that the new partition starts at the same sector and spans the whole disk using fdisk.

sudo fdisk -c -l /dev/mmcblk0.

Gracefully reboot the system

sudo reboot

Upon successful restart login, then resize the filesystem to match the partition size.

sudo resize2fs -p /dev/mmcblk0p2

We can check the filesystem for errors, to be sure we didn’t introduce issues.

sudo fsck -p /dev/mmcblk0p2

Confirm success by looking at disks.

System

Update the system.

sudo apt-get update
sudo apt-get upgrade

Set the time zone.

sudo dpkg-reconfigure tzdata

Static IP

Raspbian does its own funky thing for networking. Best way to add a static IP and keep things somewhat Debian is to edit /etc/network/interfaces

auto eth0
iface eth0 inet static
    address 172.23.21.15
    netmask 255.255.255.0
    gateway 172.23.21.1
    dns-nameservers 208.67.222.222 208.67.220.220

then adding the following to /etc/dhcpcd.conf

denyinterfaces eth0

Unifi Controller

Dependencies

Install the Java JDK.

sudo apt-get install oracle-java8-jdk

Pre-configuration

Create or edit /etc/default/mongodb to read

# Disable system mongodb
ENABLE_MONGODB=no
DAEMON_OPTS=--smallfiles

Installing the Unifi Controller Software

Installation Using .deb File
Installation Using Unifi APT Repo

Installation Using .deb File

Download the .deb package from the Uniquity web site. Note that direct download links are available from the software releases UBNT Forum.

Install package using dpkg and follow with apt-get to install dependencies.

dpgk -i unifi_4.7.6_all.deb
apt-get install -f

Installation Using Unifi APT Repo

Create /etc/apt/sources.list.d/100-ubnt.list with

## Debian/Ubuntu
# stable => unifi4
# deb http://www.ubnt.com/downloads/unifi/debian stable ubiquiti
deb http://www.ubnt.com/downloads/unifi/debian unifi4 ubiquiti

Add the GPG key

# for Ubiquiti
sudo apt-key adv --keyserver keyserver.ubuntu.com --recv C0A52C50

Update and install

# retrieve the latest package information
sudo apt-get update

# install/upgrade unifi-controller
sudo apt-get install unifi

Install Additional Fixes Manually

Per relase notes Unifi controller 4.8.12 does not support the Cloud Access feature on Linux/ARM. It requires that this file be removed. Without removing this file the controller will not start.

sudo rm /usr/lib/unifi/lib/native/Linux/armhf/libubnt_webrtc_jni.so

Per support forum thread controller 4.8.12 crashes on inform from AC AP with latest firmware (4.8.12 bundled) applied. See forum thread for details.

To address the issue download this file http://central.maven.org/maven2/org/xerial/snappy/snappy-java/1.1.2/snappy-java-1.1.2.jar

And install.

sudo cp snappy-java-1.1.2.jar /usr/lib/unifi/lib/snappy-java-1.0.5.jar

Configuration

Since Debian automatically starts installed daemons we need to stop the Unifu controller now before tweaking the installation.

sudo systemctl stop unifi

Edit /var/lib/unifi/system.properties and add or edit this line:

unifi.db.extraargs=--smallfiles

This will prevent the mongodb journal files to grow to some unmanageable size. It also helps to remove the /var/lib/unifi/db/journal directory, but do this with unifi STOPPED!

Configure the Unifi controller to use Oracle Java 8.

Create or edit /etc/default/unifi

# Use Oracle Java 8
JAVA_HOME=/usr/lib/jvm/jdk-8-oracle-arm32-vfp-hflt

Adjust the Java memory allocations.

sudo sed -i 's@-Xmx1024M@-Xmx384M@' /usr/lib/unifi/bin/unifi.init

Startup

Tell systemd that we made changes.

sudo systemctl daemon-reload

Startup unifi.

sudo systemctl start unifi

Maintenance

Run the attached script every so often to prune the database. Obtained original script from this Forum Post.

This is how to execute the script:

mongo --port=27117 < mongo_prune_js.js

This is the script:

// keep N-day worth of data
var days=7;

// change to false to have the script to really exclude old records
// from the database. While true, no change at all will be made to the DB
var dryrun=true;

var now = new Date().getTime(),
  time_criteria = now - days * 86400 * 1000,
  time_criteria_in_seconds = time_criteria / 1000;

print((dryrun ? "[dryrun] " : "") + "pruning data older than " + days + " days (" + time_criteria + ")... ");

use ace;
var collectionNames = db.getCollectionNames();
for (i=0; i<collectionNames.length; i++) {
  var name = collectionNames[i];
  var query = null;

  if (name === 'event' || name === 'alarm') {
    query = {time: {$lt:time_criteria}};
  }

  // rogue ap
  if (name === 'rogue') {
    query = {last_seen: {$lt:time_criteria_in_seconds}};
  }

  // removes vouchers expired more than '$days' ago
  // active and unused vouchers are NOT touched
  if (name === 'voucher') {
    query = {end_time: {$lt:time_criteria_in_seconds}};
  }

  // guest authorization
  if (name === 'guest') {
    query = {end: {$lt:time_criteria_in_seconds}};
  }

  // if an user was only seen ONCE, $last_seen will not be defined
  // so, if $last_seen not defined, lets use $first_seen instead
  // also check if $blocked or $use_fixedip is set. If true, do NOT purge the
  // entry no matter how old it is. We want blocked/fixed_ip users to continue
  // blocked/fixed_ip
  if (name === 'user') {
    query = { blocked: { $ne: true}, use_fixedip: { $ne: true}, $or: [
        {last_seen: {$lt:time_criteria_in_seconds} },
        {last_seen: {$exists: false}, first_seen: {$lt:time_criteria_in_seconds} }
      ]
    };
  }

  if (query) {
    count1 = db.getCollection(name).count();
    count2 = db.getCollection(name).find(query).count();
    print((dryrun ? "[dryrun] " : "") + "pruning " + count2 + " entries (total " + count1 + ") from " + name + "... ");
    if (!dryrun) {
      db.getCollection(name).remove(query);
      db.runCommand({ compact: name });
    }
  }
}

use ace_stat;
var collectionNames = db.getCollectionNames();
for (i=0; i<collectionNames.length; i++) {
  var name = collectionNames[i];
  var query = null;

  // historical stats (stat.*)
  if (name.indexOf('stat')==0) {
    query = {time: {$lt:time_criteria}};
  }

  if (query) {
    count1 = db.getCollection(name).count();
    count2 = db.getCollection(name).find(query).count();
    print((dryrun ? "[dryrun] " : "") + "pruning " + count2 + " entries (total " + count1 + ") from " + name + "... ");
    if (!dryrun) {
      db.getCollection(name).remove(query);
      db.runCommand({ compact: name });
    }
  }
}

if (!dryrun) db.repairDatabase();