Unifi Controller - Running in Docker

The purpose of this post is to provide a look at the configuration that I’m running in my own network for the Unifi Controller required for Ubiquiti UniFi equipment (wireless access points, switches, gateways). The intent here is to walk you through the process of converting to a containerized controllwe and discuss my docker-compose.yml configuration choices. Following the steps in this article should get you a functional configuration for your own environment.

Introduction

Background

Upon initially purchasing Ubiquiti APs back in 2017, I made the decision to run my UniFi controller on a Raspberry Pi. This was a cool idea but, as updates would roll-in, I constantly found myself with a broken controller. There were Java version mismatches that would break the upgrade most commonly and then random other problems with why the controller just wouldn’t start. I could usually spend a day and recover everything, but it became a point of frustration.

After I decided to retire the Pi, I moved to having the controller virtualized on top of KVM running on Ubuntu. However, my Linux ecosystem in my lab is intentionally CentOS 7. Unfortunately, Ubiquiti seems to really only support Ubuntu/Debian for this controller and I wasn’t interested in having that in my ecosystem at that point (more to manage, different tools, etc).

As I was already experimenting with Docker in my lab for learning purposes, I set out to find a good way to run UniFi as a Docker container. I found that path from linuxserver.io in the form of the docker-unifi-controller project. I based my docker-compose.yml configuration on the one they provide and then added some modifications which you’ll see in this post.

Advantages to UniFi Containerization

You may wonder why it would be beneficial to containerize a UniFi controller using Docker (especially if you’re unfamiliar with container benefits in general). Here are some of the reasons I have an appreciation for this configuration:

  • Containerizing my controller allows me to easily upgrade. The command we use in this post to handle starting the container will also update the container image as updates are available. It’s extremely painless and I have yet to have any issues with it.
  • I very often tear-down my lab and rebuild it to test one thing or another (Ansible Playbooks for actually rebuilding it, usually). This approach makes getting my controller operational extremely fast without having to store a large VM image.
    • I simply store my backup of the data volume and the configuration file and place it back on the rebuilt host (the rest is downloaded again for me by docker-compose).
  • Specifically in my configuration, the UniFi controller container is not port-forwarded like I’ve seen in other configurations. This should allow the container to discover other systems like it would in a traditional installation. This worked great for finding my APs recently.

Migrations from Old Controllers

If you already have a UniFi controller running, you’re going to want to migrate your data from that controller. This is a fairly simple matter using these steps at a high-level:

  • Upgrade your current controller to at least something relatively current (to avoid configuration import issues).
    • I personally never had issues even going from a 5.x to 6.x but Ubiquiti warns about this constantly so I imagine it’s a very real possibility.
  • Download the latest backup from your existing controller in the UI (Settings > Backup > Download Backup).
  • Shut down your old controller.
  • Once the containerized controller is available, restore the configuration from the UI (Settings > Backup > Restore Backup).

Find detailed instructions from Ubiquiti on backup and restore here.

Prerequisites

  • You should have a cursory understanding of Docker so that you can support your configuration.
  • Your Docker host system will need to have an interface on the same VLAN you intend to have your UniFi controller operate.
    • A static IP address for your host is not required but we will use one for the container image itself.
  • You’ll need to go ahead and prepare your environment by installing Docker Engine.
  • Once you have Docker installed (and running!), you’ll need to also go ahead and install docker-compose (available from the same repository as Docker).
    • Also ensure that the dockerd or similar service on your platform is set to start at boot.

Controller Setup

Environment Configuration

Once you have an environment ready to host your configuration, you should make a directory where you plan to store your docker-compose configuration. Follow these steps as the root user:

  1. Change directories into your docker-compose configuration directory.
    1. This is /data/docker_profiles for me.
  2. Create a new directory for your UniFi controller (such as unifi).
  3. Create a new user to own your controller data: useradd -u 1502 -g 1502 -c "UniFi Controller" -d /data/docker_profiles/unifi unifi
    1. The command above assumes you’ll use the same storage point I use: /data/docker_profiles.
    2. This command also assumes that you’ll use UID and GID 1502 (my chosen values) for your UniFi user.

Docker-compose Configuration

I’ve provided the docker-compose.yml that I use below. Place this configuration (named docker-compose.yml) into the new data directory you made for this container. The idea here is for you to copy this file as a basis (adjusting the fields that have placeholders below):

---
version: "2.4"
services:
  unifi-controller:
    image: ghcr.io/linuxserver/unifi-controller
    container_name: unifi-controller
    environment:
      - PUID=1502
      - PGID=1502
      - MEM_LIMIT=1024M #optional
    volumes:
      - ./data:/config
    restart: unless-stopped
    networks:
      unifi:
        ipv4_address: <literal static address for controller>

