├── hooks ├── build └── push ├── scripts ├── ci-get-unifi-version.sh ├── unifi-publi.sh └── unifi-build.sh ├── LICENSE ├── docker-compose.yml ├── gitlab-ci-example.yml ├── .gitlab-ci.yml ├── unifi.init ├── unifi.default ├── Dockerfile ├── unifi-network-service-helper └── README.md /hooks/build: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | SHA_COMMIT="$(echo "${GIT_SHA1}" | cut -b-8)" 6 | 7 | # Build stable 8 | ./scripts/unifi-build.sh -c stable -e latest -g "${SHA_COMMIT}" "${DOCKER_REPO}" 9 | 10 | # Build old stable 11 | ./scripts/unifi-build.sh -c oldstable -g "${SHA_COMMIT}" "${DOCKER_REPO}" 12 | 13 | # Build LTS 14 | ./scripts/unifi-build.sh -c unifi-5.6 -t lts -g "${SHA_COMMIT}" "${DOCKER_REPO}" 15 | -------------------------------------------------------------------------------- /scripts/ci-get-unifi-version.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | function bye { 6 | echo >&2 "$@" 7 | exit 1 8 | } 9 | 10 | if (( $# )); then 11 | UNIFI_CHANNEL="$1" 12 | shift 13 | else 14 | bye "Unifi channel (stable or oldstable or etc.) is mandatory" 15 | fi 16 | 17 | if (( $# )); then 18 | bye "Wrong number of arguments" 19 | fi 20 | 21 | curl -sSL "https://www.ubnt.com/downloads/unifi/debian/dists/${UNIFI_CHANNEL}/ubiquiti/binary-${ARG_ARCH:-amd64}/Packages.gz" \ 22 | | zgrep Version | sed -rn 's/Version: ([[:digit:]]+.[[:digit:]]+.[[:digit:]]+)-.*/\1/p' 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 J.-C. Berthon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.2' 2 | 3 | services: 4 | unifi: 5 | build: 6 | context: . 7 | dockerfile: Dockerfile 8 | args: 9 | BASEIMG: ${ARG_BASEIMG:-ubuntu} 10 | BASEVERS: ${ARG_BASEVERS:-focal} 11 | ARCH: ${ARG_ARCH:-amd64} 12 | UNIFI_CHANNEL: ${ARG_CHANNEL:-stable} 13 | image: "${CI_REGISTRY_IMAGE:-jcberthon/unifi}:${IMAGE_TAG:-latest}" 14 | restart: on-failure:5 15 | hostname: 'unifi' 16 | container_name: unifi 17 | cpus: 1.0 18 | mem_limit: 1024m 19 | memswap_limit: 2048m 20 | # On my installation the whole containers uses about 105 processes/threads 21 | # So I would not put less than 150 for the `pids_limit`. 22 | pids_limit: 1000 23 | cap_drop: 24 | - ALL 25 | security_opt: 26 | - no-new-privileges 27 | ports: 28 | - '3478:3478/udp' 29 | - '6789:6789' 30 | - '8080:8080' 31 | - '8443:8443' 32 | - '8843:8843' 33 | - '10001:10001/udp' 34 | volumes: 35 | - unifi_data:/var/lib/unifi 36 | - unifi_logs:/var/log/unifi 37 | 38 | volumes: 39 | unifi_data: 40 | driver: local 41 | unifi_logs: 42 | driver: local 43 | -------------------------------------------------------------------------------- /hooks/push: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euxo pipefail 4 | 5 | # For official documentation, see: https://docs.docker.com/docker-cloud/builds/advanced/ 6 | # Available variables: 7 | # - `SOURCE_BRANCH`: the name of the branch or the tag that is currently being tested. 8 | # - `SOURCE_COMMIT`: the SHA1 hash of the commit being tested. 9 | # - `COMMIT_MSG`: the message from the commit being tested and built. 10 | # - `DOCKER_REPO`: the name of the Docker repository being built. 11 | # - `CACHE_TAG`: the Docker repository tag being built. --> seems empty, renamed? it is still DOCKER_TAG, eventhough https://github.com/docker/docker.github.io/issues/2125 12 | # - `IMAGE_NAME`: the name and tag of the Docker repository being built. (This variable is a combination of `DOCKER_REPO`:`CACHE_TAG`.) 13 | # 14 | # SHA_COMMIT=${SOURCE_COMMIT:0:8} 15 | 16 | SHA_COMMIT="$(echo "${GIT_SHA1}" | cut -b-8)" 17 | 18 | # Push old stable 19 | ./scripts/unifi-publi.sh -c oldstable -g "${SHA_COMMIT}" "${DOCKER_REPO}" 20 | 21 | # Push LTS 22 | ./scripts/unifi-publi.sh -c unifi-5.6 -t lts -g "${SHA_COMMIT}" "${DOCKER_REPO}" 23 | 24 | # Push stable 25 | ./scripts/unifi-publi.sh -c stable -e latest -g "${SHA_COMMIT}" "${DOCKER_REPO}" 26 | -------------------------------------------------------------------------------- /scripts/unifi-publi.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Usage: 4 | # unifi-publi.sh [-c channel] [-e extra_tags] [-g git_commit] [-t tag_name] repository 5 | # 6 | # Example: 7 | # unifi-publi.sh -c stable -e latest docker.io/jcberthon/unifi 8 | 9 | set -euxo pipefail 10 | 11 | function bye { 12 | echo >&2 "$@" 13 | exit 1 14 | } 15 | 16 | 17 | channel=stable 18 | opt_extratag=0 19 | opt_tag=0 20 | opt_commit=0 21 | 22 | while getopts c:e:g:t: opt 23 | do 24 | case $opt in 25 | c) channel=$OPTARG ;; 26 | e) opt_extratag=1; extratag=$OPTARG ;; 27 | g) opt_commit=1; commit=$OPTARG ;; 28 | t) opt_tag=1; tag=$OPTARG ;; 29 | ?) bye "Option not understood" ;; 30 | esac 31 | done 32 | shift $(( OPTIND - 1 )) 33 | if (( $# )); then 34 | repo_name=$1 35 | shift 36 | else 37 | bye "Repository name mandatory" 38 | fi 39 | 40 | if (( $# )); then 41 | bye "Wrong number of arguments" 42 | fi 43 | 44 | (( ! opt_tag )) && tag=${channel} 45 | 46 | # Push full version + short commit ID (e.g. 5.10.21-s12345678) if possible 47 | VERSION_UNIFI="$(./scripts/ci-get-unifi-version.sh "${channel}")" 48 | if (( opt_commit )); then 49 | short_commit="$(echo "${commit}" | cut -b-8)" 50 | docker push "${repo_name}:${VERSION_UNIFI}-s${short_commit}" 51 | fi 52 | 53 | # Push full version (e.g. 5.10.21) 54 | docker push "${repo_name}:${VERSION_UNIFI}" 55 | 56 | # Push major.minor version (e.g. 5.10) 57 | VERSION_UNIFI_BRANCH="$(echo "${VERSION_UNIFI}" | cut -d. -f-2)" 58 | docker push "${repo_name}:${VERSION_UNIFI_BRANCH}" 59 | 60 | # Push default tag 61 | docker push "${repo_name}:${tag}" 62 | 63 | # Push extra tag 64 | if (( opt_extratag )); then 65 | docker push "${repo_name}:${extratag}" 66 | fi 67 | -------------------------------------------------------------------------------- /scripts/unifi-build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Usage: 4 | # unifi-build.sh [-c channel] [-e extra_tags] [-g git_commit] [-t tag_name] repository 5 | # 6 | # Example: 7 | # unifi-build.sh -c stable -e latest docker.io/jcberthon/unifi 8 | 9 | set -euxo pipefail 10 | 11 | function bye { 12 | echo >&2 "$@" 13 | exit 1 14 | } 15 | 16 | 17 | opt_channel=0; channel=stable 18 | opt_extratag=0 19 | opt_tag=0 20 | opt_commit=0 21 | 22 | while getopts c:e:g:t: opt 23 | do 24 | case $opt in 25 | c) opt_channel=1; channel=$OPTARG ;; 26 | e) opt_extratag=1; extratag=$OPTARG ;; 27 | g) opt_commit=1; commit=$OPTARG ;; 28 | t) opt_tag=1; tag=$OPTARG ;; 29 | ?) bye "Option not understood" ;; 30 | esac 31 | done 32 | shift $(( $OPTIND - 1 )) 33 | if (( $# )); then 34 | repo_name=$1 35 | shift 36 | else 37 | bye "Repository name mandatory" 38 | fi 39 | 40 | if (( $# )); then 41 | bye "Wrong number of arguments" 42 | fi 43 | 44 | (( ! opt_tag )) && tag=${channel} 45 | 46 | # Build the image 47 | docker build --pull --build-arg UNIFI_CHANNEL=${channel} -t "${repo_name}:${tag}" . 48 | 49 | # Add extra tags 50 | # Add full version (e.g. 5.10.21) 51 | VERSION_UNIFI="$(./scripts/ci-get-unifi-version.sh ${channel})" 52 | docker tag "${repo_name}:${tag}" "${repo_name}:${VERSION_UNIFI}" 53 | 54 | # Add major.minor version (e.g. 5.10) 55 | VERSION_UNIFI_BRANCH="$(echo ${VERSION_UNIFI} | cut -d. -f-2)" 56 | docker tag "${repo_name}:${tag}" "${repo_name}:${VERSION_UNIFI_BRANCH}" 57 | 58 | # Add full version + short commit ID if possible 59 | if (( opt_commit )); then 60 | short_commit="$(echo ${commit} | cut -b-8)" 61 | docker tag "${repo_name}:${tag}" "${repo_name}:${VERSION_UNIFI}-s${short_commit}" 62 | fi 63 | 64 | # Add extra tag 65 | if (( opt_extratag )); then 66 | docker tag "${repo_name}:${tag}" "${repo_name}:${extratag}" 67 | fi 68 | -------------------------------------------------------------------------------- /gitlab-ci-example.yml: -------------------------------------------------------------------------------- 1 | image: docker:stable 2 | 3 | stages: 4 | - build 5 | - cleanup 6 | - publish 7 | - deploy 8 | 9 | before_script: 10 | - export IMAGE_TAG="$(echo -en $CI_COMMIT_REF_NAME | tr -c '[:alnum:]_.-' '-')-$(date +%Y%m%d)" 11 | - uname -a 12 | - docker version 13 | 14 | build:test: 15 | stage: build 16 | except: 17 | - master 18 | script: 19 | - docker build -f Dockerfile -t "huygens/test/unifi:$IMAGE_TAG" . 20 | tags: 21 | - docker 22 | 23 | build:master: 24 | stage: build 25 | only: 26 | - master 27 | script: 28 | - docker build -f Dockerfile --pull --no-cache -t "${CI_REGISTRY_IMAGE}/unifi:x86_64-$IMAGE_TAG" . 29 | tags: 30 | - docker 31 | 32 | 33 | clean:test: 34 | stage: cleanup 35 | except: 36 | - master 37 | script: 38 | - docker rmi "huygens/test/unifi:$IMAGE_TAG" 39 | when: manual 40 | tags: 41 | - docker 42 | 43 | 44 | push: 45 | stage: publish 46 | only: 47 | - master 48 | script: 49 | - docker tag "${CI_REGISTRY_IMAGE}/unifi:x86_64-$IMAGE_TAG" "${CI_REGISTRY_IMAGE}/unifi:latest" 50 | - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY 51 | - docker push "${CI_REGISTRY_IMAGE}/unifi:x86_64-$IMAGE_TAG" 52 | - docker push "${CI_REGISTRY_IMAGE}/unifi:latest" 53 | tags: 54 | - docker 55 | 56 | # To be run on your own runner where you want to deploy 57 | # Replace the tags to match your specific runner 58 | deploy_example: 59 | stage: deploy 60 | only: 61 | - master 62 | script: 63 | - apk add --no-cache py2-pip 64 | - pip install docker-compose 65 | - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY 66 | - docker pull "${CI_REGISTRY_IMAGE}/unifi:x86_64-$IMAGE_TAG" 67 | - docker pull "${CI_REGISTRY_IMAGE}/unifi:latest" 68 | - IMAGE_TAG=x86_64-$IMAGE_TAG docker-compose -f docker-compose.yml up -d 69 | tags: 70 | - docker 71 | - deploy 72 | - huygens 73 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: docker:stable 2 | 3 | # When using dind, it's wise to use the overlayfs driver for 4 | # improved performance. 5 | variables: 6 | DOCKER_DRIVER: overlay 7 | ARG_ARCH: "amd64" 8 | ARG_BASEIMG: "ubuntu" 9 | ARG_BASEVERS: "focal" 10 | 11 | services: 12 | - docker:stable-dind 13 | 14 | 15 | build:amd64: 16 | only: 17 | - master 18 | variables: 19 | UNIFI_CHANNEL: stable 20 | before_script: 21 | - apk add --no-cache curl gzip bash 22 | - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" "$CI_REGISTRY" 23 | script: 24 | - ./scripts/unifi-build.sh -c stable -e latest -g "${CI_COMMIT_SHORT_SHA}" "${CI_REGISTRY_IMAGE}/unifi" 25 | - ./scripts/unifi-publi.sh -c stable -e latest -g "${CI_COMMIT_SHORT_SHA}" "${CI_REGISTRY_IMAGE}/unifi" 26 | - ./scripts/unifi-build.sh -c oldstable -g "${CI_COMMIT_SHORT_SHA}" "${CI_REGISTRY_IMAGE}/unifi" 27 | - ./scripts/unifi-publi.sh -c oldstable -g "${CI_COMMIT_SHORT_SHA}" "${CI_REGISTRY_IMAGE}/unifi" 28 | - ./scripts/unifi-build.sh -c unifi-8.2 -t lts -g "${CI_COMMIT_SHORT_SHA}" "${CI_REGISTRY_IMAGE}/unifi" 29 | - ./scripts/unifi-publi.sh -c unifi-8.2 -t lts -g "${CI_COMMIT_SHORT_SHA}" "${CI_REGISTRY_IMAGE}/unifi" 30 | tags: 31 | - docker 32 | 33 | 34 | build:dev:amd64: 35 | only: 36 | - branches 37 | except: 38 | - master 39 | - oldstable 40 | - testing 41 | before_script: 42 | - apk add --no-cache curl gzip 43 | - export VERSION_UNIFI="$(./scripts/ci-get-unifi-version.sh ${UNIFI_CHANNEL})" 44 | - docker login -u "gitlab-ci-token" -p "$CI_JOB_TOKEN" $CI_REGISTRY 45 | script: 46 | - docker build --pull --build-arg stable -t "${CI_REGISTRY_IMAGE}/unifi:dev-${VERSION_UNIFI}-$CI_COMMIT_REF_SLUG" . 47 | - docker tag "${CI_REGISTRY_IMAGE}/unifi:dev-${VERSION_UNIFI}-$CI_COMMIT_REF_SLUG" "${CI_REGISTRY_IMAGE}/unifi:dev-latest" 48 | - docker push "${CI_REGISTRY_IMAGE}/unifi:dev-${VERSION_UNIFI}-$CI_COMMIT_REF_SLUG" 49 | - docker push "${CI_REGISTRY_IMAGE}/unifi:dev-latest" 50 | tags: 51 | - docker 52 | -------------------------------------------------------------------------------- /unifi.init: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Startup script for Ubiquiti UniFi 3 | 4 | set -ex 5 | 6 | NAME="unifi" 7 | 8 | BASEDIR="/usr/lib/unifi" 9 | MAINCLASS="com.ubnt.ace.Launcher" 10 | 11 | PATH=/bin:/usr/bin:/sbin:/usr/sbin 12 | 13 | MONGOPORT=27117 14 | 15 | JAVA_ENTROPY_GATHER_DEVICE= 16 | JVM_MAX_HEAP_SIZE=1024M 17 | JVM_INIT_HEAP_SIZE= 18 | UNIFI_JVM_EXTRA_OPTS= 19 | 20 | ENABLE_UNIFI=yes 21 | JVM_EXTRA_OPTS="-XX:+ExitOnOutOfMemoryError -XX:+CrashOnOutOfMemoryError" 22 | [ -f "/etc/default/${NAME}" ] && . "/etc/default/${NAME}" 23 | 24 | [ "x${ENABLE_UNIFI}" != "xyes" ] && exit 0 25 | 26 | # Unifi Init and Init-UOS 27 | /usr/lib/unifi/bin/unifi-network-service-helper init 28 | /usr/lib/unifi/bin/unifi-network-service-helper init-uos 29 | 30 | DATADIR=${UNIFI_DATA_DIR:-/var/lib/${NAME}} 31 | LOGDIR=${UNIFI_LOG_DIR:-/var/log/${NAME}} 32 | RUNDIR=${UNIFI_RUN_DIR:-/var/run/${NAME}} 33 | 34 | JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} -Dunifi.datadir=${DATADIR} -Dunifi.logdir=${LOGDIR} -Dunifi.rundir=${RUNDIR} -XX:ErrorFile=${LOGDIR}/unifi_crash.log -Xlog:gc:logs/gc.log:time:filecount=2,filesize=5M" 35 | 36 | if [ ! -z "${JAVA_ENTROPY_GATHER_DEVICE}" ]; then 37 | JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} -Djava.security.egd=${JAVA_ENTROPY_GATHER_DEVICE}" 38 | fi 39 | 40 | if [ ! -z "${JVM_MAX_HEAP_SIZE}" ]; then 41 | JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} -Xmx${JVM_MAX_HEAP_SIZE}" 42 | fi 43 | 44 | if [ ! -z "${JVM_INIT_HEAP_SIZE}" ]; then 45 | JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} -Xms${JVM_INIT_HEAP_SIZE}" 46 | fi 47 | 48 | if [ ! -z "${UNIFI_JVM_EXTRA_OPTS}" ]; then 49 | JVM_EXTRA_OPTS="${JVM_EXTRA_OPTS} ${UNIFI_JVM_EXTRA_OPTS}" 50 | fi 51 | 52 | JVM_OPTS="${JVM_EXTRA_OPTS} -Djava.awt.headless=true -Dfile.encoding=UTF-8 -Djava.awt.headless=true" 53 | 54 | UNIFI_USER=${UNIFI_USER:-unifi} 55 | 56 | MONGOLOCK="${DATADIR}/db/mongod.lock" 57 | 58 | UNIFI_UID=$(id -u "${UNIFI_USER}") 59 | DATADIR_UID=$(stat "${DATADIR}" -Lc %u) 60 | if (( UNIFI_UID != DATADIR_UID )); then 61 | msg="${NAME} cannot start. Please create ${UNIFI_USER} user, and chown -R ${UNIFI_USER} ${DATADIR} ${LOGDIR} ${RUNDIR}" 62 | echo "$msg" >&2 63 | exit 1 64 | fi 65 | 66 | cd ${BASEDIR} 67 | 68 | echo "Starting ${NAME}" 69 | /usr/bin/java ${JVM_OPTS} \ 70 | --add-opens java.base/java.lang=ALL-UNNAMED \ 71 | --add-opens java.base/java.time=ALL-UNNAMED \ 72 | --add-opens java.base/sun.security.util=ALL-UNNAMED \ 73 | --add-opens java.base/java.io=ALL-UNNAMED \ 74 | --add-opens java.rmi/sun.rmi.transport=ALL-UNNAMED \ 75 | -jar "${BASEDIR}/lib/ace.jar" start 76 | 77 | # After exiting 78 | /usr/lib/unifi/bin/unifi-network-service-helper cleanup 79 | 80 | exit 0 81 | -------------------------------------------------------------------------------- /unifi.default: -------------------------------------------------------------------------------- 1 | # Override default value for Unifi Controller parameters 2 | 3 | # UNIFI_DATA_DIR 4 | # The "data" folder for the Unifi Controller. 5 | # This includes the system.properties file 6 | # Default value: UNIFI_DATA_DIR=/usr/lib/unifi 7 | # UNIFI_DATA_DIR=/var/lib/unifi 8 | 9 | # UNIFI_LOG_DIR 10 | # The "log" folder, useful for troubleshooting 11 | # Server logs: server.log 12 | # Database logs: mongod.log 13 | # Default value: UNIFI_LOG_DIR=/var/log/unifi 14 | # UNIFI_LOG_DIR=/var/log/unifi 15 | 16 | # UNIFI_RUN_DIR 17 | # The "run" folder containing the pid file, and other runtime ones 18 | # One notable file is update.json which contains the information 19 | # when was the last time the controller checked for an update (Unix time) and 20 | # if there is an update. Example content: 21 | # { "last_checked" : 1503545564 , "update_available" : false} 22 | # Default value: UNIFI_RUN_DIR=/var/run/unifi 23 | # UNIFI_RUN_DIR=/var/run/unifi 24 | 25 | # JAVA_ENTROPY_GATHER_DEVICE 26 | # This parameter can be used if it takes too long to access the controller 27 | # after a restart. Verify that this could be due to your entropy pool being 28 | # deplated. In this case, you could provide an entropy gathering device. 29 | # Default value (none, empty): JAVA_ENTROPY_GATHER_DEVICE= 30 | # Possible value: JAVA_ENTROPY_GATHER_DEVICE=file:/dev/urandom 31 | # JAVA_ENTROPY_GATHER_DEVICE= 32 | 33 | # JVM_MAX_HEAP_SIZE 34 | # Control the maximum heap memory used by the Unifi Controller java 35 | # processes. 36 | # This limits the heap only, a Java process can still take up to 10-20% 37 | # more memory for GC processing, memory accounting, stacks, etc. 38 | # Currenlty Unifi Controller is made of 3 Java process, so up to 3GB 39 | # of heap could be consumed, but for a home installation the total memory 40 | # is closed to 300M. 41 | # Anyway the default is 1024MB, for home installation you could decrease it 42 | # to 512MB, for Small Business the default is good, for large installations 43 | # you should try 2048MB. 44 | # Default value: JVM_MAX_HEAP_SIZE=1024M 45 | # JVM_MAX_HEAP_SIZE= 46 | 47 | # JVM_INIT_HEAP_SIZE 48 | # Control the initial heap size. Usually you do not need to change that 49 | # parameter. Perhaps to speed up the startup in very large installations 50 | # you could set it to 75% of the max heap value. 51 | # Default value: JVM_INIT_HEAP_SIZE= 52 | # JVM_INIT_HEAP_SIZE= 53 | 54 | # UNIFI_JVM_EXTRA_OPTS 55 | # Here you can specify any other JVM options you wish (related to GC, etc.) 56 | # See your vendor JVM for possibilities. 57 | # Default value: UNIFI_JVM_EXTRA_OPTS= 58 | UNIFI_JVM_EXTRA_OPTS="-XX:MaxRAMPercentage=50.0" 59 | 60 | # ENABLE_UNIFI 61 | # With this option you can for disable the Unifi Controller. 62 | # Values are 'yes' or 'no'. 63 | # Default value: ENABLE_UNIFI=yes 64 | # ENABLE_UNIFI=yes 65 | 66 | # New parameters 67 | UNIFI_CORE_ENABLED=false 68 | UNIFI_MONGODB_SERVICE_ENABLED=false 69 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | ARG BASEIMG=ubuntu 2 | ARG BASEVERS=focal 3 | FROM ${BASEIMG}:${BASEVERS} 4 | 5 | ARG ARCH=amd64 6 | ENV ARCH=${ARCH} 7 | ARG DEBIAN_FRONTEND=noninteractive 8 | ENV TINI_VERSION=v0.19.0 9 | ARG MONGODB_VERSION=3.6 10 | ENV MONGODB_VERSION=${MONGODB_VERSION} 11 | 12 | 13 | # Install Ubiquiti UniFi Controller dependencies 14 | ARG OPENJDK_VERSION=17 15 | ENV OPENJDK_VERSION=${OPENJDK_VERSION} 16 | RUN apt-get update \ 17 | && apt-get install -y --no-install-recommends \ 18 | apt-transport-https \ 19 | ca-certificates \ 20 | binutils \ 21 | curl \ 22 | dirmngr \ 23 | gnupg \ 24 | jsvc \ 25 | procps \ 26 | openjdk-${OPENJDK_VERSION}-jre-headless \ 27 | && curl -fsSL https://www.mongodb.org/static/pgp/server-${MONGODB_VERSION}.asc | \ 28 | gpg -o /usr/share/keyrings/mongodb-server.gpg \ 29 | --dearmor \ 30 | && . /etc/os-release \ 31 | # Overriding CODENAME as per Unifi instruction 32 | && UBUNTU_CODENAME="bionic" \ 33 | && echo "deb [ arch=amd64,arm64 trusted=yes ] https://repo.mongodb.org/apt/ubuntu ${UBUNTU_CODENAME}/mongodb-org/${MONGODB_VERSION} multiverse" > /etc/apt/sources.list.d/mongodb-org.list \ 34 | && apt-get update \ 35 | && apt-get install -y --no-install-recommends \ 36 | mongodb-org-server \ 37 | && apt-get clean -qy \ 38 | && rm -rf /var/lib/apt/lists/* \ 39 | && curl -L "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${ARCH}" -o /sbin/tini \ 40 | && curl -L "https://github.com/krallin/tini/releases/download/${TINI_VERSION}/tini-${ARCH}.asc" -o /sbin/tini.asc \ 41 | && gpg --batch --keyserver hkp://keyserver.ubuntu.com --recv-keys 595E85A6B1B4779EA4DAAEC70B588DFF0527A9B7 \ 42 | && gpg --batch --verify /sbin/tini.asc /sbin/tini \ 43 | && rm -f /sbin/tini.asc \ 44 | && chmod 0755 /sbin/tini 45 | 46 | 47 | # Install Ubiquiti UniFi Controller 48 | ARG UNIFI_CHANNEL=stable 49 | ENV UNIFI_CHANNEL=${UNIFI_CHANNEL} 50 | RUN groupadd -g 750 -o unifi \ 51 | && useradd -u 750 -o -g unifi -M unifi \ 52 | && curl -fsSL "https://dl.ui.com/unifi/unifi-repo.gpg" | \ 53 | gpg -o /usr/share/keyrings/unifi-repo.gpg \ 54 | --dearmor \ 55 | && echo "deb [ signed-by=/usr/share/keyrings/unifi-repo.gpg ] https://www.ui.com/downloads/unifi/debian ${UNIFI_CHANNEL} ubiquiti" > /etc/apt/sources.list.d/ubiquiti-unifi.list \ 56 | && apt-get update \ 57 | && apt-get install -y --no-install-recommends \ 58 | unifi \ 59 | && apt-get clean -qy \ 60 | && rm -rf /var/lib/apt/lists/* \ 61 | && find /usr/lib/unifi/dl/firmware -mindepth 1 \! -name bundles.json -delete 62 | 63 | EXPOSE 6789/tcp 8080/tcp 8443/tcp 8880/tcp 8843/tcp 3478/udp 10001/udp 64 | 65 | COPY unifi.default /etc/default/unifi 66 | COPY unifi.init /usr/lib/unifi/bin/unifi.init 67 | COPY unifi-network-service-helper /usr/lib/unifi/bin/unifi-network-service-helper 68 | 69 | # Enable running Unifi Controller as a standard user 70 | # It requires that we create certain folders and links first 71 | # with the right user ownership and permissions. 72 | RUN mkdir -p -m 755 /var/lib/unifi /var/log/unifi /var/run/unifi /usr/lib/unifi/work \ 73 | && ln -sf /var/lib/unifi /usr/lib/unifi/data \ 74 | && ln -sf /var/log/unifi /usr/lib/unifi/logs \ 75 | && ln -sf /var/run/unifi /usr/lib/unifi/run \ 76 | && chown unifi:unifi /var/lib/unifi /var/log/unifi /var/run/unifi /usr/lib/unifi/work \ 77 | && chmod 755 /usr/lib/unifi/bin/unifi.init 78 | USER unifi 79 | 80 | # Add healthcheck (requires Docker 1.12) 81 | HEALTHCHECK --interval=30s --timeout=3s --retries=5 --start-period=60s \ 82 | CMD curl --insecure -f https://localhost:8443/ || exit 1 83 | 84 | VOLUME ["/var/lib/unifi", "/var/log/unifi"] 85 | 86 | # execute the controller by using the init script and the `init` option of Docker 87 | # Requires to send the TERM signal to all process as JSVC does not know mongod 88 | # was launched by the Unifi application. Therefore mongod was not shutdown 89 | # cleanly. 90 | ENTRYPOINT ["/sbin/tini", "-g", "--", "/usr/lib/unifi/bin/unifi.init"] 91 | -------------------------------------------------------------------------------- /unifi-network-service-helper: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # 3 | # Helper script for Ubiquiti UniFi Network 4 | # 5 | 6 | . "/usr/sbin/unifi-network-service-common" 7 | 8 | # General parameters 9 | NAME="unifi" 10 | BASEDIR="/usr/lib/unifi" 11 | ADDITIONAL_STORAGE_BASE="/srv" 12 | 13 | # Constants 14 | 15 | ### Helper functions ### 16 | 17 | function is_link() { 18 | [ -L "$1" ] && [ -e "$1" ] 19 | } 20 | 21 | function is_mountpoint() { 22 | mountpoint -q "$1" 23 | } 24 | 25 | function is_additional_storage_available() { 26 | is_mountpoint "$ADDITIONAL_STORAGE_BASE" || is_link "$ADDITIONAL_STORAGE_BASE" 27 | } 28 | 29 | function create_additional_storage_dir() { 30 | if is_additional_storage_available && [ ! -d "$ADDITIONAL_STORAGE_DIR" ]; then 31 | mkdir "$ADDITIONAL_STORAGE_DIR" 32 | chown "$USER:$GROUP" "$ADDITIONAL_STORAGE_DIR" 33 | fi 34 | } 35 | 36 | function mount_tmpfs_dir() { 37 | if does_not_have_ubnt_tools ; then 38 | return 0 39 | fi 40 | # Support directories 41 | mkdir -p "$TMPFS_DIR" 42 | chown -R "$USER:$GROUP" "$TMPFS_DIR" 43 | 44 | # Mount temp filesystem 45 | mount -t tmpfs -o size=$TMPFS_SIZE tmpfs $TMPFS_DIR || log "Warning: Could not mount tmpfs to $TMPFS_DIR" 46 | } 47 | 48 | function unmount_tmpfs_dir() { 49 | if does_not_have_ubnt_tools ; then 50 | return 0 51 | fi 52 | log "tmpfs: $TMPFS_DIR" 53 | if is_mountpoint "$TMPFS_DIR"; then 54 | umount "$TMPFS_DIR" || log "Warning: Could not unmount tmpfs from $TMPFS_DIR" 55 | fi 56 | } 57 | 58 | function create_dirs() { 59 | log "user: $USER, group: $GROUP" 60 | log "data: $DATADIR, logs: $LOGDIR, run: $RUNDIR, additional storage: $ADDITIONAL_STORAGE_DIR" 61 | log "tmpfs: $TMPFS_DIR, size: $TMPFS_SIZE" 62 | link_to_persistent "${DATADIR}" "${BASEDIR}"/data 63 | link_to_persistent "${LOGDIR}" "${BASEDIR}"/logs 64 | link_to_persistent "${RUNDIR}" "${BASEDIR}"/run 65 | create_additional_storage_dir 66 | mount_tmpfs_dir 67 | 68 | check_dir_permissions || log "Warning: Wrong permissions for ${DATADIR}, ${LOGDIR}, ${RUNDIR}" 69 | 70 | return 0 71 | } 72 | 73 | function check_dir_permissions() { 74 | local unifi_uid=$(id -u "${USER}") 75 | 76 | local datadir_uid=$(stat ${DATADIR} -Lc %u) 77 | local logdir_uid=$( stat ${LOGDIR} -Lc %u) 78 | local rundir_uid=$( stat ${RUNDIR} -Lc %u) 79 | 80 | [ "${unifi_uid}" == "${datadir_uid}" ] && \ 81 | [ "${unifi_uid}" == "${logdir_uid}" ] && \ 82 | [ "${unifi_uid}" == "${logdir_uid}" ] && \ 83 | \ 84 | sudo -u "${USER}" test -w "${DATADIR}" && \ 85 | sudo -u "${USER}" test -w "${LOGDIR}" && \ 86 | sudo -u "${USER}" test -w "${RUNDIR}" && \ 87 | \ 88 | return 0; 89 | 90 | return 1; 91 | } 92 | 93 | function link_to_persistent() { 94 | local persistent_dir="${1}" 95 | local orig_dir="${2}" 96 | install -o "${USER}" -dD "${persistent_dir}" 97 | chown -R "${USER}": "${persistent_dir}" 98 | if ! [ -L "${orig_dir}" -a "${persistent_dir}" -ef "${orig_dir}" ]; then 99 | rm -rf "${orig_dir}" 100 | ln -sf "${persistent_dir}" "${orig_dir}" 101 | fi 102 | } 103 | 104 | function get_java_version () { 105 | java -version 2>&1 | head -1 | cut -d'"' -f2 | sed '/^1\./s///' | cut -d'.' -f1 106 | } 107 | 108 | function set_java_version () { 109 | sudo update-alternatives --set java $1 110 | } 111 | 112 | function log_unsupported_java_version() { 113 | log "Warning: Could not find alternative/supported Java version. Make sure '/usr/bin/java' points to supported version - Java $REQUIRED_JAVA_VERSION" 114 | } 115 | 116 | function set_java_home () { 117 | supported_java_version=$REQUIRED_JAVA_VERSION 118 | log "Setting Java home to supported version - $supported_java_version" 119 | arch=`dpkg --print-architecture 2>/dev/null` 120 | if [ ! "$arch" ]; then 121 | arch=`uname -m` 122 | if [ "$arch" = aarch64 ]; then 123 | arch=arm64 124 | fi 125 | fi 126 | java_list='' 127 | java_list=`echo ${java_list} java-${supported_java_version}-openjdk-${arch}` 128 | java_list=`echo ${java_list} java-${supported_java_version}-openjdk` 129 | 130 | for a in i386 amd64 armhf arm64; do 131 | java_list=$(echo ${java_list} oracle-java${supported_java_version}-jdk-${a}/jre) 132 | done 133 | for a in i386 amd64; do 134 | java_list=$(echo ${java_list} oracle-java${supported_java_version}-jre-${a}) 135 | done 136 | for a in x64 i586 arm32-vfp-hflt arm64-vfp-hflt; do 137 | java_list=$(echo ${java_list} jdk-${supported_java_version}-oracle-${a}/jre) 138 | done 139 | for a in x64 i586; do 140 | java_list=$(echo ${java_list} jre-${supported_java_version}-oracle-${a}) 141 | done 142 | for a in i386 amd64 armhf arm64; do 143 | java_list=$(echo ${java_list} adoptopenjdk-${supported_java_version}-hotspot-${a}) 144 | done 145 | 146 | java_list=$(echo ${java_list} java-${supported_java_version}-oracle/jre) 147 | 148 | cur_java=`update-alternatives --query java 2>/dev/null | awk '/^Value: /{print $2}'` 149 | cur_real_java=`readlink -f ${cur_java} 2>/dev/null` 150 | for jvm in ${java_list}; do 151 | jvm_real_java=`readlink -f /usr/lib/jvm/${jvm}/bin/java 2>/dev/null` 152 | [ "${jvm_real_java}" != "" ] || continue 153 | if [ "${jvm_real_java}" == "${cur_real_java}" ]; then 154 | set_java_version $cur_java 155 | return 0 156 | fi 157 | done 158 | 159 | alts_java=`update-alternatives --query java 2>/dev/null | awk '/^Alternative: /{print $2}'` 160 | for cur_java in ${alts_java}; do 161 | cur_real_java=`readlink -f ${cur_java} 2>/dev/null` 162 | for jvm in ${java_list}; do 163 | jvm_real_java=`readlink -f /usr/lib/jvm/${jvm}/bin/java 2>/dev/null` 164 | [ "${jvm_real_java}" != "" ] || continue 165 | if [ "${jvm_real_java}" == "${cur_real_java}" ]; then 166 | set_java_version $cur_java 167 | return 0 168 | fi 169 | done 170 | done 171 | 172 | log_unsupported_java_version 173 | } 174 | 175 | function get_unifi_property () { 176 | if [ -f ${DATADIR}/system.properties ]; then 177 | property_name=$1 178 | cut -d "=" -f2 <<< $(grep "^[^#;]" ${DATADIR}/system.properties | grep $property_name) | cut -d ' ' -f1 179 | fi 180 | } 181 | 182 | function get_environment_property () { 183 | if [ -f /etc/default/${NAME} ]; then 184 | property_name=$1 185 | cut -d "=" -f2 <<< $(grep "^[^#;]" "/etc/default/${NAME}" | grep $property_name) 186 | fi 187 | } 188 | 189 | function get_model_shortname () { 190 | if [ -f /sbin/ubnt-tools ]; then 191 | cut -d "=" -f2 <<< $(sudo /sbin/ubnt-tools id | grep "board.shortname") 192 | fi 193 | } 194 | 195 | function load_jvm_opts () { 196 | heap_min=$(get_unifi_property 'unifi.xms') 197 | heap_max=$(get_unifi_property 'unifi.xmx') 198 | stack_size=$(get_unifi_property 'unifi.xss') 199 | g1gc_enabled=$(get_unifi_property 'unifi.G1GC.enabled') 200 | 201 | set_jvm_opts="" 202 | if [ "${heap_min}" != "" ]; then 203 | set_jvm_opts=$(echo ${set_jvm_opts} -Xms${heap_min}M) 204 | elif [ "${JVM_INIT_HEAP_SIZE}" != "" ]; then 205 | set_jvm_opts=$(echo ${set_jvm_opts} -Xms${JVM_INIT_HEAP_SIZE}M) 206 | fi 207 | if [ "${heap_max}" != "" ]; then 208 | set_jvm_opts=$(echo ${set_jvm_opts} -Xmx${heap_max}M) 209 | elif [ "${JVM_MAX_HEAP_SIZE}" != "" ]; then 210 | set_jvm_opts=$(echo ${set_jvm_opts} -Xmx${JVM_MAX_HEAP_SIZE}M) 211 | else 212 | set_jvm_opts=$(echo ${set_jvm_opts} -Xmx1024M) 213 | fi 214 | if [ "${stack_size}" != "" ]; then 215 | set_jvm_opts=$(echo ${set_jvm_opts} -Xss${stack_size}K) 216 | fi 217 | if [[ "${g1gc_enabled}" != "" && $g1gc_enabled ]]; then 218 | set_jvm_opts=$(echo ${set_jvm_opts} -XX:+UseG1GC) 219 | else 220 | set_jvm_opts=$(echo ${set_jvm_opts} -XX:+UseParallelGC) 221 | fi 222 | if [ "${JAVA_ENTROPY_GATHER_DEVICE}" != "" ]; then 223 | set_jvm_opts=$(echo ${set_jvm_opts} -Djava.security.egd=${JAVA_ENTROPY_GATHER_DEVICE}) 224 | fi 225 | if [ "${UNIFI_JVM_EXTRA_OPTS}" != "" ]; then 226 | set_jvm_opts=$(echo ${set_jvm_opts} ${UNIFI_JVM_EXTRA_OPTS}) 227 | fi 228 | 229 | if [ "${set_jvm_opts}" != "-Xmx1024M -XX:+UseParallelGC" ]; then 230 | set_jvm_opts="\""$set_jvm_opts"\"" 231 | log "Adding UNIFI_JVM_OPTS=$set_jvm_opts to ${DATADIR}/system_env" 232 | echo UNIFI_JVM_OPTS="$set_jvm_opts" >> "${DATADIR}/system_env" 233 | fi 234 | } 235 | 236 | function supported_java_version_set() { 237 | current_java_version=$(get_java_version) 238 | [ "java-${current_java_version}" == "java-${REQUIRED_JAVA_VERSION}" ] 239 | } 240 | 241 | function set_java_home_if_needed() { 242 | supported_java_version_set || set_java_home 243 | } 244 | 245 | function create_system_env_file() { 246 | [ -f "${DATADIR}/system_env" ] && echo -n > "${DATADIR}/system_env" || touch "${DATADIR}/system_env" 247 | } 248 | 249 | function remove_system_env_file_if_exists() { 250 | [ -f "${DATADIR}/system_env" ] && rm "${DATADIR}/system_env" 251 | } 252 | 253 | function load_environment () { 254 | remove_system_env_file_if_exists 255 | if does_not_have_ubnt_tools ; then 256 | create_system_env_file 257 | load_jvm_opts 258 | else 259 | log "Skipping load-environment..." 260 | fi 261 | } 262 | 263 | function dir_symlink_fix () { 264 | local DSTDIR=$1 265 | local SYMLINK=$2 266 | local MYUSER=$3 267 | local MYGROUP=$4 268 | local MYMODE=$5 269 | 270 | [ -d ${DSTDIR} ] || install -o ${MYUSER} -g ${MYGROUP} -m ${MYMODE} -d ${DSTDIR} 271 | [ -d ${SYMLINK} -a ! -L ${SYMLINK} ] && mv ${SYMLINK} `mktemp -u ${SYMLINK}.XXXXXXXX` 272 | [ "$(readlink ${SYMLINK})" = "${DSTDIR}" ] || (rm -f ${SYMLINK} && ln -sf ${DSTDIR} ${SYMLINK}) 273 | } 274 | 275 | function file_symlink_fix () { 276 | local DSTFILE=$1 277 | local SYMLINK=$2 278 | 279 | if [ -f ${DSTFILE} ]; then 280 | [ -f ${SYMLINK} -a ! -L ${SYMLINK} ] && mv ${SYMLINK} `mktemp -u ${SYMLINK}.XXXXXXXX` 281 | [ "$(readlink ${SYMLINK})" = "${DSTFILE}" ] || (rm -f ${SYMLINK} && ln -sf ${DSTFILE} ${SYMLINK}) 282 | fi 283 | } 284 | 285 | function nested_symlinks_fix () { 286 | # Fix issue with nested symbolic links 287 | [ -L "${BASEDIR}"/data/data ] && rm "${BASEDIR}"/data/data 288 | [ -L "${BASEDIR}"/logs/logs ] && rm "${BASEDIR}"/logs/logs 289 | [ -L "${BASEDIR}"/run/run ] && rm "${BASEDIR}"/run/run 290 | [ -L "${BASEDIR}"/data/unifi ] && rm "${BASEDIR}"/data/unifi 291 | [ -L "${BASEDIR}"/logs/unifi ] && rm "${BASEDIR}"/logs/unifi 292 | [ -L "${BASEDIR}"/run/unifi ] && rm "${BASEDIR}"/run/unifi 293 | } 294 | 295 | function init () { 296 | local UMASK=027 297 | umask ${UMASK} 298 | local DIR_MODE=$(printf '%x' $((0x7777 - 0x${UMASK} & 0x0777))) 299 | 300 | nested_symlinks_fix 301 | dir_symlink_fix ${DATADIR} "${BASEDIR}"/data ${USER} ${GROUP} ${DIR_MODE} 302 | dir_symlink_fix ${LOGDIR} "${BASEDIR}"/logs ${USER} ${GROUP} ${DIR_MODE} 303 | dir_symlink_fix ${RUNDIR} "${BASEDIR}"/run ${USER} ${GROUP} ${DIR_MODE} 304 | [ -z "${UNIFI_SSL_KEYSTORE}" ] || file_symlink_fix ${UNIFI_SSL_KEYSTORE} "${BASEDIR}"/data/keystore 305 | [ supported_java_version_set ] || log_unsupported_java_version 306 | load_environment 307 | } 308 | 309 | function init_uos () { 310 | nested_symlinks_fix 311 | create_dirs 312 | set_java_home_if_needed 313 | load_environment 314 | } 315 | 316 | function shutdown_unifi_mongo_service_if_needed() { 317 | if [ "${UNIFI_MONGODB_SERVICE_ENABLED}" == "true" ] && [ $(systemctl is-active --quiet unifi-mongodb && echo $?) ]; then 318 | runningMongoDbVersion=$(mongo localhost:27117/ace --quiet --eval "db.version()") 319 | echo "Running MongoDB version - $runningMongoDbVersion" 320 | installedMongoDbVersion=$(dpkg-query --showformat="\${VERSION}" --show mongodb-server) 321 | echo "Installed MongoDB version - $installedMongoDbVersion" 322 | if [[ $installedMongoDbVersion == *"$runningMongoDbVersion"* ]]; then 323 | echo "MongoDB version has not changed" 324 | else 325 | log "MongoDB version has changed, shutting down unifi-mongodb.service" 326 | systemctl stop unifi-mongodb 327 | log "Finished shutting down unifi-mongodb.service" 328 | fi 329 | fi 330 | } 331 | 332 | ### Helper functions end ### 333 | 334 | # Load variables 335 | [ -f "/etc/default/${NAME}" ] && . "/etc/default/${NAME}" 336 | 337 | USER=${UNIFI_USER:-unifi} 338 | UNIFI_GROUP=$(id -gn ${USER}) 339 | GROUP=${UNIFI_GROUP:-unifi} 340 | DATADIR=${UNIFI_DATA_DIR:-/var/lib/${NAME}} 341 | LOGDIR=${UNIFI_LOG_DIR:-/var/log/${NAME}} 342 | RUNDIR=${UNIFI_RUN_DIR:-/var/run/${NAME}} 343 | ADDITIONAL_STORAGE_DIR=${UNIFI_ADDITIONAL_STORAGE_DIR:-$ADDITIONAL_STORAGE_BASE/$NAME} 344 | TMPFS_DIR=${UNIFI_TMPFS_DIR:-/var/opt/$NAME/tmp} 345 | TMPFS_SIZE=${UNIFI_TMPFS_SIZE:-64m} 346 | MODEL_SHORTNAME=$(get_model_shortname) 347 | UNIFI_CORE_ENABLED=${UNIFI_CORE_ENABLED:-"false"} 348 | UNIFI_MONGODB_SERVICE_ENABLED=${UNIFI_MONGODB_SERVICE_ENABLED:-false} 349 | REQUIRED_JAVA_VERSION='17' 350 | 351 | case "$1" in 352 | init) 353 | if [[ "${UNIFI_CORE_ENABLED}" != "true" ]]; then 354 | init 355 | log "init complete..." 356 | else 357 | log "Skipping init..." 358 | fi 359 | ;; 360 | init-uos) 361 | if [[ "${UNIFI_CORE_ENABLED}" == "true" ]]; then 362 | init_uos 363 | log "init-uos complete..." 364 | else 365 | log "Skipping init-uos..." 366 | fi 367 | ;; 368 | create-dirs) 369 | create_dirs 370 | ;; 371 | cleanup) 372 | unmount_tmpfs_dir 373 | ;; 374 | set-java-home) 375 | set_java_home_if_needed 376 | ;; 377 | load-environment) 378 | load_environment 379 | ;; 380 | shutdown-unifi-mongo-if-needed) 381 | shutdown_unifi_mongo_service_if_needed 382 | ;; 383 | service-started) 384 | log_unifi_service_cpu_time 385 | ;; 386 | *) 387 | log "Usage: $0 {init|init-uos|create-dirs|healthcheck|cleanup|set-java-home|load-environment}" 388 | exit 1 389 | ;; 390 | esac 391 | 392 | exit 0 393 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UniFi Network Controller in a Box - Docker Edition 2 | 3 | 4 | ## Supported tags and respective `Dockerfile` links 5 | 6 | *Note: I do not update the README file regularly. So please check the tag lists 7 | if newer releases have been pushed.* 8 | 9 | * [`8.2`, `latest`, `stable` (Dockerfile)](https://github.com/jcberthon/unifi-docker/blob/master/Dockerfile): currently unifi-8.2 branch 10 | * [`5.9.29`, `5.9`, `oldstable` (Dockerfile)](https://github.com/jcberthon/unifi-docker/blob/master/Dockerfile): currently unifi-5.9 branch 11 | * [`5.6.40`, `5.6`, `lts` (Dockerfile)](https://github.com/jcberthon/unifi-docker/blob/master/Dockerfile): currently unifi-5.6 branch 12 | 13 | You will find specific versions (as they build), e.g. `5.6.39` or `5.10.19` or etc. 14 | And "branch versions" tag such as `5.6` and `5.10` which always point to the latest release within a branch (e.g. the most recent `5.10.x` release). 15 | 16 | "Build" versions per release (e.g. `5.6.39-syyyyyyyy`), I'm using the first 8 characters of the SHA1 commit ID. The purpose is when I'm changing my image definition but UniFi Controller release has not changed, I need to distinguish between the previous and newer image although both are `5.6.39` variants. So when a user picks one of the "build" image he is sure to get the same image definition. 17 | 18 | My recommendation is to either stick to a "rolling tag" (e.g. `lts` or `stable`) 19 | or to pick one of the build tag (for better repeatability, e.g. `5.5.20-s11594497`). 20 | In any case it is recommended to activate scheduled backup in your UniFi Controller 21 | so that if you automatically upgrade when using a rolling tag, you always have a 22 | backup file to revert if things get broken. 23 | 24 | ## Project Presentation 25 | 26 | This project has for purpose to run the UniFi Network Controller (also known as 27 | UniFi Controller or UniFi SDN Controller) inside a Docker container with the 28 | following principles: 29 | - Minimum privilege basis, we expose or need what's required 30 | - Update often, we want security fixes to be includes asap 31 | - Rolling update of the stable UniFi Controller releases 32 | 33 | We have currently the following features to progress towards those goals: 34 | - We drop all capabilities Docker usually grant to a container, no privilege container; 35 | - We forbid acquiring new privileges (e.g. via setuid programs); 36 | - We run the container as a non-root user; 37 | - We provide instructions so you do not need to use the host network; 38 | - We have weekly rebuild, so the full stack (from base image to UniFi Controller package) is kept up-to-date; 39 | - We provide a `stable` tag, which follow the stable branch of UniFi; 40 | - We usually have the UniFi Controller tested internally before it is published here; 41 | - And of course **it works**! 42 | 43 | > **WARNING**: in order to guarantee stability of the UID and GID. We are now 44 | creating a `unifi` dedicated user which will always have the UID 750 and its 45 | main group is also called `unifi` and has GID 750. When updating you will need 46 | to perform a change of ownership on your volumes (`chmod -R 750:750 ...`). 47 | This feature is compatible with the new UniFi Controller 5.6 which supports 48 | a similar feature. 49 | 50 | This project container image can be pulled from: 51 | * [Docker Hub](https://hub.docker.com/r/jcberthon/unifi/): e.g. `docker pull jcberthon/unifi:stable` 52 | * [GitLab Registry](https://gitlab.com/huygens/unifi-docker/container_registry): e.g. `docker pull registry.gitlab.com/huygens/unifi-docker/unifi:stable` 53 | 54 | ## Description 55 | 56 | This is a containerized version of [Ubiquiti Network](https://www.ubnt.com/)'s 57 | UniFi Controller (current LTS is version 5.6 branch). 58 | 59 | Use `docker run --net=host -d jcberthon/unifi` to run it using your host network 60 | stack and with default user settings (usually this is `root` unless you 61 | configured user namespaces), but you might want to do better than that see below. 62 | 63 | The following options may be of use: 64 | 65 | - Set the timezone with `TZ` 66 | - Use volumes to persist application data: the `data` and `log` volumes 67 | 68 | Below are a few examples to test with or simply use the `docker-compose.yml` file 69 | in the repository and do `docker-compose up -d` to start it. That file contains 70 | also a number of sane options which we recommend to use as well, such as: 71 | `restart`; `cpus`; `mem_limit`; `memswap_limit`; `pids_limit`; `cap_drop`; `security_opt`. 72 | It is recommended to change the values of those options to match your environment 73 | and requirements (e.g. increasing the number of cpus). 74 | 75 | > *Note: the following examples set permissions on the volumes so that the 76 | container can run with an **unprivileged user**. This is because the examples are 77 | using bind-mount and therefore you must grant permission to read/write/search 78 | those folders just like if you launched a process as another user which should 79 | access those folders. The example shows that we are setting both user and group 80 | ownership, but of course you full flexibility (only setting user or group, 81 | providing the privileges via the group, using ACLs if your filesystem support 82 | them, etc.)* 83 | 84 | ```console 85 | $ mkdir -p ~/unifi/data 86 | $ mkdir -p ~/unifi/logs 87 | $ sudo chown 750:750 ~/unifi/data ~/unifi/logs 88 | $ docker run --rm --cap-drop ALL -e TZ='Europe/Berlin' \ 89 | -p 8080:8080 -p 8443:8443 -p 8843:8843 \ 90 | -v ~/unifi/data:/var/lib/unifi \ 91 | -v ~/unifi/logs:/var/log/unifi \ 92 | --name unifi jcberthon/unifi 93 | ``` 94 | 95 | In this example, we drop all privileges, activate port forwarding and it can run 96 | on a Docker host with user namespaces configured. However, note that in this 97 | configuration you will need to follow the [UniFi Layer 3 methods for adoption and management] 98 | (https://help.ubnt.com/hc/en-us/articles/204909754-UniFi-Layer-3-methods-for-UAP-adoption-and-management). 99 | I have personally used the DNS and DHCP approach, both works fine. 100 | 101 | A similar example but with the easier L2 adoption, we will need to map the UDP 102 | port 10001. 103 | 104 | > *Note that I expect the following to work but I haven't tested it, simply replace 105 | the last line of the commands given above by:* 106 | 107 | ```console 108 | $ docker run --rm --cap-drop ALL -e TZ='Europe/Berlin' \ 109 | -p 8080:8080 -p 8443:8443 -p 8843:8843 -p 10001:10001/udp \ 110 | -v ~/unifi/data:/var/lib/unifi \ 111 | -v ~/unifi/logs:/var/log/unifi \ 112 | --name unifi jcberthon/unifi 113 | ``` 114 | 115 | You could of course avoid all port mapping and simply use `--net=host`, but by 116 | doing so you give access to the container to your network device(s). If you 117 | run the container as root, it means someone exploiting a future vulnerability 118 | in the UniFi Controller software stack could potentially use that to spy on your 119 | network traffic or worse. So you are removing the isolation layer between your 120 | network stack and your container. It is not bad, it is like if you were running 121 | the UniFi services directly on the host without Docker. Anyway, by default this 122 | container will run as a non-root user, so you could still use that option and 123 | have limited security risk. 124 | 125 | ## Upgrading to newer version 126 | 127 | When upgrading to newer version (e.g. going from the 5.6 to 5.7 branch) it is 128 | good practice to perform a backup before. You can use the UniFi Controller app 129 | to perform such a backup (under Settings -> Maintenance and click the 130 | "Download Backup" button). Make sure your backup is handy, potentially create a 131 | new container on a different host and try to restore the backup to make sure it 132 | works. 133 | 134 | It is highly recommended to check the UniFi changelog for the newer revisions to 135 | verify for known issues, changes, depreciation, etc. They can also contain 136 | additional instructions for upgrading. 137 | 138 | Usually simply stop and delete the previous container and respawn a new container 139 | with the updated Docker image. If you are using docker-compose, you can update 140 | the image tag and simply do `docker-compose up --pull -d` this will pull a newer 141 | image if any, and recreate the container using that image (so it will stop, remove, 142 | create and start the container). 143 | 144 | ## Volumes: 145 | 146 | - `/var/lib/unifi`: Configuration data (e.g. `system.properties`) 147 | - `/var/log/unifi`: Log files (not really needed) 148 | 149 | > *Note: UniFi Controller writes also data under the `/var/run/unifi` folder. 150 | I do not expose that folder in the Dockerfile because I do not need it to 151 | persist its data (there is a PID file and a json file with information about 152 | firmware or controller update). But if you think this information should be 153 | persisted (e.g. when you delete and recreate the container), you can just add 154 | the volume mapping even if the Dockerfile does not define it.* 155 | 156 | ## Environment Variables: 157 | 158 | - `TZ`: TimeZone. (i.e "Europe/Berlin") 159 | 160 | If you want to set UniFi Controller or JVM environment options, you can add 161 | them as environment data when spawning your container or edit the `unifi.default` 162 | file in the current folder and mount the file as a volume (`/etc/default/unifi`), 163 | if we take the previous examples, that would be: 164 | 165 | ```console 166 | $ docker run --rm --cap-drop ALL -e TZ='Europe/Berlin' \ 167 | -p 8080:8080 -p 8443:8443 -p 8843:8843 -p 10001:10001/udp \ 168 | -v ~/unifi/data:/var/lib/unifi \ 169 | -v ~/unifi/logs:/var/log/unifi \ 170 | -v unifi.default:/etc/default/unifi:ro \ 171 | --name unifi jcberthon/unifi 172 | ``` 173 | 174 | ## Ports used by the UniFi Controller: 175 | 176 | The ports which are not exposed by the container image are marked as such. When 177 | not specified, assume the port is exposed. 178 | 179 | - `3478/udp`: STUN service (for NAT traversal - WebRTC, SIP, etc.) - I'm not sure 180 | for which purpose exactly Ubiquiti uses that service, but it seems that one needs 181 | it (at least for switches) to display some information about the managed devices 182 | (e.g. on switches it can display in pseudo-real time the status of the ports). 183 | - `5656-5699/udp`: Used for UPA-EDU (not exposed) 184 | - `6789/tcp`: Speed Test (unifi5 only) 185 | - `8080/tcp`: Device command/control (API) 186 | - `8443/tcp`: Web interface + API 187 | - `8843/tcp`: HTTPS portal (Guest WiFi?) 188 | - `8880/tcp`: HTTP portal (Guest WiFi?) 189 | - `8881/tcp`: do not use (reserved, not exposed) 190 | - `8882/tcp`: do not use (reserved, not exposed) 191 | - `10001/udp`: UBNT Discovery 192 | - `27017/tcp` and 27117/tcp: Local-bound port for DB server (for MongoDB, not exposed) 193 | - `54123/udp`: ??? 194 | 195 | A container should at least redirect port 8443/tcp and port 8843/tcp (if usage of 196 | guest network is required). Also check the `docker-compose.yml` file in this 197 | repository for a list of useful ports to map in order to have a functional 198 | UniFi Controller. 199 | 200 | See [UniFi - Ports Used](https://help.ubnt.com/hc/en-us/articles/218506997-UniFi-Ports-Used) 201 | 202 | ## Running the container on low-memory devices 203 | 204 | Our image is based on Java 8u131 or newer, therefore the JVM is container aware, 205 | it is able to optimise itself to support the CPU and RAM limits set on the 206 | container (based on the CGroups limits). Therefore, the JVM will adapt to the 207 | resource limits you will give the container (e.g. if you use the `--cpus 2.0` or 208 | `--memory 2048m`). Our current approach is that the JVM process could use up to 209 | half of the container allowed max memory, the other half can be used by the MongoDB 210 | database. Not that for a home usage (one AP and one switch), the memory usage of 211 | the container did not exceed 600MB with the current 5.6 branch. 212 | 213 | I haven't tried running it on devices with less than 1GB. But on devices with 1GB 214 | of RAM or less, you should make sure that the Java process can allocate up to 512MB 215 | heap. This means, you will need to set the maximum heap size manually to 512. 216 | 217 | In addition, it is recommended to limit the memory of the complete container. E.g. 218 | if you have 1GB RAM, limit the memory to 768MB so your system (kernel, etc.) 219 | always have some breathing room. And with this setting, there is still enough 220 | memory for MongoDB. 221 | 222 | Example with limits to 768MB memory: 223 | 224 | ```console 225 | $ docker run --rm --cap-drop ALL \ 226 | -e JVM_MAX_HEAP_SIZE="512m" \ 227 | --memory 768m \ 228 | -p 8080:8080 -p 8443:8443 -p 8843:8843 -p 10001:10001/udp \ 229 | -v ~/unifi/data:/var/lib/unifi \ 230 | -v ~/unifi/logs:/var/log/unifi \ 231 | --name unifi jcberthon/unifi 232 | ``` 233 | 234 | ## Container Content 235 | 236 | This container is based on the Docker Hub official image for Ubuntu 18.04 ( 237 | `FROM ubuntu:bionic`) which is currently using OpenJDK 8. Ubiquiti 238 | recommends using either Debian or Ubuntu, so that image looks good. The official 239 | Mongo image is based on Debian 7 Wheezy which means using outdated packages and 240 | based on OpenJDK 7 by default. Not an option. 241 | We did not consider Alpine because of it's use of the musl libc instead of the 242 | GNU libc. The former is not as well tested and I did not want to do extensive 243 | tests of MongoDB and Java 8 based on this C library. 244 | 245 | Our approach does not strictly follows Docker best practices with respect to 246 | micro-services and running one process per container. Our container includes 247 | everything the UniFi controller needs, it has notably an embedded MongoDB 248 | database, along the 3 Java processes which makes the controller. Therefore we 249 | needed a very lightweight sort of init system. The official init script provided 250 | by Ubiquiti provides some signal handling and process watching (restarting on exit, 251 | etc.). These features are slightly redundant to what Docker can do so we do not reuse 252 | their technic but use what Docker provides natively. With our version of the startup 253 | script, we can ensure that **all services can run as a non-privilege user**. 254 | 255 | Our solution originally relied on the Docker-provided `init` daemon (triggered 256 | using `--init`) which provides proper signal handling (catching of SIGTERM, and 257 | "propagation" of signals to childs) and zombie reaping. So the init function 258 | traps SIGTERM to issue the appropriate stop command to the UniFi controller 259 | processes so that they shutdown gracefully. It also prevents zombie to linger 260 | and accumulate. However, this solution relied on Docker 1.13+ which is still 261 | not widely available (many vendors are still only providing Docker 1.12 or older 262 | versions). Therefore, the current solution is to embed a tiny init process, the 263 | same that Docker chose to implement its `init` option: [tini] 264 | (https://github.com/krallin/tini). It offers the signal handlings and zombie 265 | reaping we wanted and it is very tiny (<100KB). 266 | 267 | Example seen within the container after it was started 268 | 269 | ```console 270 | $ docker exec -t 49b9e24a58f8 ps -e -o pid,ppid,cmd 271 | PID PPID CMD 272 | 1 0 /sbin/init -- /usr/lib/unifi/bin/unifi.init start 273 | 6 1 /bin/bash /usr/lib/unifi/bin/unifi.init start 274 | 55 6 unifi -home /usr/lib/jvm/java-8-openjdk-amd64 -cp /usr/share/java/commons-daemon.jar:/usr/lib/unifi/lib/ace.jar -p 275 | 56 55 unifi -home /usr/lib/jvm/java-8-openjdk-amd64 -cp /usr/share/java/commons-daemon.jar:/usr/lib/unifi/lib/ace.jar -p 276 | 57 55 unifi -home /usr/lib/jvm/java-8-openjdk-amd64 -cp /usr/share/java/commons-daemon.jar:/usr/lib/unifi/lib/ace.jar -p 277 | 70 57 /usr/lib/jvm/java-8-openjdk-amd64/jre/bin/java -Xmx1024M -XX:ErrorFile=/usr/lib/unifi/data/logs/hs_err_pid.lo 278 | 89 70 bin/mongod --dbpath /usr/lib/unifi/data/db --port 27117 --logappend --logpath logs/mongod.log --nohttpinterface -- 279 | 959 0 ps -e -o pid,ppid,cmd 280 | ``` 281 | 282 | ## Build and Advanced options/configurations 283 | 284 | We provide the `Dockerfile` so of course you can build your own container image. 285 | Example build instructions: 286 | 287 | ```console 288 | $ docker build -t jcberthon/unifi . 289 | ``` 290 | 291 | Before building your container, you can tweak the file `unifi.default`. 292 | 293 | This files contains several parameters which can override the default configuration. 294 | The file contains descriptions of those parameters. But you should be aware that 295 | by changing them you could break the controller (especially if you try to change 296 | the data and log folders, but do not change the volumes of the container). 297 | 298 | The possible parameters can be (they are described in the unifi.default file in much details): 299 | * `UNIFI_DATA_DIR`: data folder for UniFi Controller, change with caution 300 | * `UNIFI_LOG_DIR`: log folder for UniFi Controller, change with caution 301 | * `UNIFI_RUN_DIR`: runtime folder for UniFi Controller 302 | * `JAVA_ENTROPY_GATHER_DEVICE`: advanced parameter, most people should not require it 303 | * `JVM_MAX_HEAP_SIZE`: limit the JVM maximum heap size (for home and SOHO, 512M or 1024M is a good value) 304 | * `JVM_INIT_HEAP_SIZE`: minimum JVM heap size (on startup), usually not needed 305 | * `UNIFI_JVM_EXTRA_OPTS`: additional JVM parameters can be added here 306 | * `ENABLE_UNIFI`: boolean ('yes' or 'no') leave it to 'yes' or unset, as you want the UniFi Controller to run 307 | 308 | ## Changelog 309 | 310 | This work was based on the original project https://github.com/jacobalberty/unifi-docker. 311 | However, there is little left of the original project and not really chances of 312 | merging. So I've decided to cut the link between the parent project and this one. 313 | Anyway, thank you @jacobalberty for getting me kick started on this. 314 | 315 | The first change compare to original fork is that I use `tini` init process and 316 | the UniFi Controller init script. So I could remove a lot of unecessary stuff. 317 | 318 | In addition, I use the [Debian/Ubuntu APT repository from UniFi](https://help.ubnt.com/hc/en-us/articles/220066768-UniFi-How-to-Install-Update-via-APT-on-Debian-or-Ubuntu) 319 | instead of downloading individual packages, this avoids changing the Dockerfile 320 | for each new release from UniFi. I simply need to rebuild my image. In addition, 321 | Ubiquiti is using "rolling updates" so that by using the "stable" branch you get 322 | always the latest stable release (was 5.4.x when I started, is now 5.6.x at time 323 | of edition) 324 | 325 | Finally the last change is about security, I'm dropping every possible privileges, 326 | I can use user namespaces so that the container processes do not run as root, 327 | I'm not binding the container to the host networking but using Docker default 328 | bridge network so that I can control which service I expose on my network, it 329 | works very good using L3 adoption, it should work with L2 adoption if you 330 | expose the port `10001/udp` but I haven't tried it. 331 | 332 | Note that with the latest update, you do not need to have user namespaces activated, 333 | I've set-up the Dockerfile so that all services can run as non-root user and I have 334 | set a default user (non-root). So you do not need to add special instructions, 335 | when you spawn your container, it will run as non-root user. You still need to 336 | specify proper permissions on the bind-mounted folder (UID should be 750 and GID 337 | should be 750) in order for the processes to have the rights to read or modify 338 | data. If you use Docker named volumes (the provided `docker-compose.yml` does 339 | that by default, or you can create them using the `docker volume create ...` 340 | command), you do not need to specify permissions, Docker will do that to you (at 341 | least with the `local` driver, the default one). 342 | *Note: If you really want to run as root, you can simply add `--user root` to the 343 | `docker run` command (or `user: root` inside the compose file).* 344 | 345 | A small extra touch, I've added a `HEALTHCHECK` directive in the `Dockerfile`, it 346 | will require you to build the container image with at least Docker 1.12. But it 347 | provides a neat visualisation when querying the container for its state (starting, 348 | healthy, etc.) and can be used by others (e.g. Swarm) for better orchestration. 349 | 350 | Example: 351 | ```console 352 | $ docker ps 353 | CONTAINER ID IMAGE CREATED STATUS NAMES 354 | 7bb52a751107 jcberthon/unifi-docker/unifi:latest 44 seconds ago Up 43 seconds (health: starting) unifi 355 | $ docker ps 356 | CONTAINER ID IMAGE CREATED STATUS NAMES 357 | 7bb52a751107 jcberthon/unifi-docker/unifi:latest 3 minutes ago Up 3 minutes (healthy) unifi 358 | ``` 359 | --------------------------------------------------------------------------------