A self-hosted homelab dashboard built with Next.js. Monitor your services, manage widgets, and keep an eye on your infrastructure from one place.
- Drag-and-drop widget grid
- Widgets for Docker, Proxmox, TrueNAS, Unraid, Plex, Jellyfin, Sonarr, Radarr, Pi-hole, AdGuard, Uptime Kuma, and more
- Encrypted secret storage for API keys
- Import/export configuration
- Dark mode
- Docker with Compose v2 (
docker compose) - The machine running the container must have Docker installed (for the Docker widget)
mkdir homelabarr && cd homelabarr
# Create a docker-compose.yml (or copy the one below)
cat <<'EOF' > docker-compose.yml
services:
homelabarr:
image: ghcr.io/dpawson905/homelabarr:latest
container_name: homelabarr
restart: unless-stopped
ports:
- "3575:3575"
volumes:
- ./data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- NODE_ENV=production
- PORT=3575
# - ENCRYPTION_SECRET=change-me-to-a-long-random-string
EOF
docker compose up -dOpen http://your-server-ip:3575 in your browser.
Data is stored in ./data/ on the host — it persists across container restarts and upgrades.
git clone https://github.com/dpawson905/homelabarr.git
cd homelabarr
docker compose up -d- In Portainer, go to Stacks → Add stack
- Choose Repository and point it at your repo, or paste a
docker-compose.ymldirectly - To use the pre-built image, replace
build: .withimage: ghcr.io/dpawson905/homelabarr:latest - Deploy the stack
- Access the dashboard at http://your-server-ip:3575
Pre-built image (recommended):
services:
homelabarr:
image: ghcr.io/dpawson905/homelabarr:latest
container_name: homelabarr
restart: unless-stopped
ports:
- "3575:3575"
volumes:
- ./data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- NODE_ENV=production
- PORT=3575
# Uncomment and set to persist encryption key across rebuilds:
# - ENCRYPTION_SECRET=change-me-to-a-long-random-stringBuild from source:
services:
homelabarr:
build: .
container_name: homelabarr
restart: unless-stopped
ports:
- "3575:3575"
volumes:
- ./data:/app/data
- /var/run/docker.sock:/var/run/docker.sock
environment:
- NODE_ENV=production
- PORT=3575
# Uncomment and set to persist encryption key across rebuilds:
# - ENCRYPTION_SECRET=change-me-to-a-long-random-string| Variable | Default | Description |
|---|---|---|
PORT |
3575 |
Port the app listens on |
NODE_ENV |
production |
Node environment |
ENCRYPTION_SECRET |
(auto-generated) | Key used to encrypt stored API secrets. If not set, a key is auto-generated and saved to ./data/.encryption-key. Set this explicitly if you want to retain your secrets after wiping the data directory. |
| Host path | Container path | Purpose |
|---|---|---|
./data |
/app/data |
SQLite database + encryption key |
/var/run/docker.sock |
/var/run/docker.sock |
Docker widget host access |
Pre-built image:
docker compose pull
docker compose up -dBuild from source:
git pull
docker compose build
docker compose up -dMigrations run automatically on startup — no manual steps needed.
- Node.js 20+
- npm
# 1. Clone the repo
git clone https://github.com/your-username/homelabarr.git
cd homelabarr
# 2. Install dependencies
npm install
# 3. Initialize the database (creates ./data/homelabarr.db, runs migrations, seeds a default board)
npm run db:setup
# 4. Start the dev server
npm run devOpen http://localhost:3000.
No
.envfile needed. There are no required environment variables for local development. The SQLite database and encryption key are auto-created in./data/on first run.
app/ Next.js App Router pages and API routes
api/widgets/ One folder per widget type (server-side data fetching)
board/[id]/ Board page and layout
settings/ Settings page
components/
widgets/ React widget components
ui/ Shared UI primitives (shadcn)
lib/
db/ Drizzle ORM schema, queries, and DB connection
crypto/ AES-256-GCM secret encryption
services/ HTTP client for homelab services (TLS-tolerant)
drizzle/ SQL migration files
| Command | Description |
|---|---|
npm run db:setup |
Generate + migrate + seed (run once after cloning) |
npm run db:generate |
Generate a new migration file after editing lib/db/schema.ts |
npm run db:migrate |
Apply pending migrations to the local DB |
npm run db:seed |
Seed a default board (no-op if one already exists) |
npm run db:studio |
Open Drizzle Studio in the browser to inspect the DB |
- Edit
lib/db/schema.ts - Run
npm run db:generateto create a new migration file indrizzle/ - Run
npm run db:migrateto apply it locally - Commit both the schema change and the generated migration file
Each widget is made up of:
| File | Purpose |
|---|---|
components/widgets/{type}-widget.tsx |
React component |
app/api/widgets/{type}/route.ts |
Server-side API route |
app/api/widgets/{type}/types.ts |
Response type interfaces |
After creating those files, register the widget in:
components/widget-renderer.tsx— add acasefor the new typecomponents/add-widget-dialog.tsx— add toWIDGET_CATEGORIESandWIDGET_DEFAULT_SIZES
Use an existing widget (e.g. dns, uptime-kuma) as a reference for the pattern.
Homelabarr encrypts all API keys at rest. To configure a widget that needs an API key:
- Go to Settings → Secrets
- Create a new secret with a name (e.g.
PLEX_TOKEN) and paste your API key as the value - In the widget's settings, enter the secret name (not the key itself) in the "Secret Name" field
Docker widget shows "Cannot connect to Docker daemon"
Make sure /var/run/docker.sock is mounted. If you're using Portainer, add the volume in the stack editor.
Secrets stop working after wiping ./data/
Set ENCRYPTION_SECRET to a fixed value in docker-compose.yml. Without it, a new key is auto-generated, making previously encrypted secrets unreadable.
Widget shows an error after entering a secret name Check that the secret name in the widget settings matches exactly what you named it in Settings → Secrets (case-sensitive).