
Your VPS runs Portainer, n8n and a few more tools, and their ports live in a note somewhere. A start page such as Homepage, Homarr or Dashy puts them on one tab, with live status from each app. It is also a map of everything you run, and with widgets it holds API keys and often a line into Docker. Homepage's documentation warns that with widgets it "can access personal information". So build it as a private page from the first command.
Quick answer: Pick Homepage if you like YAML files, Homarr for drag-and-drop boards with user accounts, Dashy for one small config file. For all three, publish the port on 127.0.0.1 only and open it through an SSH tunnel. Give it Docker access through a read-only socket proxy, not the raw /var/run/docker.sock. Keep API keys where only the server reads them: a Docker secret for Homepage, Homarr's encrypted database, DASHY_ variables with useProxy: true for Dashy. Use a domain only with a login in front.
Homepage, Homarr and Dashy Compared
At the time of writing, the latest releases were Homepage 2.7.0 and Dashy 4.9.0 (both 23 October 2026) and Homarr 2.10.1 (30 October 2026). Each runs as one Docker container:
| Homepage | Homarr | Dashy | |
|---|---|---|---|
| You configure it in | YAML files, Docker labels | The browser, drag and drop | conf.yml or its in-page editor |
| Login | Optional password or OIDC, since 2.0 | Built-in users, LDAP, OIDC | Built-in users, header auth, OIDC |
| Widget API calls come from | The Homepage server | The Homarr server | Your browser, unless the widget uses its proxy |
| API keys live in | Config files, or files via HOMEPAGE_FILE_ | Its database, encrypted | conf.yml, or DASHY_ variables |
| Container port | 3000 | 7575 | 8080 |
| Memory, per its docs | No figure given | 500 MB | About 80 to 120 MB idle |
This guide builds Homepage, whose widgets call APIs from the server and read keys from files; Homarr and Dashy follow after Step 4.
Step 1: Run Homepage on 127.0.0.1
Install Docker Engine from Docker's repository (Ubuntu, Debian). The commands here work the same on Ubuntu 24.04 and Debian 12, and a Space-Node VPS gives you KVM with full root access, so nothing here is blocked. Create a folder, plus a shared network for the apps the dashboard will query:
sudo mkdir -p /srv/apps/homepage/config
sudo docker network create dash
Save this as /srv/apps/homepage/compose.yaml:
services:
dockerproxy:
image: ghcr.io/tecnativa/docker-socket-proxy:latest
container_name: dockerproxy
environment:
- CONTAINERS=1
- POST=0
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
restart: unless-stopped
homepage:
image: ghcr.io/gethomepage/homepage:latest
container_name: homepage
ports:
- "127.0.0.1:3000:3000"
volumes:
- ./config:/app/config
networks:
- default
- dash
restart: unless-stopped
networks:
dash:
external: true
Start it from that folder:
cd /srv/apps/homepage
sudo docker compose up -d
The 127.0.0.1: prefix keeps Homepage off the internet. Docker diverts traffic to published ports "before it goes through the ufw firewall settings", so UFW can't close them, but with the loopback address "only the Docker host can access" the port (port publishing; see our Docker and UFW guide). The proxy publishes no port: Homepage reaches it by name on the project's own network, where nothing else runs, as the proxy's README asks. CONTAINERS=1 opens the API section that container status needs, and with POST=0 "only GET and HEAD operations are allowed".
Step 2: Open It Through an SSH Tunnel
On your computer:
ssh -N -L 3000:127.0.0.1:3000 user@203.0.113.10
Browse to http://localhost:3000. -L forwards your local port 3000 over SSH to 127.0.0.1:3000 on the server, and -N runs no remote command (ssh manual). Keep the local port at 3000: since version 1.0, Homepage only answers for hosts in HOMEPAGE_ALLOWED_HOSTS, where localhost:3000 and 127.0.0.1:3000 are always included (installation docs). For several dashboards in one tunnel, or WireGuard and Tailscale for a team, see our dashboard access guide.
Step 3: Add Services, Bookmarks and Container Status
When a config file is missing, Homepage copies in a sample, so ./config fills with services.yaml, bookmarks.yaml, docker.yaml and more. Point docker.yaml at the proxy:
my-docker:
host: dockerproxy
port: 2375
Then replace services.yaml:
- Admin:
- Portainer:
icon: portainer.png
href: https://localhost:9443
description: Docker management
server: my-docker
container: portainer
- Automation:
- n8n:
icon: n8n.png
href: http://localhost:5678
description: Workflows
server: my-docker
container: n8n
href is the link your browser opens, so through the tunnel it's a port you forward as well, such as -L 9443:127.0.0.1:9443. server and container add the container's state, read through the proxy, and a click on it shows CPU, memory and network use. container must match a name in the NAMES column of sudo docker ps. Icons load by name from the Dashboard Icons set, and bookmarks.yaml takes plain links in the same group format.
Containers can also list themselves with homepage.* labels, such as homepage.group=Automation (Docker integration). After an edit, the refresh icon at the bottom right clears Homepage's cached data and reloads the page.
Step 4: Add Widgets Without Leaking API Keys
A service widget shows live data from an app's API, such as running and stopped containers from Portainer. Homepage calls those APIs from its server and sends the browser only a whitelist of display fields, so key and url stay on the server (service-helpers.js). That protects the browser, not your config files: a key in services.yaml travels with every backup, Git commit or forum post of that file. Keep it in a root-only file.
In Portainer, click your user name, open My account and add a token under Access tokens. It's shown once and gives "the same level of access to Portainer functionality" as your account (API access), so treat it like your password:
sudo mkdir -m 700 /srv/apps/homepage/secrets
read -rs KEY && printf '%s' "$KEY" | sudo tee /srv/apps/homepage/secrets/portainer_key > /dev/null; unset KEY
sudo chmod 600 /srv/apps/homepage/secrets/portainer_key
Paste the token after the second line and press Enter. read -s keeps it off the screen and out of your shell history, and printf '%s' writes no trailing newline, which matters because Homepage pastes a file's contents into the YAML exactly as they are (config.js).
Add the secret to the homepage service, below container_name:
environment:
HOMEPAGE_FILE_PORTAINER_KEY: /run/secrets/portainer_key
secrets:
- portainer_key
And at the end of compose.yaml:
secrets:
portainer_key:
file: ./secrets/portainer_key
Compose mounts the file read-only at /run/secrets/portainer_key (secrets in Compose). Docker prefers this to environment variables, with which "you risk unintentional information exposure". Homepage replaces {{HOMEPAGE_FILE_PORTAINER_KEY}} in any config file with the file's contents (Docker installation).
Inside Homepage's container, 127.0.0.1 is Homepage itself, so the widget reaches Portainer by name on the shared network, where containers "can communicate using only another container's IP address or name" (docker network connect):
sudo docker network connect dash portainer
cd /srv/apps/homepage && sudo docker compose up -d
For an app that runs from its own compose file, add dash there as an external network. Then add the widget under Portainer in services.yaml:
widget:
type: portainer
url: https://portainer:9443
env: 1
key: "{{HOMEPAGE_FILE_PORTAINER_KEY}}"
env is the environment ID at the end of the URL when you open the environment under Environments, such as #!/endpoints/1 (Portainer widget). Homepage doesn't verify certificates on these calls (http.js), so Portainer's self-signed one works. Some widgets need no key at all: the Uptime Kuma widget takes only its URL and a status page slug.
Homarr and Dashy: What Changes
Homarr. This is the compose file from its Docker install page, with the port on 127.0.0.1 and the Docker socket line removed:
services:
homarr:
container_name: homarr
image: ghcr.io/homarr-labs/homarr:latest
restart: unless-stopped
volumes:
- ./homarr/appdata:/appdata
environment:
- SECRET_ENCRYPTION_KEY=paste-the-output-of-openssl-rand-hex-32
ports:
- "127.0.0.1:7575:7575"
Generate the key with openssl rand -hex 32. It encrypts the secrets in Homarr's database and the container exits without it (environment variables), so back it up with ./homarr/appdata. Until onboarding is done, every visitor lands on it at /init, and it writes the administrator to the database (after the installation), so finish it through a tunnel right away. Integration credentials "are encrypted and used only by the server" (integrations), and "Public boards are readable without signing in", so make a board public only on purpose.
Homarr's Docker page says the socket "grants extensive host control" and to prefer "a least-privilege socket proxy when lifecycle access is not required". Add the same dockerproxy service to Homarr's compose file and set DOCKER_HOSTNAMES=dockerproxy and DOCKER_PORTS=2375. Start, stop and restart actions need POST=1.
Dashy. Everything in its user-data folder is served from the web root, conf.yml included (quick start). Without ENABLE_HTTP_AUTH=true, its login page is "a UI overlay" and "Anyone can fetch /conf.yml, /system-info, /cors-proxy, etc. directly" (built-in auth). So publish it as "127.0.0.1:8080:8080", and set ENABLE_HTTP_AUTH=true if you add users. Widgets call APIs from your browser unless they use Dashy's proxy, and "widget options are sent to the browser". For a key, give the name of a DASHY_ environment variable and turn the proxy on:
- type: weather
useProxy: true
options:
apiKey: DASHY_WEATHER_TOKEN
city: London
Then set DASHY_WEATHER_TOKEN in the container's environment. Dashy's server swaps in the value when it forwards the request; without useProxy, the placeholder reaches the API unchanged and the call fails (widgets).
If You Want a Domain
On a domain anyone can load the page, so put a login in front of it. With Caddy as the reverse proxy (set up as in several apps on one VPS), run caddy hash-password, type a password, and add a site block to /etc/caddy/Caddyfile:
dash.example.com {
basicauth {
admin <hash from caddy hash-password>
}
reverse_proxy 127.0.0.1:3000
}
Reload with sudo systemctl reload caddy. Caddy 2.8 renamed the directive to basic_auth (docs); basicauth still works, and it's the only name the Caddy 2.6.2 package in Ubuntu 24.04 and Debian 12 knows. Caddy passes the Host header through unchanged, so add HOMEPAGE_ALLOWED_HOSTS: dash.example.com to Homepage's environment and run sudo docker compose up -d.
Homepage 2.0 added its own password gate (HOMEPAGE_AUTH_ENABLED=true plus HOMEPAGE_AUTH_SECRET, HOMEPAGE_EXTERNAL_URL and HOMEPAGE_AUTH_PASSWORD), but its docs warn it has no rate limiting on password attempts, so use it only as a second lock. With Cloudflare Tunnel, Cloudflare Access can do the login (tunnel or reverse proxy).
Which One Fits
- Homepage if you're happy editing YAML over SSH and want keys in root-only files, so the config itself can go into Git without them.
- Homarr if several people use the dashboard or you'd rather build it in the browser. Its docs ask for 500 MB of RAM, and with the socket its admins control Docker.
- Dashy for a small footprint and one file to back up, as long as every widget key goes through a
DASHY_variable.
If It Still Fails
The page only says "Host validation failed. See logs for more details." sudo docker logs homepage names the rejected host, such as localhost:13000. Tunnel to local port 3000, or add that exact value to HOMEPAGE_ALLOWED_HOSTS (comma separated, no spaces) and run sudo docker compose up -d.
A widget shows an API error. Click API Error Information in the widget. Often the url points at 127.0.0.1 or localhost, which inside the container is Homepage itself, or ends with a slash or an API path, which Homepage's troubleshooting page rules out. Test the name from inside with sudo docker exec homepage ping portainer (Ctrl+C stops it); if it doesn't resolve, connect the app to the dash network.
No container status. server: must match the name in docker.yaml (my-docker), and container: a name from sudo docker ps. The proxy answers blocked calls with "HTTP 403 Forbidden"; status needs CONTAINERS=1.
Homarr shows the onboarding again after a restart. Check that /appdata is mounted, writable and persisted, as its docs advise.
FAQ
Can I leave Homepage public without a login? No. Its docs say public deployments "should be secured via a reverse proxy, VPN, or similar", and the page lists every service and link you configured. Keep each app's own login too: the dashboard's login only guards the dashboard.
Why a socket proxy instead of mounting /var/run/docker.sock in Homepage?
Homepage's docs call the direct socket "not the recommended method of integration", and the proxy's README warns that "Giving access to your Docker socket could mean giving root access to your host". Through the proxy with POST=0, Homepage reads container status, and no request through it can start, stop or create a container. Docker's security page is short on this: "only trusted users should be allowed to control your Docker daemon."
How much memory does a dashboard use?
Dashy's docs say its server idles around 80 to 120 MB and 256 MB is comfortable; Homarr's ask for 500 MB; Homepage's give no figure. sudo docker stats --no-stream prints each container's memory use once.
How do I update Homepage?
From /srv/apps/homepage, run sudo docker compose pull, then sudo docker compose up -d, which recreates the container on the new image. Your settings stay in ./config and ./secrets; to keep copies off the server, see our Docker volumes backup guide.
Sources
All opened on 31 October 2026:
- Homepage: Installation, Docker installation, Services, Docker, Bookmarks, Settings, Portainer widget, Uptime Kuma widget, Troubleshooting, README, releases, source: config.js, auth.js, service-helpers.js, http.js
- Homarr: Requirements, Docker installation, After the installation, Environment variables, Single sign-on, Integrations, Docker integration, Boards, releases
- Dashy: Quick start, Deployment, Docker deployment, Authentication, Built-in auth, Widgets, releases
- Docker: Port publishing, Packet filtering and firewalls, Security, Secrets in Compose, Set environment variables, Compose services, Compose networks, Networking in Compose, Compose application model, docker compose up, docker compose pull, docker network create, docker network connect, docker container ls, docker container stats, Install on Ubuntu, Install on Debian
- Tecnativa docker-socket-proxy
- Portainer: API access
- Caddy: basic_auth, command line, reverse_proxy, Keep Caddy running, caddyauth source, packages for Ubuntu 24.04 and Debian 12
- Man pages: ssh(1), bash(1), tee(1), mkdir(1), chmod(1)