{"id":1972,"date":"2026-03-07T20:48:18","date_gmt":"2026-03-07T18:48:18","guid":{"rendered":"https:\/\/cln.io\/blog\/?p=1972"},"modified":"2026-03-08T17:52:36","modified_gmt":"2026-03-08T15:52:36","slug":"self-hosting-wifiman-speedtest-on-synology-with-docker-compose","status":"publish","type":"post","link":"https:\/\/cln.io\/blog\/self-hosting-wifiman-speedtest-on-synology-with-docker-compose\/","title":{"rendered":"Self-Hosting WiFiMan Speedtest on Synology with Docker Compose"},"content":{"rendered":"\n<p class=\"wp-block-paragraph\">Ubiquiti&#8217;s <a href=\"https:\/\/wifiman.com\/\">WiFiMan<\/a> lets you host your own public speed test server. The official install script works great on a standard Linux box, but on a Synology NAS you need a different approach.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Here&#8217;s how I set it up with Docker Compose on my Synology DS1823xs+, both via the command line and through Synology&#8217;s Container Manager UI.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">The Problem with the Official Installer<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The WiFiMan install script (<code>curl -fsSL https:\/\/sp-dir.uwn.com\/api\/v1\/install?t=YOUR_TOKEN > \/tmp\/speed_inst.sh && sudo bash \/tmp\/speed_inst.sh<\/code>) is interactive and expects a standard Linux environment. On Synology DSM:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>The Docker socket is owned by <code>root:root<\/code> with no docker group<\/li>\n<li><code>sudo<\/code> requires special handling<\/li>\n<li>Container Manager may or may not be running<\/li>\n<li>The script uses <code>docker run<\/code> directly instead of Compose<\/li>\n<\/ul>\n\n\n\n<p class=\"wp-block-paragraph\">Instead, let&#8217;s extract what the script does and set it up properly with Docker Compose.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Requirements<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">From <a href=\"https:\/\/wifiman.com\/host\">wifiman.com\/host<\/a>, Ubiquiti lists these requirements for hosting a speed test server:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li>Stable, full duplex <strong>2+ Gbps<\/strong> internet connection for public hosting<\/li>\n<li><strong>Static public IP<\/strong> address<\/li>\n<li>Low CPU capacity required<\/li>\n<li>Recommended: Linux server or VM with <strong>Ubuntu 20.04<\/strong> and Docker<\/li>\n<\/ul>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"907\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-requirements.png\" alt=\"WiFiMan hosting requirements from wifiman.com\/host\" class=\"wp-image-1979\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-requirements.png 1200w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-requirements-300x227.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-requirements-1024x774.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-requirements-768x580.png 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">The static IP requirement is important &#8211; the API key is tied to the IP you register with, so dynamic IPs won&#8217;t work. For the Synology setup specifically, you&#8217;ll also need:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong>Container Manager<\/strong> installed from Package Center<\/li>\n<li>Port <strong>8080<\/strong> forwarded from your router to your Synology&#8217;s LAN IP<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Getting Your Installation Token<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Before you can register a speed test server, you need an installation token from Ubiquiti:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Go to <a href=\"https:\/\/console.wifiman.com\/\">console.wifiman.com<\/a> and sign in with your Ubiquiti account<\/li>\n<li>Click <strong>Add Server<\/strong> (top-right corner)<\/li>\n<\/ol>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"907\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-add-server-1.png\" alt=\"WiFiMan Console showing the Add Server button in the top-right corner\" class=\"wp-image-2002\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-add-server-1.png 1200w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-add-server-1-300x227.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-add-server-1-1024x774.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-add-server-1-768x580.png 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<ol start=\"3\" class=\"wp-block-list\">\n<li>A dialog appears with the install command. The <code>t=<\/code> parameter in the URL is your installation token \u2014 copy it<\/li>\n<\/ol>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"907\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-install-token.png\" alt=\"WiFiMan Console install dialog showing the installation token in the t= parameter\" class=\"wp-image-2000\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-install-token.png 1200w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-install-token-300x227.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-install-token-1024x774.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-console-install-token-768x580.png 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You don&#8217;t need to run their install script. We just need the token to register via the API ourselves.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">How the WiFiMan Registration Works<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The install script does three things:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li><strong>Validates<\/strong> your server IP and port with the directory server<\/li>\n<li><strong>Registers<\/strong> to get an API key<\/li>\n<li><strong>Starts<\/strong> the <code>ubnt\/speedtest<\/code> Docker container with that key<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The registration API is straightforward:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\"># Get your public IP\ncurl -s https:\/\/sp-dir.uwn.com\/api\/v1\/ip | jq -r '.ip'\n\n# Register (returns apiKey and serverId)\ncurl -s -X POST \\\n  -H \"x-auth-token: YOUR_INSTALLATION_TOKEN\" \\\n  -H \"Accept: text\/plain\" \\\n  -F \"ip=YOUR_PUBLIC_IP\" \\\n  -F \"port=8080\" \\\n  https:\/\/sp-dir.uwn.com\/api\/v1\/install<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">The Docker Compose File<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After registering and getting your API key, create <code>compose.yaml<\/code>:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"yaml\">services:\n  speedtest:\n    image: ubnt\/speedtest\n    container_name: ubnt-speedtest\n    network_mode: host\n    environment:\n      - API_KEY=YOUR_API_KEY_HERE\n      - PORT=8080\n      - DIRECTORY_SERVER_URL=wss:\/\/sp-dir.uwn.com\/connect\n    labels:\n      - \"com.centurylinklabs.watchtower.enable=true\"\n    restart: unless-stopped\n\n  watchtower:\n    image: v2tec\/watchtower\n    container_name: ubnt-speedtest-watchtower\n    volumes:\n      - \/var\/run\/docker.sock:\/var\/run\/docker.sock\n    command: --label-enable --cleanup\n    restart: unless-stopped<\/pre>\n\n\n\n<p class=\"wp-block-paragraph\">Key details:<\/p>\n\n\n\n<ul class=\"wp-block-list\">\n<li><strong><code>network_mode: host<\/code><\/strong> &#8211; The official script uses host networking with a <code>PORT<\/code> override. The container&#8217;s internal default port is 8050, but we set it to 8080.<\/li>\n<li><strong>Watchtower<\/strong> &#8211; Auto-updates the speedtest container when Ubiquiti releases new versions. The <code>--label-enable<\/code> flag means it only watches containers with the watchtower label.<\/li>\n<\/ul>\n\n\n\n<h2 class=\"wp-block-heading\">Option 1: Deploy via Container Manager UI<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you prefer using Synology&#8217;s web UI instead of SSH:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Open <strong>Container Manager<\/strong> from the DSM desktop<\/li>\n<li>Go to <strong>Project<\/strong> in the left sidebar<\/li>\n<li>Click <strong>Create<\/strong><\/li>\n<li>Set the <strong>Project name<\/strong> to <code>wifiman<\/code><\/li>\n<li>Set the <strong>Path<\/strong> to <code>\/volume2\/docker\/compose\/wifiman<\/code> (or wherever you keep your compose projects)<\/li>\n<li>Paste the <code>compose.yaml<\/code> contents above into the YAML editor, replacing <code>YOUR_API_KEY_HERE<\/code> with your actual API key<\/li>\n<li>Click <strong>Next<\/strong>, review the summary, then click <strong>Done<\/strong><\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">Container Manager will pull the images and start the containers. You can monitor the project from the <strong>Project<\/strong> tab:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"907\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-synology-containers.png\" alt=\"Synology Container Manager showing the ubnt-speedtest container running\" class=\"wp-image-1974\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-synology-containers.png 1200w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-synology-containers-300x227.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-synology-containers-1024x774.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-synology-containers-768x580.png 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">If you ever need to update the API key or change settings, go to the project, click <strong>Action &rarr; Stop<\/strong>, edit the YAML under the <strong>YAML Configurations<\/strong> tab, then <strong>Action &rarr; Build<\/strong> to rebuild and start the containers with the new configuration.<\/p>\n\n\n\n<h2 class=\"wp-block-heading\">Option 2: Deploy via Command Line<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">If you prefer SSH, create the compose file and start it directly:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\"># Create the project directory\nmkdir -p \/volume2\/docker\/compose\/wifiman\n\n# Create compose.yaml (paste the YAML above)\nvi \/volume2\/docker\/compose\/wifiman\/compose.yaml\n\n# Start it\nsudo docker compose -f \/volume2\/docker\/compose\/wifiman\/compose.yaml up -d<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Port Forwarding<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The speedtest server needs to be publicly accessible. On my UniFi gateway (UCG Fiber), I added a port forwarding rule: WAN port 8080 (TCP\/UDP) forwarding to the Synology&#8217;s LAN IP on port 8080.<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1280\" height=\"720\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-unifi-port-forward.png\" alt=\"UniFi Network port forwarding rule for WiFiMan speedtest: WAN port 8080 TCP\/UDP forwarding to Synology LAN IP port 8080\" class=\"wp-image-2011\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-unifi-port-forward.png 1280w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-unifi-port-forward-300x169.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-unifi-port-forward-1024x576.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-unifi-port-forward-768x432.png 768w\" sizes=\"auto, (max-width: 1280px) 100vw, 1280px\" \/><\/figure>\n\n\n\n<p class=\"wp-block-paragraph\">You can verify it&#8217;s working by hitting the diagnostics endpoint:<\/p>\n\n\n\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"shell\">curl http:\/\/YOUR_PUBLIC_IP:8080\/diagnostics\n# {\"connected\":true,\"error\":null,\"version\":\"1.4.2\",...}<\/pre>\n\n\n\n<h2 class=\"wp-block-heading\">Monitoring Results<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">Once your server is live, you can monitor all speed test results from <a href=\"https:\/\/console.wifiman.com\/\">console.wifiman.com<\/a>. The console shows each test with upload\/download speeds, latency, client IP, and origin (Mobile, Web, Firmware, etc.). You can also switch to a map view to see where your users are testing from:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1200\" height=\"907\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-console-map.png\" alt=\"WiFiMan Console map view showing speed test results clustered around Belgium\" class=\"wp-image-1989\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-console-map.png 1200w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-console-map-300x227.png 300w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-console-map-1024x774.png 1024w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-console-map-768x580.png 768w\" sizes=\"auto, (max-width: 1200px) 100vw, 1200px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Approval Process<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">After your server is running and connected, it won&#8217;t immediately appear in the WiFiMan app or web speed test. There&#8217;s an approval process on Ubiquiti&#8217;s side before your server gets listed publicly. This can take a few days.<\/p>\n\n\n\n<p class=\"wp-block-paragraph\">Once approved, your server will show up in the speed test server list for nearby users. Here&#8217;s what it looks like in the WiFiMan iOS app:<\/p>\n\n\n\n<figure class=\"wp-block-image size-large\"><img loading=\"lazy\" decoding=\"async\" width=\"1178\" height=\"2560\" src=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-app-server-list-scaled.png\" alt=\"WiFiMan app speed test server list showing CLN.io in Gent, Belgium\" class=\"wp-image-1991\" srcset=\"https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-app-server-list-scaled.png 1178w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-app-server-list-138x300.png 138w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-app-server-list-471x1024.png 471w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-app-server-list-768x1669.png 768w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-app-server-list-707x1536.png 707w, https:\/\/cln.io\/blog\/wp-content\/uploads\/2026\/03\/screenshot-wifiman-app-server-list-943x2048.png 943w\" sizes=\"auto, (max-width: 1178px) 100vw, 1178px\" \/><\/figure>\n\n\n\n<h2 class=\"wp-block-heading\">Summary<\/h2>\n\n\n\n<p class=\"wp-block-paragraph\">The complete setup involves:<\/p>\n\n\n\n<ol class=\"wp-block-list\">\n<li>Get an installation token from <a href=\"https:\/\/console.wifiman.com\/\">console.wifiman.com<\/a><\/li>\n<li>Register your server&#8217;s static public IP to get an API key<\/li>\n<li>Create a <code>compose.yaml<\/code> with the speedtest container (host networking, port 8080) and watchtower<\/li>\n<li>Deploy it via Container Manager UI or <code>docker compose up -d<\/code><\/li>\n<li>Set up port forwarding on your router (external 8080 &rarr; Synology:8080)<\/li>\n<\/ol>\n\n\n\n<p class=\"wp-block-paragraph\">The whole thing runs headlessly on the Synology. Watchtower keeps the speedtest image updated automatically. No manual intervention needed.<\/p>\n\n\n\n<hr class=\"wp-block-separator has-alpha-channel-opacity\"\/>\n\n\n\n<p class=\"wp-elements-e310da23723257d0d909d6fce89b9a26 wp-block-paragraph\" style=\"color:#9ca3af;font-size:14px\">This post was written with the help of Claude (Opus 4), Anthropic&#8217;s AI assistant.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Ubiquiti&#8217;s WiFiMan lets you host your own public speed test server. The official install script works great on a standard Linux box, but on a Synology NAS you need a different approach. Here&#8217;s how I [&hellip;]<\/p>\n","protected":false},"author":1,"featured_media":2031,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[26,37],"tags":[],"class_list":["post-1972","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-it","category-networking"],"_links":{"self":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1972","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/comments?post=1972"}],"version-history":[{"count":16,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1972\/revisions"}],"predecessor-version":[{"id":2012,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/posts\/1972\/revisions\/2012"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media\/2031"}],"wp:attachment":[{"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/media?parent=1972"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/categories?post=1972"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/cln.io\/blog\/wp-json\/wp\/v2\/tags?post=1972"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}