job "unifi" { datacenters = ["dc1"] region = "global" group "unifi" { network { mode = "bridge" port "stun" { to = 3478 } } service { name = "unifi" port = 8888 connect { sidecar_service { } sidecar_task { config { args = [ "-c", "${NOMAD_SECRETS_DIR}/envoy_bootstrap.json", "-l", "${meta.connect.log_level}", "--concurrency", "${meta.connect.proxy_concurrency}", "--disable-hot-restart" ] } resources { cpu = 50 memory = 64 } } } check { type = "http" path = "/status" expose = true interval = "10s" timeout = "5s" check_restart { limit = 12 grace = "2m" } } tags = [ "traefik.enable=true", "traefik.http.routers.unifi.entrypoints=https", "traefik.http.routers.unifi.rule=Host(`unifi.example.org`)", "traefik.http.middlewares.csp-unifi.headers.contentsecuritypolicy=default-src 'self';font-src 'self' data:;img-src 'self' data:;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';", "traefik.http.middlewares.unifi-inflight.inflightreq.amount=300", "traefik.http.middlewares.unifi-rate-limit.ratelimit.average=100", "traefik.http.middlewares.unifi-rate-limit.ratelimit.burst=200", "traefik.http.routers.unifi.middlewares=security-headers@file,unifi-rate-limit,forward-proto@file,inflight-std@file,unifi-inflight,hsts@file,compression@file,csp-unifi", "traefik.enable=true", "traefik.http.routers.unifi-inform.entrypoints=unifi-inform", "traefik.http.routers.unifi-inform.rule=(Path(`/inform`) && Method(`POST`)) || (PathPrefix(`/dl/firmware-cached`) && (Method(`GET`) || Method(`HEAD`)))", "traefik.http.routers.unifi-inform.middlewares=rate-limit-std@file,inflight-std@file", "traefik.enable=true", "traefik.http.routers.unifi-portal.entrypoints=unifi-portal", "traefik.http.routers.unifi-portal.rule=Host(`unifi-portal.example.org`) && PathPrefix(`/guest`)", "traefik.http.middlewares.csp-unifi-portal.headers.contentsecuritypolicy=default-src 'self';font-src 'self' data:;img-src 'self' data:;script-src 'self' 'unsafe-inline' 'unsafe-eval';style-src 'self' 'unsafe-inline';", "traefik.http.routers.unifi-portal.middlewares=security-headers@file,rate-limit-std@file,forward-proto@file,inflight-std@file,hsts@file,compression@file,csp-unifi-portal", ] } service { name = "unifi-stun" port = "stun" tags = [ "traefik.enable=true", "traefik.consulcatalog.connect=false", "traefik.udp.routers.unifi-stun.entrypoints=unifi-stun", ] } service { name = "unifi-mongo" port = 27017 check { type = "script" command = "sh" args = ["-c", "mongo --quiet --eval 'db.runCommand(\"ping\").ok'"] interval = "30s" timeout = "5s" task = "mongo" check_restart { limit = 4 grace = "3m" } } } volume "mongo" { source = "unifi-mongo" type = "csi" access_mode = "single-node-writer" attachment_mode = "file-system" } volume "data" { source = "unifi-data" type = "csi" access_mode = "single-node-writer" attachment_mode = "file-system" } # wait for required services tp be ready before starting the main task task "wait-for" { driver = "docker" user = 1053 config { image = "danielberteaud/wait-for:24.3-1" readonly_rootfs = true pids_limit = 20 } lifecycle { hook = "prestart" } env { SERVICE_0 = "unifi-mongo.service.consul" } resources { cpu = 10 memory = 10 memory_max = 30 } } task "nginx" { driver = "docker" user = 8306 lifecycle { hook = "poststart" sidecar = "true" } config { image = "nginxinc/nginx-unprivileged:alpine" volumes = ["local/nginx.conf:/etc/nginx/conf.d/default.conf"] readonly_rootfs = true pids_limit = 20 mount { type = "tmpfs" target = "/tmp" tmpfs_options { size = 3000000 } } } template { data = <<_EOF map $http_upgrade $connection_upgrade { default upgrade; '' close; } server { listen 127.0.0.1:8888; server_name _; server_tokens off; root /usr/share/html; proxy_set_header Origin ""; proxy_set_header Authorization ""; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection $connection_upgrade; proxy_set_header Host $host; proxy_socket_keepalive on; client_max_body_size 100m; set_real_ip_from 127.0.0.1; real_ip_header X-Forwarded-For; real_ip_recursive on; # Inform endpoint location ~ ^/(inform|dl/firmware-cached).* { if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 405; } proxy_pass http://localhost:8080; } # Guest portal location /guest/ { if ($request_method !~ ^(GET|HEAD|POST)$ ) { return 405; } proxy_pass https://localhost:8843; } # Main console location / { if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE)$ ) { return 405; } proxy_pass https://localhost:8443; } } _EOF destination = "local/nginx.conf" } resources { cpu = 10 memory = 15 } } task "controller" { leader = true driver = "docker" config { image = "danielberteaud/unifi:8.1.113-1" volumes = [ "local/init-system.properties.sh:/entrypoint.d/10-init-system.properties.sh" ] readonly_rootfs = true pids_limit = 200 mount { type = "tmpfs" target = "/opt/unifi/run" tmpfs_options { size = 3000000 } } mount { type = "tmpfs" target = "/tmp" tmpfs_options { size = 3000000 } } } vault { policies = ["unifi"] env = false disable_file = true change_mode = "noop" } env { TMPDIR = "/local/tmp" } # Use a template block instead of env {} so we can fetch values from vault template { data = <<_EOT LANG=fr_FR.utf8 TZ=Europe/Paris _EOT destination = "secrets/.env" perms = 400 env = true } template { data = <<_EOF unifi.http.port=8080 unifi.https.port=8443 portal.http.port=8880 portal.https.port=8843 unifi.stun.port={{ env "NOMAD_PORT_stun" }} unifi.db.nojournal=true db.mongo.local=false db.mongo.uri=mongodb://127.0.0.1:27017/unifi? statdb.mongo.uri=mongodb://127.0.0.1:27017/unifi_stats? debug.device=info debug.mgmt=info debug.system=info debug.sdn=warn autobackup.dir=/data/backup _EOF destination = "secrets/system.properties" } template { data = <<_EOF #!/bin/sh # vim: syntax=sh set -euo pipefail mkdir -p /data/unifi mkdir -p /data/logs if [ \! -f "/opt/unifi/data/system.properties" ]; then echo "System initialization, copy the default system.properties" cp /secrets/system.properties /opt/unifi/data/system.properties else for PROP in $(grep -vE '^(\s*$|#)' /secrets/system.properties); do KEY=$(echo ${PROP} | cut -d= -f1) VALUE=$(echo ${PROP} | cut -d= -f2) if grep -q -E "^${KEY}=" /opt/unifi/data/system.properties; then if grep -q -E "^${PROP}" /opt/unifi/data/system.properties; then echo "${PROP} already set in system.properties" else echo "Updating ${PROP} in system.properties" sed -i -E "s|^${KEY}=.*|${PROP}|" /opt/unifi/data/system.properties fi else echo "Adding ${PROP} in system.properties" echo ${PROP} >> /opt/unifi/data/system.properties fi done fi _EOF destination = "local/init-system.properties.sh" perms = "755" } volume_mount { volume = "data" destination = "/data" } resources { cpu = 200 memory = 1024 } } task "mongo" { driver = "docker" lifecycle { hook = "prestart" sidecar = true } config { image = "danielberteaud/mongo:5.0.24.3-1" command = "mongod" readonly_rootfs = true pids_limit = 200 args = ["--config", "/local/mongod.conf"] mount { type = "tmpfs" target = "/tmp" tmpfs_options { size = 3000000 } } } template { data = <<_EOF net: bindIp: 127.0.0.1 storage: dbPath: /data/db directoryPerDB: true wiredTiger: engineConfig: directoryForIndexes: true journalCompressor: snappy collectionConfig: blockCompressor: snappy _EOF destination = "local/mongod.conf" } volume_mount { volume = "mongo" destination = "/data/db" } resources { cpu = 100 memory = 256 } } } }