One private workspace for your files, notes, links, feeds, and secrets. Self-hosted, owned by you.
Silo is a self-hosted workspace that replaces a stack of separate cloud services with one app on your own server. Files, notes, bookmarks, an RSS reader, an encrypted secrets vault, and a photo gallery all live behind a single login, in your database, on your storage. Nothing leaves your infrastructure.
The surfaces are not bolted together. They share one identity, one permission model, one global search, and one automation engine. Save a file and it is versioned. Star an article and it appears next to your starred files. Bookmark a page and Silo can detect its RSS feed and offer to subscribe. Grant a teammate access to a folder and every file inside inherits it. You run it on Laravel 13 and Vue 3 with MySQL, MariaDB, Postgres, or SQLite and a local or S3 disk, plus one queue worker for background processing.
Silo is built for real use, not a demo. Every action is permission-checked and sensitive ones are audit-logged. Uploads are processed in the background for thumbnails and metadata, and optionally virus-scanned before they are served. File bodies are served safely (attachment plus nosniff plus a strict Content-Security-Policy), feed and article HTML is sanitized before render, every server-side URL fetch is SSRF-guarded, and secrets are encrypted at rest with authenticated AES-256-GCM. Saves are versioned and deletes are recoverable from Trash.
Teams, self-hosters, agencies, privacy-sensitive organizations, and homelabs that want an all-in-one workspace on their own hardware instead of renting five SaaS subscriptions and trusting each one with their data.
Logging in lands on a home screen built to get you back to work in seconds: a Jump Back In button for the file or note you last edited, Quick Actions (Upload File, New Note, Save Bookmark, Add Secret), a Continue Working list of recently touched items mixed across every surface, What's New (your unread RSS), and a quiet Needs Attention card that only speaks up when something is wrong (failed scans, expiring shares, dead bookmarks, storage nearly full). Admins also get a System Health card: management-by-exception operational checks (runtime, security, features, maintenance, backup freshness and destination) plus an opt-in, cached Silo update check that never phones home unless enabled.
A file manager with a thumbnail grid, a list, or a lazy tree explorer that shows folders and files inline. Drag-and-drop multi-file upload and transactional move, copy, rename, and delete keep the tree and disk in sync. Quick Look previews images, PDFs, audio, video, Markdown, Office documents, and text with arrow-key paging. In-browser editing covers Markdown, HTML, Word (.docx), and spreadsheets (.xlsx, .xls, .csv, .ods) with live formulas (see Editing files). Every save is versioned with an optional change note. Colored tags and starring filter in one click, deletes go to Trash with a purge window, and per-user quotas come with a usage meter and a storage treemap.
A Markdown notes surface with folders, [[wiki-style]] backlinks, @mentions, tags, autosave that snapshots versions, and full-text search. A private, linkable knowledge base that lives next to your files.
An internal link catalog organized into folders, with auto-fetched favicons, optional self-hosted page-preview screenshots, dead-link detection, RSS-feed auto-discovery with one-click subscribe, browser-export import, and de-duplication. Bookmarks are personal by default; mark one shared to publish it to the whole team.
A complete feed reader with feeds and folders, smart folders (Today, This week, Unread, Starred, Recently read, and more), a cursor-paginated inbox, an article reader, OPML import and export for migrating from Feedly or Inoreader, per-feed health metrics and stats, favicons, mute, and notifications on new articles. Starred articles surface on the global /starred view alongside starred files.
An encrypted store for credentials and secrets (username, URL, secret, notes), grouped by category. Entries are encrypted at rest with authenticated AES-256-GCM, with a password generator, rate-limited reveal, and import. Server-side encryption is keyed by a dedicated VAULT_KEY (see Data safety).
A timeline gallery grouped by month, with albums, a full-screen lightbox and slideshow, drag-to-reorder, and in-browser crop, rotate, and flip saved as a new version.
An event-driven rules engine. Subsystems emit events such as rss.item.created, and user-defined rules match conditions and run actions like notifications, search indexing, and activity logging, with an execution log and replay. RSS is the first module wired in; the engine itself is generic.
- Unified search palette: click the navbar search (or press Cmd/Ctrl+K) anywhere to open a Finder/Alfred-style palette with live results grouped by Files, Notes, Articles, and Bookmarks, plus commands and navigation. A scope toggle searches just the surface you are on or all of Silo; a full results page backs it for deep dives. Matching runs on name and content via Laravel Scout, and any file search can be saved as a Smart Folder.
- Sharing grants view, download, or edit to a user by email or to a group. Grants on a folder inherit to everything inside. Public links support an optional password, expiry, and download toggle.
- Admin manages users and groups, reads a security audit log, runs checksummed compressed backups (database dump plus blobs plus manifest) with one-click Test restore verification, watches the home-screen System Health card, and indexes existing server folders in place.
- Light, dark, or auto theme from the user menu, remembered per browser.
- Antivirus scanning (optional): when enabled, every upload is ClamAV-scanned before it is served, and infected or unscannable files are quarantined (fail-closed).
- Background jobs refine MIME types, extract EXIF, image dimensions, ID3 tags, and text snippets, and generate image and PDF first-page thumbnails.
- Hardened by default: files are served as attachments unless safely previewable (nosniff plus a strict CSP), server-side URL fetches are SSRF-guarded, public unlock and uploads are rate-limited, and feed and article HTML is sanitized before render.
| Type | Extensions / MIME | How it renders |
|---|---|---|
| Images | jpg jpeg png gif webp (image/*) |
Inline image |
pdf |
Inline iframe | |
| Audio | audio/* |
<audio> player |
| Video | video/* |
<video> player |
| Markdown | md markdown |
Rendered as HTML (Toast UI viewer) |
| Word | docx |
Rendered document (docx-preview) |
| Spreadsheet | xlsx xls csv ods |
Rendered table (SheetJS) |
| Text | txt log, etc. |
Extracted text snippet |
| Anything else | any | Icon + download prompt |
| Editor | Extensions | Engine | Where it opens |
|---|---|---|---|
| Markdown | md markdown txt text log |
Toast UI Editor (WYSIWYG + Markdown + Preview) | Full-screen modal |
| HTML | html htm |
VibeUI WYSIWYG (Quill) | Full-screen modal |
| Word | docx |
SuperDoc (true .docx round-trip) |
Full-screen editor page |
| Spreadsheet | xlsx xls csv ods |
jspreadsheet-ce + SheetJS | Full-screen editor page |
Spreadsheet editor includes a formula bar and the full jspreadsheet feature set: live formulas (=SUM(A1:A5)), a formatting toolbar, multiple sheets/tabs, search, header filters, column sort/drag/resize, row resize/drag, insert/delete rows & columns, merged cells, comments, and word wrap. Saving preserves number formats, dates, currency, merged cells, column widths, and formulas. (Rich cell styling, font/fill colors, is not written back by the bundled engine.)
Uploads of any type are accepted (subject to the size cap and virus scan). The tables above describe what can be previewed/edited; everything else can still be stored, shared, versioned, and downloaded.
Silo's surfaces, Files, Notes, Bookmarks, RSS, Vault, Photos: are reached from the left rail and each work as you'd expect from the What's inside tour. This section walks the Files workflow in detail (plus the shared Sharing, Profile, and Admin areas that apply across the app); everything below is done from the UI, no shell access required.
- Click a folder to open it; use the breadcrumb or the sidebar tree to navigate.
- Toggle list / grid / tree view from the toolbar (remembered per browser).
- Toggle the sidebar with the menu button; switch theme from the user menu.
- New ▾ menu: New Folder, Markdown file (opens the editor on a blank doc), or Upload files (multi-file, drag-and-drop).
- Open a file's ⋯ menu → Edit (or Actions → Edit inside Quick Look).
- Text/Markdown/HTML open in a full-screen modal; Office docs open on a full-screen editor page with a Full screen toggle.
- Click Save → a "What changed?" popup (optional note) records a new version.
- Click a file (or press space) for Quick Look; arrow buttons page through files; the Actions menu runs any file action without leaving the preview.
- ⋯ menu → Versions lists every prior save with its change note, size, and date, download or restore any version.
- ⋯ menu → Share: grant view/download/edit to a user (by email) or a group; grants on a folder inherit to everything inside.
- Create a public link with optional password, expiry, and download toggle; copy or revoke it any time.
- Tags: add colored tags from the ⋯ menu → Tags, then click a tag to filter.
- Star a file/folder to find it under Starred.
- Rename / Move / Copy / Delete from the ⋯ menu. Deleted items go to Trash and can be restored.
- Profile: change name/email/password and upload + crop an avatar.
- Users: assign groups, toggle admin.
- Groups: create/manage groups used in sharing.
- Audit: security-relevant action log.
- Backups: set a schedule (off/daily/weekly/monthly) + retention, Back up now, Test restore (non-destructive verification), and download or delete any archive. See Backups.
- System Health (home screen): operational checks and backup freshness at a glance, only abnormalities are surfaced.
The fastest way to self-host. One image bundles nginx, PHP-FPM, and the queue worker; on first boot it migrates the database, creates your admin account, and starts serving, no manual steps.
git clone https://github.com/velkymx/silo.git
cd silo
# Set your admin login, then start.
ADMIN_EMAIL=you@example.com ADMIN_PASSWORD='change-me' docker compose up -dOpen http://localhost:8080 and sign in with those credentials.
- Data persists in the
app_storagevolume (uploads + the SQLite database + the app key survive container recreation). - Change the port with
APP_PORT(e.g.APP_PORT=9000 docker compose up -d). - Index an existing folder in place: set
IMPORT_ENABLED=trueand mount it at/import(uncomment the volume indocker-compose.yml); files are referenced, never copied or deleted. - Virus scanning: uncomment the
clamavservice and setFILEMANAGER_AV_ENABLED=true.
By default it runs on SQLite for a zero-dependency start; point DB_* at MySQL/MariaDB/PostgreSQL for production scale. For a manual (non-Docker) install, see Setup below.
Using Docker above? The image ships the core dependencies. Two optional features need extra host packages the base image does not include: ClamAV (upload virus scanning) and Chromium (bookmark screenshots), see their notes below.
- PHP 8.3+ with
gd,exif, andfileinfo(thumbnails + metadata).imagickadds PDF thumbnails;bz2enables ultra backup compression. - Composer 2.x
- Node.js 24+ and npm
- MySQL 8 / MariaDB / PostgreSQL / SQLite
- A queue worker process (see Background processing)
- Optional: ClamAV (
clamd) for upload virus scanning - Optional: Node + puppeteer + Chromium on the worker host for self-hosted bookmark screenshots (see Bookmark link previews)
# 1. Dependencies
composer install
npm install
# 2. Environment
cp .env.example .env
php artisan key:generate
# edit .env: DB credentials, FILEMANAGER_DISK (upload storage), VAULT_KEY, and the FILEMANAGER_* options
# for anything beyond local testing, read "Data safety & production hardening" first
# 3. Database
php artisan migrate
# 4. Public storage symlink (so the configured disk is reachable)
php artisan storage:link
# 5. Build the front end
npm run build # or: npm run dev (during development)
# 6. Serve
php artisan serveThe app is then available at http://127.0.0.1:8000.
php artisan tinker
>>> \App\Models\User::where('email', 'you@example.com')->update(['is_admin' => true]);Uploaded files are processed off the request cycle (virus scan, MIME refinement, metadata, thumbnails). A queue worker must be running, or files stay in "Processing":
php artisan queue:workRun the worker under a supervisor in production, and run the scheduler (one cron line) so trash purging, stalled-upload reconciliation (files:reconcile), and scheduled backups fire:
* * * * * cd /path/to/app && php artisan schedule:run >> /dev/null 2>&1If a worker dies mid-batch, files:reconcile (scheduled every 10 min) re-queues anything stuck in "Processing".
Admins create on-demand or scheduled backups from Admin → Backups. Each backup is an ultra-compressed archive (bzip2 when bz2 is available, otherwise deflate) containing a database dump + all stored file blobs + a manifest. Retention prunes the oldest archives beyond the configured count.
Backups are built to be trusted, not just taken:
- Integrity checksums. Every archive gets a sha256 recorded at creation. Restore verifies it first and refuses a corrupt, truncated, or unverifiable archive instead of restoring garbage over live data.
- Test restore (dry run). One click on any backup verifies the checksum, extracts to a scratch area, import-tests the database dump in isolation, and reconciles blob counts against the manifest, all without touching live data. Prove a backup restores before you need it.
- Restore rolls back on failure. A real restore first snapshots the current live state; if anything fails mid-restore, the system is rolled back to that snapshot instead of being left half-restored. If even the rollback fails, the snapshot is preserved on disk for manual recovery.
- Configurable destination. Archives go to the disk named by
BACKUP_DISK(defaultlocal). The admin System Health card warns when backups share the data disk, when none exist, or when the latest is older thanBACKUP_MAX_AGE_DAYS.
You can also run one from the CLI:
php artisan backup:runStill read Data safety & production hardening. The default destination is the same host as your live data; point
BACKUP_DISKat a separate (ideally offsite-mounted) volume and run a periodic Test restore so a host loss cannot take the data and its backups together.
Silo is a solid application layer over your storage and database, but it is not, by itself, a backup or durability system. If you are entrusting it with important files, the guarantees you actually need (durable storage, restorable backups, protected secrets) come from how you deploy it. Do not skip this section.
Uploads are stored on the disk named by FILEMANAGER_DISK (default public), a local driver: every blob lives on one local disk on one host, with no replication, no integrity re-verification over time, and no offsite copy. A single disk failure loses everything. Pick one of these before storing anything you can't lose:
- Recommended: object storage with versioning. Add an S3 (or S3-compatible bucket such as MinIO, R2, Backblaze B2, or Wasabi) disk in
config/filesystems.php, pointFILEMANAGER_DISKat it, and enable bucket versioning + lifecycle rules. Versioning means an accidental delete or overwrite is recoverable even outside the app, and the provider handles redundancy for you.FILEMANAGER_DISK=s3 AWS_ACCESS_KEY_ID=... AWS_SECRET_ACCESS_KEY=... AWS_DEFAULT_REGION=... AWS_BUCKET=...
- Minimum for a single-server local deploy: put the storage directory (
storage/app) on a redundant volume (RAID / a managed block volume) and replicate it offsite on a schedule with a snapshotting tool such asresticorborg(both give encrypted, deduplicated, point-in-time snapshots you can restore selectively):# nightly, via cron, offsite, encrypted, versioned restic -r s3:s3.amazonaws.com/my-silo-offsite backup storage/app restic -r s3:s3.amazonaws.com/my-silo-offsite forget --keep-daily 7 --keep-weekly 4 --keep-monthly 6 --prune - Whatever you choose, the golden rule: a copy of the data on the same disk as the data is not a backup. You need at least one copy on different hardware, ideally offsite, ideally versioned.
The in-app backup dumps the database, but you should also back up the DB with your engine's native tooling so you can recover to a point in time (not just to the last archive):
- MySQL/MariaDB: enable binary logs + take periodic
mysqldump/xtrabackupsnapshots (or use a managed DB with automated PITR). - PostgreSQL: enable WAL archiving / continuous archiving, or use a managed instance with PITR.
- Store these DB backups on different infrastructure than the DB host, encrypted.
- Move archives off the box. By default (
BACKUP_DISK=local) backups sit beside your data, and the System Health card will warn about it. PointBACKUP_DISKat a separate local-driver disk whose root is an offsite-mounted volume (NFS, rclone/s3fs mount), or sync the archives offsite (S3, restic, or a cronrsyncto another host) so a host loss cannot take the backups with it. - Verify with Test restore. Every archive carries a sha256 checksum, and a restore refuses an archive that fails verification. Use the Test restore button (Admin, Backups) after setup and periodically after: it verifies the checksum, import-tests the database dump in isolation, and reconciles blob counts without touching live data. For maximum confidence, also restore into a throwaway environment occasionally.
- Restore overwrites live data, with a safety net. Restoring replaces the live database and blobs with the archive's contents. It snapshots the current state first and rolls back automatically if the restore fails partway, but it is still a full overwrite when it succeeds: take a fresh backup immediately before restoring, and prefer proving the archive with Test restore first.
The Secrets Vault encrypts entries at rest with a dedicated VAULT_KEY. In production, vault operations refuse to run until VAULT_KEY is set (fail-fast) rather than silently falling back to APP_KEY, because rotating APP_KEY (a routine security action) would make every fallback-encrypted secret permanently undecryptable. Outside production the APP_KEY fallback is allowed, with a logged warning, for developer convenience.
php artisan vault:key # prints a dedicated base64 32-byte key- Set the printed value as
VAULT_KEYin your environment before using the vault in production. - Back up
VAULT_KEYseparately from the database, in a secrets manager (Vault, AWS/GCP Secrets Manager, 1Password, etc.). The encrypted vault data is worthless without it, and it is not included in the in-app backup archive. - With
VAULT_KEYset explicitly,APP_KEYrotation is safe for the vault.
Before exposing Silo to anyone but yourself:
- HTTPS everywhere, with HSTS added at your terminating proxy/load balancer (nginx/Caddy/Cloudflare). Serve nothing over plain HTTP.
- Change the seeded admin password.
ADMIN_PASSWORDdefaults topassword; set a strong one (and a realADMIN_EMAIL) or rotate it immediately after first login. - Set
VAULT_KEYexplicitly and back it up (section 4). - Enable antivirus.
FILEMANAGER_AV_ENABLED=falseby default → uploads are not malware-scanned. Run clamd and setFILEMANAGER_AV_ENABLED=truefor any multi-user or internet-facing deploy (uploads fail closed when a scan errors). - Keep
ALLOW_REGISTRATION=falseunless you intend open signup; create accounts as an admin. - Run the queue worker under a supervisor and the scheduler via cron (see Background processing). Without them: uploads never finish processing, trash never purges, scheduled backups never run.
- Set a per-user quota (
FILEMANAGER_USER_QUOTA_MB) so no single account can exhaust the disk. - Lock down the storage disk and DB to the app host only; never make the raw storage directory web-served.
- Keep dependencies patched:
composer auditandnpm auditshould both be clean before each deploy.
FILEMANAGER_DISK=s3with bucket versioning on (or a redundant volume + nightlyresticoffsite).- Managed DB (or self-managed) with automated point-in-time backups to separate infrastructure.
- In-app scheduled backups on an offsite
BACKUP_DISK(or synced offsite) and proven with the in-app Test restore periodically. VAULT_KEYset and backed up in a secrets manager.- HTTPS + HSTS, AV enabled, admin password changed, worker + scheduler running.
Key options in config/filemanager.php (overridable via .env):
| Variable | Default | Purpose |
|---|---|---|
FILEMANAGER_DISK |
public |
Storage disk for uploads (any disk in config/filesystems.php). |
FILEMANAGER_MAX_UPLOAD_KB |
(unset) | Max KB per file. Unset → PHP's upload_max_filesize / post_max_size. |
FILEMANAGER_USER_QUOTA_MB |
1024 |
Per-user storage quota in MB (0 = unlimited). |
FILEMANAGER_TRASH_RETENTION_DAYS |
30 |
Days an item stays in trash before trash:purge removes it. |
FILEMANAGER_AV_ENABLED |
false |
Scan uploads with the configured antivirus command. |
FILEMANAGER_AV_COMMAND |
clamdscan --no-summary --fdpass |
Scanner command (the file path is appended). |
FILEMANAGER_AV_TIMEOUT |
120 |
Scan timeout in seconds. |
FILEMANAGER_NOTES_FOLDER |
Notes |
Name of the per-user root folder for the Notes surface. |
FILEMANAGER_NOTES_SNAPSHOT_INTERVAL |
10 |
Minutes of continuous editing before notes autosave archives a new version (explicit "Save version" always snapshots). |
VAULT_KEY |
(required in production) | Base64 32-byte key for Secrets Vault encryption at rest (php artisan vault:key). In production, vault operations fail fast when it is unset; outside production it falls back to APP_KEY with a warning. Back it up separately, see Data safety §4. |
BACKUP_DISK |
local |
Destination disk for backup archives (local-driver disks only; mount an offsite volume for durability). System Health warns while backups share the data disk. |
BACKUP_MAX_AGE_DAYS |
7 |
A newest successful backup older than this is flagged as stale on System Health. |
SILO_UPDATE_CHECK |
false |
Opt-in update check: compares the installed Silo version against the latest GitHub release, cached 24h. Never phones home when disabled. |
ALLOW_REGISTRATION |
false |
Open self-service signup. When false, the /register routes return 404 and accounts are created by an admin. |
ADMIN_EMAIL |
admin@example.com |
Email of the admin account created by db:seed on a clean build. |
ADMIN_PASSWORD |
password |
Password for the seeded admin (change for any non-local deploy). |
ADMIN_NAME |
Administrator |
Display name for the seeded admin. |
SCOUT_DRIVER |
database |
Search engine (database, meilisearch, typesense, …). |
- File bodies are served
Content-Disposition: attachmentunless they are a safe, previewable type (image/audio/video/PDF);svg/html/xmlare never inline. All responses sendX-Content-Type-Options: nosniff. - A site-wide Content-Security-Policy plus
X-Frame-Options,Referrer-Policy, andPermissions-Policyare applied by middleware. - Public share unlock is rate-limited and failed attempts are logged; uploads and public downloads are throttled.
- Uploads are virus-scanned (when enabled) and fail closed: infected or unscannable files are not served. Note: AV is off by default (
FILEMANAGER_AV_ENABLED=false); enable it for any shared or internet-facing deploy. - Server-side URL fetches (RSS feeds/favicons/OPML, bookmark liveness checks & screenshots) are guarded against SSRF: non-http(s) schemes and hosts resolving to private/reserved ranges are refused, on every redirect hop.
For durability, backups, the Vault key, and a full deploy checklist, read Data safety & production hardening: security of the data depends as much on how you deploy as on the app itself.
The Launchpad can show a thumbnail preview of each bookmarked page. There are two sources, and both are off by default: a bookmark with neither simply shows a favicon-and-host placeholder card (never a broken image).
-
Self-hosted screenshots (recommended, private). Your server renders each page with headless Chromium via
spatie/browsershot. The URL never leaves your infrastructure, and it works for internal or private tools too. Every capture is SSRF-guarded (private/reserved hosts are refused), and--no-sandboxis enabled by default so Chromium launches under a root/containerized worker.Prerequisites: the queue worker's host (or container) needs Node.js, the puppeteer npm package, and a Chromium/Chrome binary. Two common setups:
- Puppeteer-bundled Chromium (simplest): in the app directory,
npm install puppeteer(this downloads a matching Chromium intonode_modules). Browsershot finds it automatically when it runs from the project root. - System Chromium: install your distro's package (Debian/Ubuntu:
apt-get install -y chromium; Alpine:apk add chromium nss) and point Browsershot at it withBOOKMARK_CHROME_PATH.
Then enable it:
BOOKMARK_SCREENSHOTS=true # Set these if node/chromium aren't on PATH for the worker process: # BOOKMARK_NODE_BINARY=/usr/bin/node # BOOKMARK_CHROME_PATH=/usr/bin/chromium # Only if you run the worker as a non-root user with a real sandbox: # BOOKMARK_CHROME_NO_SANDBOX=false
Docker: the shipped image does not include Chromium (Node is only in the asset-build stage). Add it to your runtime image, e.g.
RUN apt-get update && apt-get install -y chromium && rm -rf /var/lib/apt/lists/*and setBOOKMARK_CHROME_PATH=/usr/bin/chromium, or bake innpm install puppeteer.Apply & verify: new/edited bookmarks capture on the next
ProcessBookmarkrun; Admin → re-hydrate backfills existing ones. Screenshots are best-effort, if one fails (missing Chromium, sandbox, timeout) it logsbookmark.screenshot.failedwith the reason, sotailyour logs /laravel.logto diagnose rather than guessing. - Puppeteer-bundled Chromium (simplest): in the app directory,
-
WordPress mShots fallback (public sites only). An on-demand thumbnail from
s.wordpress.com. It sends the full bookmark URL to a third party, so it is off by default: enable it only if every bookmark points at a public site:BOOKMARK_SCREENSHOT_FALLBACK=true
Never enable this on an intranet or with private/internal bookmarks.
Admins can index an existing directory (e.g. a folder under public/ or a symlink) in place without copying:
php artisan files:import --disk=public --owner=1 --path=existing/folder --into= --name="Imported"Imported files are marked referenced, purging trash never deletes the original source blobs.
php artisan test # backend feature + unit tests
npm run test:e2e # Playwright browser end-to-end suiteThe end-to-end suite boots the app, seeds a deterministic admin, and drives the real UI in a headless browser.
Laravel 13 · Inertia.js · Vue 3 · VibeUI (Bootstrap 5.3) · Laravel Scout · Intervention Image · getID3 · Toast UI Editor · jspreadsheet-ce · SheetJS · SuperDoc.
Released under the MIT License: see LICENSE.

