Run a UniFi controller on a VPS
Host the UniFi Network Application on a VPS: RAM sizing, Docker with MongoDB, Layer 3 adoption with set-inform, and the ports to keep private.
What a UniFi controller on a VPS actually does
A UniFi controller on a VPS is one management server that stays reachable when the sites it manages go down. The software is Ubiquiti's UniFi Network Application: a Java program with a MongoDB database behind it. It configures your access points and switches, stores their statistics, and serves the admin interface. It does not carry client traffic.
That last point decides where it should live. Put the controller on a machine inside the office it manages and you lose the network and the tool for looking at the network in the same minute. Put it on a VPS with a stable public address and it keeps running, keeps collecting, and can adopt devices at several sites from one place. It wants uptime, not horsepower.
When the controller is offline, adopted access points and switches keep forwarding traffic with the configuration already pushed to them. You lose the dashboard and the statistics, plus any feature that needs the controller live: a guest portal login, or RADIUS (remote authentication dial-in user service) if the controller is your RADIUS server. Clients stay connected.
How much RAM does a UniFi controller need?
Two GB is the floor and 4 GB is the number to buy. There are two memory consumers in one box, Java and MongoDB, and they size themselves independently of each other.
The Java heap is capped by MEM_LIMIT, which the container image sets to 1024 MB by default. MongoDB is the other half. Its WiredTiger storage engine sizes its cache at half of the RAM above 1 GB, or 256 MB, whichever is larger. On a 2 GB VPS that is roughly 512 MB of cache plus a 1 GB heap plus the JVM's own non-heap memory plus the operating system. It fits until a busy day, and then the kernel out-of-memory killer ends one of the two processes. After any unexplained restart, run dmesg -T | grep -i 'killed process' to find out whether that is what happened. Add a swap file if 2 GB is what you have.
CPU and disk are undemanding. One or two vCPU handles a few dozen devices. Start with 20 GB of disk and watch it, because the database grows with the number of clients you see and how long you keep statistics. A controller alone leaves most of a 4 GB box idle, so if you plan to give it a housemate, size for the housemate first, because PhotoPrism and Immich have very different RAM floors and either one asks for more than the controller does.
One CPU feature matters, and it is easy to miss on a cheap plan:
grep -m1 -o avx /proc/cpuinfoMongoDB 5.0 and later need AVX (advanced vector extensions) on x86_64 hardware. If that command prints nothing, mongod dies during startup and the container restarts in a loop, because the binary runs an instruction the CPU does not have. Older Intel Celeron and Pentium hosts are the usual cause, as are hypervisors that hide CPU flags from the guest. MongoDB 4.4 does not need AVX and is the only fallback, but that is a database version upstream no longer patches. Moving to a host with a newer CPU is the better answer. On an ARM VPS the question does not arise, since AVX is an x86 instruction set, and both images publish arm64 builds. If you are choosing between the two, the differences between ARM and x86 VPS plans go further than the price.
Install the UniFi Network Application with Docker Compose
Docker is the path with the fewest surprises, because it lets you pin MongoDB to a version the application supports instead of taking whatever your distribution ships. If Docker is not on the box yet, install Docker on a VPS first.
mkdir -p ~/unifi/config ~/unifi/db
cd ~/unifiMongoDB needs a user before the application can log in. The official MongoDB image runs any script it finds in /docker-entrypoint-initdb.d on first start. Save this as ~/unifi/init-mongo.sh:
#!/bin/bash
if which mongosh > /dev/null 2>&1; then
mongo_init_bin='mongosh'
else
mongo_init_bin='mongo'
fi
"${mongo_init_bin}" <<EOF
use ${MONGO_AUTHSOURCE}
db.auth("${MONGO_INITDB_ROOT_USERNAME}", "${MONGO_INITDB_ROOT_PASSWORD}")
db.createUser({
user: "${MONGO_USER}",
pwd: "${MONGO_PASS}",
roles: [
"clusterMonitor",
{ db: "${MONGO_DBNAME}", role: "dbOwner" },
{ db: "${MONGO_DBNAME}_stat", role: "dbOwner" },
{ db: "${MONGO_DBNAME}_audit", role: "dbOwner" },
{ db: "${MONGO_DBNAME}_restore", role: "dbOwner" }
]
})
EOFThat script runs only when the database directory is empty. Start the stack once with the wrong password and the user is created with the wrong password, and editing the compose file afterwards changes nothing, because the script never runs again. The symptom is the application container logging MongoDB authentication failures while the web interface never appears. On a fresh install the fix is to stop the stack, delete ~/unifi/db, and start again.
Then write ~/unifi/compose.yaml:
services:
unifi-db:
image: docker.io/mongo:8.0
container_name: unifi-db
environment:
- MONGO_INITDB_ROOT_USERNAME=root
- MONGO_INITDB_ROOT_PASSWORD=change-this-root-password
- MONGO_USER=unifi
- MONGO_PASS=change-this-unifi-password
- MONGO_DBNAME=unifi
- MONGO_AUTHSOURCE=admin
volumes:
- ./db:/data/db
- ./init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh:ro
restart: unless-stopped
unifi-network-application:
image: lscr.io/linuxserver/unifi-network-application:10.5.67-ls141
container_name: unifi-network-application
depends_on:
- unifi-db
environment:
- PUID=1000
- PGID=1000
- TZ=Etc/UTC
- MONGO_USER=unifi
- MONGO_PASS=change-this-unifi-password
- MONGO_HOST=unifi-db
- MONGO_PORT=27017
- MONGO_DBNAME=unifi
- MONGO_AUTHSOURCE=admin
- MEM_LIMIT=1024
- MEM_STARTUP=1024
volumes:
- ./config:/config
ports:
- "8080:8080"
- "3478:3478/udp"
- "127.0.0.1:8443:8443"
restart: unless-stoppedBoth image tags are pinned on purpose. 10.5.67-ls141 was the current application release in August 2026, so check the image's release list and pin whatever is current when you install. The database tag matters more. MongoDB does not upgrade its data files across major versions on its own, so mongo:latest will one day pull a new major version, refuse to open the files it finds, and restart in a loop. Pin the major version and move it deliberately. UniFi Network 8.1 and later support MongoDB 3.6 through 7.0, and 9.0 added support for MongoDB 8.0.
PUID and PGID must match a real user on the host, or the files under ./config end up owned by an identity that cannot write them. Run id to get yours. how PUID and PGID work in container images covers what a mismatch looks like.
Start it and watch:
docker compose up -d
docker compose ps
docker compose logs -f unifi-network-applicationdocker compose ps should show both containers as running. A unifi-db stuck in restarting is either the AVX problem above or a permission problem on ./db. Once the log settles, check the two listeners:
curl -sk -o /dev/null -w '%{http_code}\n' https://127.0.0.1:8443/
curl -s -o /dev/null -w '%{http_code}\n' http://127.0.0.1:8080/informAny HTTP status code at all means the listener is bound and answering. Connection refused means the application is still starting, which takes a minute or two on a small VPS on the first run, or that it never started.
Reach the admin interface without exposing it
Port 8443 is published on 127.0.0.1 in the file above, so nothing outside the VPS can reach the admin interface. Forward it over SSH to run the setup wizard:
ssh -L 8443:127.0.0.1:8443 you@vps.example.comLeave that session open and browse to https://127.0.0.1:8443. The certificate is self signed, so the browser warns once. Create the administrator account, name the site, and skip device adoption for now.
An SSH tunnel is fine for one administrator. For a team, give the VPS a private address and bind the interface to that instead. a WireGuard VPN on your own VPS and a Tailscale subnet router both give you an address only your people can route to. Change the published port to 10.8.0.1:8443:8443 for WireGuard, or to the address Tailscale assigns. One catch: Docker cannot publish on an address that does not exist yet, so the tunnel interface has to come up before the container starts, or the container fails with a bind error.
Why a remote UniFi device will not adopt
Out of the box a UniFi device finds its controller by broadcasting on the local network, UDP port 10001. A broadcast does not leave the LAN, so a device in an office in another city will never discover a controller on a VPS. This is Layer 3 adoption, and it is where most people get stuck. The device is fine and the controller is fine. Nothing has told the device where to look.
First, tell the controller what address to hand out. In the controller's Settings, in the System section, there is an inform host setting with an override option. Set it to the public hostname or IP of your VPS. Without it the controller advertises the address it sees on its own interface, which inside a Docker bridge network is a private address such as 172.18.0.3. The device receives that address, cannot route to it, and goes back to searching.
Then point the device at that address. SSH to the device on the remote LAN. A factory default device accepts the username ubnt with the password ubnt:
ssh ubnt@192.168.1.20
set-inform http://vps.example.com:8080/informNewer device firmware drops you into a menu instead of a shell. Run the same thing as a single command:
ssh ubnt@192.168.1.20 mca-cli-op set-inform http://vps.example.com:8080/informThe device now appears in the controller as ready to adopt. Click Adopt, and the state changes to Adopting. Here is the part that surprises everyone: you usually have to run set-inform a second time. The device restarts into provisioning and falls back to the inform URL saved in its own configuration, which the controller has not finished replacing. Running the command again while the state reads Adopting completes the handover. Type info on the device to see the inform URL and state it currently holds.
If the device was adopted by another controller before, set-inform alone will not finish, because it still holds that controller's credentials. Reset it to factory default first, either with the reset button or with set-default over SSH using the old credentials.
For more than a handful of devices, use DHCP instead. DHCP (dynamic host configuration protocol) option 43 carries a vendor-specific value, and UniFi devices read the inform URL out of suboption 2. Build the hex string on any Linux box:
URL="http://vps.example.com:8080/inform"
HEX=$(printf '%s' "$URL" | od -An -tx1 | tr -d ' \n')
printf '02%02x%s\n' "${#URL}" "$HEX"For http://192.168.3.10:8080/inform, a 31 byte string, that prints 021f687474703a2f2f3139322e3136382e332e31303a383038302f696e666f726d. Paste the result into your router's DHCP option 43 field as a hex value. Every device that boots on that network then learns the controller address from its lease, with no SSH at all. Older guides show suboption 1 instead, 0104 followed by the four bytes of an IPv4 address in hex, and devices still accept that form.
There is a third route if you run DNS at the site. A UniFi device tries to resolve the hostname unifi on boot, so an A record for unifi pointing at your VPS address adopts devices with no per-device work. It only helps where you control the resolver the devices actually use.
Which UniFi ports to open, and which to keep private
Only two ports need to be reachable from a remote site.
- TCP 8080 is the inform channel, and every adopted device connects to it. The payload inside is AES encrypted with a key the controller gave the device during adoption, which is why plain HTTP is the normal setting here.
- UDP 3478 is STUN (session traversal utilities for NAT), which devices use to keep a path back to the controller.
Everything else stays closed on a VPS.
- TCP 8443 is the admin interface. This is the one that must never be public. It holds the configuration for every site the controller manages, behind one password.
- UDP 10001 and UDP 1900 are broadcast discovery. Broadcasts do not cross the internet, so opening them achieves nothing.
- TCP 8880 and TCP 8843 are the guest portal redirects. Open them only if you run a guest portal.
- TCP 6789 is the mobile speed test and UDP 5514 is remote syslog. Add them when you use them.
- TCP 27117 is MongoDB. In the compose file above the database publishes no ports at all, so it exists only on the internal Docker network. Keep it that way.
If your sites have static public addresses, allow only those:
sudo ufw allow OpenSSH
sudo ufw allow proto tcp from 203.0.113.4 to any port 8080
sudo ufw allow proto udp from 203.0.113.4 to any port 3478
sudo ufw enable
sudo ufw status verbosethe ufw basics for a VPS firewall covers the default deny setup those rules assume.
There is a trap here that catches people every time. Docker's published ports walk around ufw. Publishing a port writes NAT and forwarding rules straight into iptables, and that traffic is filtered in Docker's own chain, not in the INPUT chain ufw manages. So ufw deny 8443 looks correct in ufw status while the port stays open to the world. Test it from another machine, never from the VPS itself:
nc -vz vps.example.com 8443A refusal or a timeout is what you want. If it connects, the port is public whatever ufw says. The reliable fix is the one already in the compose file: publish the port on 127.0.0.1 or on a tunnel address, so Docker never binds it to the public interface. A rule in the DOCKER-USER chain also works, but binding is simpler, and a rule ordering mistake cannot undo it.
What about Ubiquiti's own installers?
Ubiquiti publishes a Debian package for the Network Application. It works, but on current Ubuntu it raises a MongoDB question the distribution no longer answers: Ubuntu 22.04 and 24.04 ship no MongoDB server package, so you end up adding MongoDB's own repository and matching versions by hand. The container above does that matching in one pinned tag, which is why it is the path here.
Ubiquiti's newer self-hosted product is UniFi OS Server, which runs the UniFi applications in Podman containers and gives you the same UniFi OS as their hardware consoles. As of August 2026 it wants x86_64 Ubuntu 22.04 or 24.04, Podman 4.3.1 or newer with slirp4netns, and asks for 2 vCPU with 4 GB of RAM as a minimum, 4 vCPU with 8 GB recommended. The installer sits behind a free Ubiquiti account on their downloads page, so there is no stable one line URL to paste into a guide. It creates a system user called uosserver and runs the containers as that user. Choose it if you want the vendor's own packaging. Choose the container stack if you want to pin versions yourself and keep the box free for other work.
Where UniFi backups live, and how to get them off the box
The controller writes its own backups on a schedule you set in Settings, in the backup section, along with how many to keep. The files land in /config/data/backup/autobackup inside the container, which is ~/unifi/config/data/backup/autobackup on the host, named like autobackup_10.5.67_20260813_1200_1755086400004.unf.
Check that they actually appear:
ls -l ~/unifi/config/data/backup/autobackupAn empty directory a day after you set a schedule is a known failure on fresh container installs. The application expects the autobackup directory to exist and does not create it, so the scheduled job silently writes nothing. Create it yourself as the same user the container runs as, then wait for the next run:
mkdir -p ~/unifi/config/data/backup/autobackup
docker compose restart unifi-network-applicationA .unf file holds the site configuration and the administrator accounts, so treat it like a key. Pull copies to a machine you control and keep them private:
rsync -av you@vps.example.com:~/unifi/config/data/backup/autobackup/ ~/unifi-backups/Restoring is one step. The first page of the setup wizard on a new install offers to restore from a backup file, and a running controller takes one from the same settings page. Restore into the same version or a newer one. A backup written by a newer application than the one you are restoring into is rejected, which is the reason to record the version number along with the file.
What a controller upgrade can break
Take a manual backup and download it before every upgrade. Then:
docker compose pull
docker compose up -d
docker compose logs -f unifi-network-applicationThe database is the first thing that goes wrong. Changing the mongo tag to a new major version in the same edit as the application is the fastest route to a controller that will not start, because MongoDB will not open data files from a different major version without a staged upgrade. Upgrade the application on its own. Move MongoDB separately, one major version at a time, with a fresh backup in hand.
Memory is the next thing. A larger release wants a larger heap. If the application starts, runs for a few minutes, and dies, raise MEM_LIMIT and MEM_STARTUP to 1536 or 2048 and restart. dmesg -T | grep -i 'killed process' on the host confirms whether the kernel is the one ending it.
Device firmware is the risk people forget. After the controller upgrades itself it offers firmware upgrades for adopted devices. Do not accept them in the same session. If a device upgrade and a controller upgrade overlap and the link between them drops, the device can sit half provisioned, and you are back to set-inform over SSH on hardware in another building.
The upgrade window itself is gentler than it sounds. Devices keep forwarding traffic while the controller restarts, so users see nothing. What does stop is the guest portal and RADIUS if the controller serves it, so pick a time when neither is in use. A controller that dies quietly at 3 a.m. is worth knowing about, so point an Uptime Kuma status monitor at port 8080 and let it tell you.
The honest alternative: Ubiquiti's hosted console
Ubiquiti sells the same job as a service. As of August 2026 the Official UniFi Cloud Console starts at $29 a month and manages up to 500 UniFi devices, with Ubiquiti running the updates and the backups. The self-hosted application you just installed is free and carries no subscription.
Choose the hosted console if you manage one site and would rather pay than patch. Choose a VPS if you manage several sites, or if you want the controller inside a network you control and sharing a box with the other services you run. The cost difference at small scale is real, but it is not the only thing to weigh: a hosted console is somebody else's uptime, and your VPS is yours, including the night its disk fills up. If the box is going to earn its keep either way, what else you can run on a VPS is the list to read next.
FAQ
Why will my UniFi device not adopt to a controller on a VPS?
Devices discover controllers by broadcasting on UDP port 10001, and a broadcast never leaves the local network, so a device at a remote site cannot find a controller on the public internet. Set the inform host override in the controller's system settings to your VPS hostname, then point the device at it with ssh ubnt@<device-ip> followed by set-inform http://vps.example.com:8080/inform. If the device sits in the Adopting state, run set-inform again while it is there. If another controller adopted it before, reset it to factory default first, because it still holds the old controller's credentials.
How much RAM does a self-hosted UniFi controller need?
Two GB is the working floor and 4 GB is comfortable. The application is Java plus MongoDB, and the two size their memory separately: the container image caps the Java heap at 1024 MB by default, while MongoDB's WiredTiger cache takes half of the RAM above 1 GB. On x86_64, also confirm the CPU exposes AVX with grep -m1 -o avx /proc/cpuinfo, because MongoDB 5.0 and later will not start without it and the database container restarts in a loop.
Should I expose port 8443 to the internet?
No. Port 8443 is the admin interface, and it holds the configuration for every site the controller manages. Publish it on 127.0.0.1 and reach it with ssh -L 8443:127.0.0.1:8443 you@vps.example.com, or bind it to a WireGuard or Tailscale address. Only TCP 8080 and UDP 3478 need to be reachable from your sites, and you can restrict those to the sites' public addresses when they are static. Remember that a Docker published port is not filtered by ufw, so test from an outside machine rather than trusting ufw status.
Does my network stop working if the VPS controller goes down?
No. Adopted access points and switches keep forwarding traffic using the configuration the controller already pushed, so clients stay connected and Wi-Fi keeps working. What stops is management. You lose the dashboard and statistics collection, plus any live feature the controller serves, such as guest portal authentication or RADIUS when the controller is the RADIUS server.
Where does the UniFi controller store its automatic backups?
In the container image used here they land in /config/data/backup/autobackup, which maps to your data path plus data/backup/autobackup on the host, as .unf files named after the version and a timestamp. On some fresh installs the autobackup directory does not exist, and the scheduled backup then writes nothing without reporting an error, so list that directory a day after you set a schedule and create it yourself if it is empty. Copy the files off the VPS, because a .unf contains the site configuration and the administrator accounts.