Selfstack is a free and open-source, self-hosted dashboard builder for organizing your bookmarks, services, and homelab tools into customizable boards with categories, groups, and tiles — a lightweight alternative to heavier homelab dashboards, designed to be deployed in a single container.
Live demo → — sign in with demo@selfstack.local / demo1234 (resets hourly).
- Multiple boards per user with drag-and-drop reordering (categories, groups, tiles, and boards themselves)
- Two layout modes per board: auto-flow grid and free placement
- Fixed tile sizes (small / default / large / list) that reflow responsively
- Per-tile, per-group, per-category background and border colors, with "border matches background" shortcut
- Icon picker with full Lucide library or upload your own PNG/SVG icons
- One-click reset all colors per board
- Status ping indicator per tile (HEAD → GET fallback, tolerates self-signed TLS for LAN services)
- Public boards reachable via
/board/<username>/<slug>(viewer-only) - Board members with per-board roles: owner / editor / viewer
- Organizations to group users and share boards within a team (owner / admin / editor / member roles)
- Global roles:
user,editor(edit any board),admin(full access) - Copyable share link with app-URL awareness (works behind reverse proxies)
- Session-based auth (JWT + bcrypt)
- Two-factor authentication (TOTP) — QR enrollment in settings, verification step on login
- Password reset / "must change password on next login" flow
- Registration can be toggled by admins
- Required unique username, used as URL prefix for the user's boards (
/board/maxmuster/dashboard) - Rename-your-username safely: owned board slugs are cascade-renamed in a transaction
- Light / dark / system mode
- Multiple built-in color presets (Sunset Horizon, Ocean, Forest, …)
- Per-variable override (primary, background, accent, …) with a Figma-style color picker including shadcn/tailwind swatches
- Preferences stored per user, applied SSR-side to avoid flash
- German and English UI, per-user locale
- User CRUD with one-time password generation and welcome email
- Organization management (create, edit, add/remove members, change roles)
- System health page (DB, uploads, SMTP reachability, memory, uptime)
- System settings (registration toggle, public app URL, legal texts)
- SMTP configuration UI with test-email send
- Role management
- Live instance: https://selfstack-demo.larsbeck.dev/
- Single-flag deployment (
NEXT_PUBLIC_DEMO_MODE=true) - Database is wiped + re-seeded with demo content on boot and every
DEMO_RESET_MINUTES(default 60) - All settings mutations are blocked server-side
- Bottom banner announces demo status, localized and reacts to the language toggle
- Sends
noindex, nofollowheaders and/robots.txtwithDisallow: /by default - Opt in to crawlers with
ALLOW_SEARCH_INDEXING="true"
- SQLite database (better-sqlite3 adapter), no external services required
- Runs in a single container
- Next.js 16 (App Router, Turbopack, standalone output)
- React 19
- Prisma 6 + SQLite (via
@prisma/adapter-better-sqlite3) - shadcn/ui + Tailwind CSS 4
- dnd-kit for drag-and-drop
- Lucide icons
- otpauth + qrcode for 2FA
- nodemailer for transactional email
- jose for JWT, bcryptjs for password hashing
The whole app runs in a single container. SQLite database and uploaded icons live in Docker volumes, so your data survives rebuilds and updates.
git clone https://github.com/larsbeckdev/selfstack.git
cd selfstackThe JWT_SECRET signs session tokens. Generate a random one:
export JWT_SECRET=$(node -e "console.log(require('crypto').randomBytes(32).toString('hex'))")No Node.js on the host? Use OpenSSL instead:
export JWT_SECRET=$(openssl rand -hex 32)Prefer a .env file next to docker-compose.yml (Compose reads it
automatically) so the secret survives a new shell:
JWT_SECRET=your-generated-secret-here
APP_URL=https://dash.example.comdocker compose up -d --buildOn every start the container runs prisma db push (idempotent schema
sync) and, on first boot only, seeds an admin user — no manual migration
step. The entrypoint also repairs volume ownership on boot, so
bind-mounts work out of the box.
Open http://localhost:3026 and sign in with the seeded admin:
| Password | |
|---|---|
admin@selfstack.local |
admin123 |
Change the admin password immediately after first login.
Once you have built the selfstack:latest image (step 3 above builds and
tags it), you can run it anywhere without the source tree using a minimal
docker-compose.yml:
services:
selfstack:
image: selfstack:latest
container_name: selfstack
restart: unless-stopped
ports:
- "3026:3026"
environment:
DATABASE_URL: "file:/data/selfstack.db"
JWT_SECRET: "${JWT_SECRET}"
APP_URL: "${APP_URL:-http://localhost:3026}"
# SECURE_COOKIES: "true" # enable behind HTTPS
volumes:
- selfstack-data:/data
- selfstack-uploads:/app/public/uploads
volumes:
selfstack-data:
selfstack-uploads:Swap build: . for image: selfstack:latest to skip rebuilding.
Pull the new code, rebuild, and restart. Your data stays in the volumes — the schema is re-synced automatically on boot.
cd selfstack
git pull
docker compose up -d --buildprisma db push runs on every start, so schema changes from an update
apply without a separate migration command. To reclaim disk from old
image layers afterwards:
docker image prune -fBoth volumes are worth backing up. Copy them to a tarball:
docker run --rm \
-v selfstack-data:/data \
-v selfstack-uploads:/uploads \
-v "$(pwd):/backup" \
busybox tar czf /backup/selfstack-backup.tar.gz /data /uploadsRestore by extracting the tarball back into fresh volumes with the same
docker run ... tar xzf pattern.
| Variable | Default | Description |
|---|---|---|
DATABASE_URL |
file:/data/selfstack.db |
Prisma connection string. Use a path on the mounted /data volume. |
JWT_SECRET |
— | Required. Secret used to sign session tokens. |
SECURE_COOKIES |
(unset) | Set to "true" when serving over HTTPS. |
PORT |
3026 |
HTTP port inside the container. |
APP_URL |
(unset) | Public base URL (e.g. https://dash.example.com). Used for share links and email links. Can also be set in the admin UI. |
DISABLE_REGISTRATION |
(unset) | Set to "true" to hard-disable self-registration regardless of the admin toggle. |
SMTP_HOST / SMTP_PORT / SMTP_USER / SMTP_PASS / SMTP_FROM / SMTP_SECURE |
(unset) | Fallback SMTP config. Admin UI values override env. |
ALLOW_SEARCH_INDEXING |
false |
Set to "true" to drop the noindex meta tag and allow /robots.txt crawling. |
NEXT_PUBLIC_DEMO_MODE |
false |
Set to "true" to enable demo mode (auto-reset + mutation lock). |
DEMO_RESET_MINUTES |
60 |
How often the demo database is wiped + re-seeded. |
- Node.js 20+
- npm
git clone https://github.com/larsbeckdev/selfstack.git
cd selfstack
cp .env.example .env
npm installnpx prisma generate
npx prisma db push
npx tsx prisma/seed.tsnpm run devOpen http://localhost:3025.
npm run build
npm startRuns on port 3026.
/dashboard— user overview of all accessible boards/board— list of public boards/board/<username>/<slug>— a user's board (e.g./board/maxmuster/dashboard)/board/<orgslug>/<slug>— an organization-owned board (e.g./board/acme/team)/board/<slug>— admin-created system boards (no prefix)/settings·/settings/appearance·/settings/account·/settings/boards/admin/users·/admin/organizations·/admin/settings·/admin/health(admin only)
Public boards are reachable at the same path without authentication.
Note: Board slugs cannot start with
@— segments beginning with@are reserved by Next.js App Router for parallel route slots.
src/
├── app/
│ ├── (app)/ # Authenticated app routes
│ │ ├── board/[...slug] # Board view (supports username/slug paths)
│ │ ├── dashboard/ # Dashboard overview
│ │ ├── settings/ # User settings (general, appearance, account, boards)
│ │ ├── media/ # Uploaded icon management
│ │ └── admin/ # Admin panel (users, organizations, settings, health)
│ ├── (auth)/ # Login (with 2FA step) & register
│ ├── api/
│ │ ├── tile-status/ # Tile status ping endpoint
│ │ ├── media/ # Icon uploads
│ │ └── upload/ # Generic upload endpoint
│ └── change-password/ # Forced password change flow
├── components/
│ ├── dashboard/ # Board, category, group, tile components
│ ├── layout/ # Sidebar, header, layout-mode toggle
│ ├── settings/ # Settings pages & two-factor card
│ ├── admin/ # Admin-only components (user table, SMTP card)
│ ├── auth/ # Login / register / 2FA / change-password forms
│ ├── media/ # Media library
│ └── ui/ # shadcn/ui components + color picker
├── lib/
│ ├── actions/ # Server actions (board, auth, settings)
│ ├── auth.ts # Session & 2FA-pending cookie management
│ ├── totp.ts # TOTP secret/QR/verify helpers
│ ├── email.ts # SMTP transport (DB-backed with env fallback)
│ ├── db.ts # Prisma client
│ ├── theme-presets.ts # Built-in theme presets
│ ├── shadcn-palette.ts # shadcn color swatches
│ └── i18n/ # Translations (de, en)
└── generated/prisma/ # Generated Prisma client
Issues and pull requests are welcome. If you are planning a larger change, please open an issue first so we can discuss the direction.
Selfstack is released under the MIT License.