UniFi Network Application and MongoDB pod
An app and its database in one pod, with the passwords in podman secrets instead of the unit files.
- Pod with several containers
- Rootless: systemctl --user
- Tested with podman 5.0 and newer
Last updated
Updated : added health checks and a Caddy block that fixes the 403 on login.
See where this fits on the homelab connections map, with every port it uses.
In short: The linuxserver UniFi image needs an external MongoDB. Put both in unifi.pod: the app reaches the database on localhost:27017 and the database port is never published. The passwords live in two podman secrets that are injected as environment variables, and an init script creates the app's database user on the first start. Pin MongoDB to a major version; mongo:8.0 needs UniFi Network Application 9.0 or newer.
The quadlet files
Every quadlet file below passes podman's quadlet generator and
systemd-analyze verify on podman 5.0 and the current release. Personal values
are placeholders: replace example.com, /srv/... paths and the
time zone with your own.
unifi.pod
Save as ~/.config/containers/systemd/unifi.pod
[Unit]
Description=UniFi Network Application pod
[Pod]
PodName=unifi
# Web UI (https)
PublishPort=8443:8443
# Device inform: must stay 8080 on both sides
PublishPort=8080:8080
# STUN
PublishPort=3478:3478/udp
# Device discovery
PublishPort=10001:10001/udp
unifi-db.container
Save as ~/.config/containers/systemd/unifi-db.container
[Unit]
Description=MongoDB for the UniFi Network Application
[Container]
# Pin the major version: MongoDB does not upgrade across major versions on its own.
Image=docker.io/library/mongo:8.0
ContainerName=unifi-db
Pod=unifi.pod
Volume=unifi-db.volume:/data/db
Volume=%h/.config/containers/unifi/init-mongo.sh:/docker-entrypoint-initdb.d/init-mongo.sh:ro,Z
Environment=MONGO_INITDB_ROOT_USERNAME=root
Environment=MONGO_USER=unifi
Environment=MONGO_DBNAME=unifi
Environment=MONGO_AUTHSOURCE=admin
Secret=unifi-mongo-root,type=env,target=MONGO_INITDB_ROOT_PASSWORD
Secret=unifi-mongo-pass,type=env,target=MONGO_PASS
HealthCmd=mongosh --quiet --norc --eval 'db.adminCommand({ping:1}).ok'
HealthInterval=2m
HealthTimeout=30s
HealthRetries=3
HealthStartPeriod=2m
HealthOnFailure=kill
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target
unifi-db.volume
Save as ~/.config/containers/systemd/unifi-db.volume
[Volume]
VolumeName=unifi-db
unifi-app.container
Save as ~/.config/containers/systemd/unifi-app.container
[Unit]
Description=UniFi Network Application
Requires=unifi-db.service
After=unifi-db.service
[Container]
Image=lscr.io/linuxserver/unifi-network-application:latest
ContainerName=unifi-app
Pod=unifi.pod
AutoUpdate=registry
Volume=unifi-app.volume:/config
Environment=TZ=Etc/UTC
# Containers in a pod share localhost, so the database is not reachable by name.
Environment=MONGO_HOST=localhost
Environment=MONGO_PORT=27017
Environment=MONGO_USER=unifi
Environment=MONGO_DBNAME=unifi
Environment=MONGO_AUTHSOURCE=admin
Secret=unifi-mongo-pass,type=env,target=MONGO_PASS
HealthCmd=curl -fsk -o /dev/null https://127.0.0.1:8443/status
HealthInterval=2m
HealthTimeout=15s
HealthRetries=3
HealthStartPeriod=5m
HealthOnFailure=kill
[Service]
Restart=always
TimeoutStartSec=900
[Install]
WantedBy=default.target
unifi-app.volume
Save as ~/.config/containers/systemd/unifi-app.volume
[Volume]
VolumeName=unifi-app
init-mongo.sh
Save as ~/.config/containers/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" }
]
})
EOF
Caddyfile
Save as /etc/caddy/Caddyfile
unifi.example.com {
reverse_proxy https://10.0.0.10:8443 {
# The controller answers the login with 403 when Host and Origin do not
# match its own address, so send the upstream's address in both.
header_up Host {upstream_hostport}
header_up Origin https://{upstream_hostport}
transport http {
# The controller uses a self-signed certificate.
tls_insecure_skip_verify
}
}
}
Why is it built this way?
This is the general app-plus-database shape, shown with the UniFi Network Application because the linuxserver image needs an external MongoDB, and that setup trips many people up. Swap the images and variables and the same five files run any app with PostgreSQL, MariaDB or Redis.
Why a pod. The app and its database always run together. Inside
unifi.pod the app connects to localhost:27017, and the pod publishes
only the UniFi ports: the database is never reachable from your network. Starting
unifi-pod.service starts both containers.
Start order. Requires=unifi-db.service and
After=unifi-db.service in the app's [Unit] section start MongoDB
first, and stop the app if the database unit fails. The app retries its database connection on
its own while MongoDB finishes starting.
Secrets, not passwords in files. Both passwords are podman secrets injected as
environment variables with Secret=…,type=env,target=…. The unit files contain no
password, so you can keep them in git. The same unifi-mongo-pass secret feeds the
init script in the database and the connection settings in the app, so they always match.
The init script. The official MongoDB image runs every script in
/docker-entrypoint-initdb.d/ once, when /data/db is empty.
init-mongo.sh is the script from the linuxserver documentation, unchanged: it creates
the unifi user with the roles the application needs. %h in the
Volume= line is systemd's placeholder for your home directory.
Different update rules. MongoDB is pinned to mongo:8.0 without
AutoUpdate=, because a major-version jump needs a manual migration and a database
should change only after a backup. The app uses latest with
AutoUpdate=registry, like the linuxserver images in the
media stack pod.
Health checks. The app is checked on its own /status page and the
database with a mongosh ping, every two minutes. A hung controller or database is
killed and restarted by systemd; HealthStartPeriod=5m covers the controller's slow
first start.
Behind Caddy. The controller speaks HTTPS with a self-signed certificate on port
8443, so Caddy connects with tls_insecure_skip_verify on the LAN hop only. The
controller also compares the Host and Origin headers of the login
request with its own address and answers 403 when they differ; the two header_up
lines send the upstream's address instead. Device inform traffic on port 8080 goes straight to
the pod, never through Caddy.
How do I start it?
Run these as your normal user, not as root. Quadlet reads ~/.config/containers/systemd/ for user services.
mkdir -p ~/.config/containers/systemd ~/.config/containers/unifi
# save init-mongo.sh in ~/.config/containers/unifi/, the other files in ~/.config/containers/systemd/
# two random passwords; tr strips the newline so it does not end up in the password
openssl rand -hex 24 | tr -d '\n' | podman secret create unifi-mongo-root -
openssl rand -hex 24 | tr -d '\n' | podman secret create unifi-mongo-pass -
systemctl --user daemon-reload
systemctl --user start unifi-pod.service
loginctl enable-linger "$USER"
# then open https://<server>:8443 and run the setup wizard
Check the result with systemctl --user status and
the logs with journalctl --user -u followed by the
service name. To turn a docker-compose file of your own into files like these, use the
compose to quadlet converter.
Common mistakes
MongoDB exits with Illegal instruction (core dumped)
MongoDB 5.0 and newer need a CPU with AVX on x86_64, which some Celeron and Pentium models lack. Use Image=docker.io/library/mongo:4.4 on those machines; the UniFi Network Application still supports it.
UniFi cannot log in to MongoDB: Authentication failed
The init script only runs when /data/db is empty. If the database first started without it, or with a different password, stop the pod, delete the data (podman volume rm unifi-db) and start again. Also check that the secret has no trailing newline.
Access points stay on Adopting or never appear
The controller announces its container address, which your devices cannot reach. In the UniFi settings, enable Inform Host override and set it to the server's LAN address, or run set-inform http://<server>:8080/inform over SSH on the device. Keep port 8080 identical on both sides.
Login through a reverse proxy fails with 403 Forbidden
The controller rejects the login request when the Host and Origin headers do not match its own address. In Caddy, add header_up Host {upstream_hostport} and header_up Origin https://{upstream_hostport} inside the reverse_proxy block, as in the Caddyfile above, then reload Caddy.
MONGO_HOST=unifi-db does not resolve
Containers in a pod share one network namespace and reach each other on localhost, not by container name. Set MONGO_HOST=localhost.
Frequently asked questions
Which MongoDB version does the UniFi Network Application support?
According to the linuxserver image documentation, version 8.1 of the application supports MongoDB 3.6 through 7.0, and version 9.0 adds MongoDB 8.0. Pin the major version in the image tag, because MongoDB does not upgrade across major versions by itself.
Why run the app and its database in one pod?
They always start and stop together, and in a pod the database listens on localhost only, so its port is never published to your network. One unifi-pod.service starts both; Requires= and After= make sure MongoDB starts before the app.
How do I keep database passwords out of quadlet files?
Create a podman secret (podman secret create name -) and reference it with Secret=name,type=env,target=VARIABLE. The value is injected as an environment variable when the container starts, so it never appears in the unit file or the generated service.
Can I use this layout for other apps with a database?
Yes. Replace the images and variables: for example an app with PostgreSQL uses docker.io/library/postgres:17 with Secret=db-pass,type=env,target=POSTGRES_PASSWORD, and the app connects to localhost:5432. The pod, volumes and dependencies stay the same.
How do I back up the UniFi controller?
Use the application's built-in automatic backups, which it stores in its config volume, and copy that volume off the host: podman volume export unifi-app -o unifi-app.tar. Run the export from a systemd timer if you want it daily.