networks:
  unifi:
    driver: macvlan
    driver_opts:
      parent: <network adapter where controller should go>
    ipam:
      config:
        - subnet: "<literal definition of your network>"
          ip_range: "<same address value as ipv4_address above>/32"
          gateway: "<gateway address>"

Now let’s walk through the important parts of this configuration:

  • We’re using version: "2.4" in order to ensure that docker-compose will actually support our networks configuration below. Version 3.0+ seems to have dropped support for this IP address configuration.
  • In the environment section, we’re defining the PUID (user ID) and PGID (group ID) that our process will use. The data this container creates will be owned by this user.
  • For volumes, we’re creating a ./data folder on the host that will be mounted at /config within the container. We’ll want to backup the ./data folder for this container as this is where our configuration will persist.
  • We want to ensure that this container starts up automatically unless we stopped it so we use restart: unless-stopped to ensure this.
  • We need to assign our container to a network that we define (unifi) and give it a static IP address (ipv4_address).
    • This should just be the straight IP address (such as 192.168.0.8).
  • We have to define unifi using the driver macvlan, which is what allows it to directly access a network on the host system.
    • Special thanks to Sarunas Zilinskas for the clues in this post on the proper syntax for docker-compose.yml!
  • We must define the name of the host network adapter we want to use for this network as parent (such as enp1s0 or eth0).
  • For subnet, we must specify the actual size of our network as a CIDR address (such as 192.168.0.0/24 for a traditional home network).
  • For ip_range, we’re simply defining a single address for this network that will only be used by this container. This needs to be the same value as the ipv4_address field above but with the CIDR notation at the end (so 192.168.0.8/32).

Controller Initialization

Now that you’ve configured everything to make it ready for your controller, you need to run your controller for the first time and make sure that it starts without an issue. This is fairly straight-forward if everything is configured properly.

From your container directory (so unifi as defined above), run this command as the root user: docker-compose up.

The first time you run this command, it will take some time as it downloads all of the appropriate images needed for your controller. Once these images are downloaded, the controller will start and you should see something similar to the output below:

Creating network "unifi_unifi" with driver "macvlan"
Creating unifi-controller ... done
Attaching to unifi-controller
unifi-controller    | [s6-init] making user provided files available at /var/run/s6/etc...exited 0.
unifi-controller    | [s6-init] ensuring user provided files have correct perms...exited 0.
unifi-controller    | [fix-attrs.d] applying ownership & permissions fixes...
unifi-controller    | [fix-attrs.d] done.
unifi-controller    | [cont-init.d] executing container initialization scripts...
unifi-controller    | [cont-init.d] 01-envfile: executing...
unifi-controller    | [cont-init.d] 01-envfile: exited 0.
unifi-controller    | [cont-init.d] 10-adduser: executing...
unifi-controller    |
unifi-controller    | -------------------------------------
unifi-controller    |           _         ()
unifi-controller    |          | |  ___   _    __
unifi-controller    |          | | / __| | |  /  \
unifi-controller    |          | | \__ \ | | | () |
unifi-controller    |          |_| |___/ |_|  \__/
unifi-controller    |
unifi-controller    |
unifi-controller    | Brought to you by linuxserver.io
unifi-controller    | -------------------------------------
unifi-controller    |
unifi-controller    | To support LSIO projects visit:
unifi-controller    | https://www.linuxserver.io/donate/
unifi-controller    | -------------------------------------
unifi-controller    | GID/UID
unifi-controller    | -------------------------------------
unifi-controller    |
unifi-controller    | User uid:    1502
unifi-controller    | User gid:    1502
unifi-controller    | -------------------------------------
unifi-controller    |
unifi-controller    | [cont-init.d] 10-adduser: exited 0.
unifi-controller    | [cont-init.d] 20-config: executing...

It’s important to note that this container takes a bit of time to start (on my system, around 2-3 minutes regularly). You should see this message appear once the container is almost ready:

unifi-controller    | [services.d] starting services
unifi-controller    | [services.d] done.

It seems like there’s still about 1 minute or less needed for services to start properly within the container so you may have to be patient during this process. Once the controller is ready, you should now be able to browse to it on your network:

https://<ipv4_address>:8443/

So, for our examples, I would use https://192.168.0.8:8443 to access my container. We don’t actually want to start using the system because we have our console attached. So we want to terminate the container (safely) using Ctrl+C and wait for it to end.

Now, in order you start the controller container and have it run in the background, execute this command:

docker-compose up -d

This will detach the controller process and run it in the background (but there’s still a delay in the container being ready for you to access).

Summary

After following everything in this article, you should now have a UniFi controller running inside of a Docker container. You can now enjoy the benefits of easy maintenance moving forward and easy portability if you need to move your controller in the future. Thanks for your time!