* fix(metadata): re-fetch titles/overviews when a library's metadata language changes An item's default_metadata_language was stamped once at first match and never updated: the canonical-language pin in mergeAndPersist routed any refresh in a different language into the localization tables, the upserts' COALESCE kept the old stamp forever, quick-mode library refresh skipped complete items entirely, and changing the language in HandleUpdateLibrary triggered nothing. Items stayed in the old language no matter how often the admin refreshed (#211). Four coupled changes: - ProcessRequest.AdoptLanguage: folder-scoped manual refreshes adopt the library's language as the item's new canonical language when it differs from the stamp, rewriting the base row instead of localizing to the side. Only ModeManualRefresh adopts — scheduled refreshes merge fill-empty and would restamp without rewriting the text. - Upsert language pins inverted (media_items, seasons, episodes): prefer the incoming non-empty default_metadata_language over the existing stamp. All existing callers send the unchanged stamp or empty, so behavior is unchanged outside adoption; the restamp is atomic with the canonical write. - Quick-mode refresh lister now includes complete items whose stamp differs from the library's configured language. - HandleUpdateLibrary enqueues a quick library metadata refresh when the metadata language changes, mirroring the paths-change rescan trigger. Fixes #211 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> * fix(metadata): gate language adoption on field locks and library agreement Two hardening fixes for AdoptLanguage from review of #278: - Locked fields defeat the restamp: title/overview merges honor per-field locks, but the restamp was unconditional. An item with both language- bearing fields locked kept its old-language text yet got stamped the new language, so the quick-refresh mismatch predicate never flagged it again. mergeAndPersist now skips adoption when both name and overview are locked, falling back to the non-adopting behavior: the stamp stays put, isCanonicalWrite goes false, and the fetch routes to the localization tables exactly like a non-adopting refresh in that language does today. One locked field still adopts — the other is actually rewritten. - Multi-library flip-flop: an item in libraries with different metadata languages had its canonical base row rewritten to whichever library refreshed last, oscillating forever. Process now requires every library containing the item (media_item_libraries) to resolve to the adoption target before setting AdoptLanguage, via the existing GetDistinctMetadataLanguagesForItem (which applies the same empty→en default as resolveFolderLanguage). Disagreement or a lookup failure keeps the current stamp — stable beats flip-flopping. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com> Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
Silo
A self-hosted media streaming server with a React frontend and Go backend. Supports direct play, remuxing, and hardware-accelerated transcoding. Includes optional Jellyfin/Emby-compatible app support for clients such as VidHub and Findroid.
Join the community on Discord.
Deploy with Docker (recommended)
The easiest way to run Silo is with Docker Compose. The default stack assumes you do not already have PostgreSQL and Redis available, so it bundles PostgreSQL, Redis, FFmpeg, and the application for a one-command start.
-
Create a
.envfilecp .env.example .env -
Set your media path
Edit
.envand set:MEDIA_ROOT=/path/to/your/mediaMEDIA_ROOTis the one value most users need to change. You can also overrideSILO_DATA_ROOTif you do not want bind mounts under/opt/silo, and change ports if the defaults conflict with something else on the host. -
Start the default integrated stack
docker compose up -dThis starts PostgreSQL, Redis, and the integrated Silo server. The app is available at
http://localhost:8090. Jellyfin-compatible app support is disabled until an administrator enables it in onboarding or admin settings.If you already have PostgreSQL and Redis available, omit those bundled service examples from compose and point Silo at your existing
DATABASE_URLandREDIS_URLinstead.Optional NVIDIA/NVENC
GPU support is kept out of the default compose file so hosts without NVIDIA drivers work unchanged.
Install the NVIDIA Container Toolkit and use a Docker Compose version with GPU reservation support before enabling this override.
Use the optional override file when you want NVENC:
docker compose -f docker-compose.yml -f docker-compose.nvidia.yml up -dIf you want this controlled from
.env, setCOMPOSE_FILE:COMPOSE_FILE=docker-compose.yml:docker-compose.nvidia.yml NVIDIA_GPU_COUNT=1Windows uses
;instead of:between compose files.Then
docker compose up -dwill include the NVIDIA override automatically. -
Configure through the admin UI
Add libraries, users, metadata providers, and playback settings from the web interface.
Bind Mount Layout
The deploy-oriented compose files use host folder mappings rather than Docker-managed volumes.
By default, data is stored under /opt/silo:
/opt/silo/postgres/opt/silo/redis/opt/silo/transcode/opt/silo/catalog-seeds
Media is mounted into the container at /mnt/media from the host path you set in MEDIA_ROOT.
Optional Profiles
The main compose file is integrated-first. These profiles exist for operators testing distributed mode or mirroring a split deployment shape. Most single-host installs should stay on the default integrated service, because it already includes proxying and transcoding.
| Profile | Command | Description |
|---|---|---|
| default | docker compose up -d |
Integrated server plus bundled PostgreSQL and Redis |
proxy |
docker compose --profile proxy up -d |
Start a standalone proxy service for distributed-mode testing |
transcode |
docker compose --profile transcode up -d |
Start a standalone transcode service for distributed-mode testing |
You can enable both optional examples together:
docker compose --profile proxy --profile transcode up -d
If you are splitting workers across multiple hosts, use the separate remote worker example instead of trying to stretch the main compose file across machines.
Advanced Remote Node Example
For a dedicated remote transcode worker, use docker-compose.remote-transcode.yml. That file is intended for a separate worker host that connects back to an existing Silo deployment using shared PostgreSQL and Redis.
Deployment Notes
The default compose stack intentionally bundles PostgreSQL and Redis for ease of setup and assumes a fresh install without those services already available. If you already operate PostgreSQL and Redis, omit those examples from compose and point Silo at your existing infrastructure instead. For serious installs, PostgreSQL is better on a separate VM or a managed service so upgrades, tuning, and backups are isolated from the app host. Redis can stay local for many installs, but externalizing it is also reasonable if you already operate shared infrastructure.
Silo is externally stateful by default rather than fully stateless. Durable application state lives in PostgreSQL. Redis only stores coordination and cache-style data. Silo still writes transient transcode output locally under /tmp/silo-transcode. If you switch userdb.backend=sqlite, Silo also becomes locally stateful at /var/lib/silo/userdb.
Migrating an existing Continuum Docker install should be done with the preflight helper and cutover guide in docs/continuum-to-silo-docker-migration.md.
Build from Source
Prerequisites
- Go 1.24+
- Bun 1.0+
- PostgreSQL 18+
- FFmpeg (for transcoding support)
Quick Start
-
Start PostgreSQL (skip if you already have one running)
docker compose up -d postgres redisThe main compose file still expects
MEDIA_ROOTto be set even if you only want the bundled PostgreSQL and Redis services, so set that in.envfirst. -
Configure the database connection
cp .env.example .envEdit
.envand setDATABASE_URLto point to your PostgreSQL instance. -
Build and run
make build ./siloThe server starts at
http://localhost:8080by default. All other settings are configured through the admin UI.
Development
Local development remains intentionally separate from the deploy-oriented compose setup. Use docker-compose.yml and the existing source-build workflow for local development.
# Run the frontend dev server (hot reload, proxies API to :8090)
make dev-frontend
# Run the Go backend
make dev-backend
If you are developing Silo and silo-plugin-sdk together, keep using the local go.work workspace. That workspace is a developer convenience only. CI and release builds run with GOWORK=off, so any new SDK helper used here must be pushed and tagged in silo-plugin-sdk before this repo can merge or release the change.
See CONTRIBUTING.md for contribution expectations, merge request guidance, and the policy for AI-assisted submissions.
Plugin authors should start with docs/architecture/plugin-development.md, which covers the RPC plugin package format, generated proto workflow, SDK import paths, route and asset exposure, and auth or user-config integration points.
Reporting Issues
If you are reporting a bug, install problem, or performance issue, start with the admin workflow and reproduction steps, not Claude/Codex analysis.
Please include:
- What you were trying to do
- Exact steps you took
- What you expected to happen
- What actually happened
- What exact action is slow or broken (
save,scan,browse,import,playback, etc.) - Whether it happens every time or only sometimes
- The library, media type, filter, setting, or value involved
- Version, branch, commit, and deployment details if you know them
- Screenshots, recordings, or log snippets if relevant
If you used Claude/Codex for debugging, put that under Technical notes at the end. Suspected files, SQL output, stack traces, and root-cause theories can be helpful, but only after the workflow and repro steps are clear.
Use this template:
Goal:
Steps:
Expected:
Actual:
What is slow/broken:
Scope:
Version/branch:
Deployment:
Technical notes:
Make Targets
| Target | Description |
|---|---|
make build |
Build frontend + Go binary |
make frontend |
Build frontend only |
make dev-frontend |
Vite dev server with HMR |
make dev-backend |
Run Go backend (integrated mode) |
make dev-proxy |
Run a standalone proxy node |
make dev-transcode |
Run a standalone transcode node |
make migrate-create NAME=add_thing |
Create a timestamped Goose SQL migration |
make migrate-validate |
Validate Goose migration files without touching a database |
make migrate-status |
Show Goose migration status using Silo's bootstrapping runner |
make migrate-up |
Apply pending Goose migrations using Silo's bootstrapping runner |
make clean |
Remove build artifacts |
Database Migrations
PostgreSQL schema migrations are managed by Goose. Migration SQL files live in
migrations/sql/ and use Goose annotations. Converted legacy migrations keep
their original numeric versions so existing schema_versions rows can bootstrap
cleanly into Goose without replaying old SQL. New migrations should be created
with timestamped filenames:
make migrate-create NAME=add_thing
make migrate-validate
Do not run goose fix; timestamped migrations are the repository policy because
they avoid version collisions across parallel PRs. The existing 001-style
files are historical compatibility records, not the naming pattern for new work.
Runtime migrations are applied by the integrated/API server only. Proxy and
transcode modes never mutate schema.
For existing installs, use make migrate-status and make migrate-up rather
than invoking the Goose CLI directly; those targets copy legacy
schema_versions rows into public.goose_db_version under the migration lock
before reading or applying migrations. Set ENV_FILE=path/to/.env when the
database URL should be read from a non-default env file.
Running Tests
# Go tests (uses testcontainers — Docker must be running)
go test ./...
# Frontend tests
cd web && bun test
Linting
# Go
golangci-lint run
# Frontend
cd web && bun run lint
cd web && bun run format:check
License & Trademarks
Silo's source code is licensed under the GNU Affero General Public License
v3.0 or later (AGPL-3.0-or-later) — see LICENSE.
The Silo name, logo, and wordmark are trademarks of Silo Media L.L.C. and are not covered by the AGPL. You're free to fork and redistribute the code, but forks and redistributions must not use the Silo brand as their identity and must remove or replace the brand assets. Publishing a Silo-branded app to an app store requires written permission. See TRADEMARK.md for what's permitted — including referential use like "compatible with Silo."
Configuration
Silo requires only a DATABASE_URL when running from source or against external infrastructure. In the default Docker Compose path, the stack wires the database and Redis URLs for you. All other settings — libraries, metadata providers, transcoding, users — are managed through the admin UI after first launch.
Server Modes
| Mode | Description |
|---|---|
integrated |
Full server: API + frontend + scanner + transcode (default) |
api |
API server only, no local transcoding |
proxy |
Stream proxy node that connects to the shared deployment database and Redis |
transcode |
HLS transcode worker node that connects to the shared deployment database and Redis |
PostgreSQL Auto-Tuning
The default Docker Compose stack does not require a checked-in postgresql.conf.
It enables Silo's pgtune-style OLTP tuning
by default:
POSTGRES_TUNE: auto
When enabled, Silo connects with DATABASE_URL and applies recommendations with
ALTER SYSTEM, which writes to PostgreSQL's postgresql.auto.conf inside the
database data directory. Reloadable settings are applied immediately with
pg_reload_conf(). Settings that PostgreSQL marks as restart-only are written
too, and Silo logs the setting names so you can restart PostgreSQL once:
docker compose restart postgres
The default Compose database user has the required PostgreSQL permissions. If
you use an external PostgreSQL server, make sure the configured DATABASE_URL
user can run ALTER SYSTEM, or set POSTGRES_TUNE=off and manage
PostgreSQL yourself.
For POSTGRES_TUNE_MEMORY=auto, Silo uses the first trustworthy memory source:
a finite Docker cgroup limit, the read-only /host/proc/meminfo mount supplied
by the bundled Compose file, then /proc/meminfo with container safety guards.
Auto-detected memory is treated as a PostgreSQL budget, defaulting to 75% of
detected RAM so Silo, Redis, plugins, transcodes, and the OS retain headroom.
POSTGRES_TUNE_DB_SIZE=auto queries pg_database_size(current_database()) and
classifies the workload by comparing the database size to that memory budget.
Optional tuning overrides:
| Variable | Default | Description |
|---|---|---|
POSTGRES_TUNE_PROFILE |
oltp |
Tuning profile. Only oltp is currently supported. |
POSTGRES_TUNE_MEMORY |
auto |
Server/container RAM, such as 8GB or 32GB; explicit values are used as-is. |
POSTGRES_TUNE_MEMORY_BUDGET_PERCENT |
75 |
Percent of auto-detected RAM used for PostgreSQL recommendations. |
POSTGRES_TUNE_CPUS |
auto |
CPU count used for worker recommendations. |
POSTGRES_TUNE_STORAGE |
ssd |
One of hdd, ssd, san, or nvme. |
POSTGRES_TUNE_DB_SIZE |
auto |
Use less_ram when the database comfortably fits in RAM, mid_ram, or greater_ram for very large databases. |
POSTGRES_TUNE_CONNECTIONS |
100 |
PostgreSQL max_connections; automatically raised if Silo's app pool is configured higher. |
POSTGRES_SHM_SIZE |
8gb |
Docker /dev/shm size for the bundled PostgreSQL container. |
Advanced operators can still supply their own PostgreSQL configuration or
override these env vars. Set POSTGRES_TUNE=off when you do not want Silo to
change PostgreSQL server settings. Settings already written with ALTER SYSTEM
remain in postgresql.auto.conf; reset those PostgreSQL parameters if you later
move fully to a custom postgresql.conf.
Project Structure
cmd/silo/ Entry point
internal/
api/ HTTP router, handlers, middleware
auth/ JWT authentication and sessions
catalog/ Media item, episode, season repositories
config/ YAML + env var configuration
jellycompat/ Jellyfin/Emby protocol compatibility
metadata/ Plugin-driven metadata matching and enrichment
playback/ Direct play, remux, transcode session management
scanner/ Media file discovery and FFProbe
worker/ Background jobs (scan, match, reconcile)
web/ React + TypeScript frontend (Vite, Tailwind, shadcn/ui)
migrations/sql/ Goose-managed PostgreSQL schema migrations