All apps · 0 apps
Sylo
Docker app from Ferdinand99's Repository
Overview
Readme
View on GitHubSylo
Multi-function Discord bot with a server-management web dashboard, packaged for self-hosting on an Unraid server via Docker. Eleven per-guild modules (moderation, logging, tickets, reaction roles, welcome, sticky messages, auto-moderation, counting, custom commands, scheduled messages, leveling), Discord OAuth2 login, a public leveling leaderboard, and Battlefield-series player stats via the public gametools.network API.
What's new in 2.0
- All eleven modules are functional and configurable from the dashboard
- Custom commands (prefix and optional
/slash), scheduled messages (1 min – 4 weeks), and a full leveling system with/rank,/leaderboardand a shareable public leaderboard page - Auto-moderation, a Counting mini-game, a YAGPDB-style combined overview, and a topbar server switcher
/forgetfor self-service data deletion; automatic data purge when Sylo leaves a server; a per-server config audit log and JSON config export- CI now runs the test suite before any image is built or published; rate-limited public and auth endpoints; a database integrity check on boot
Features
- Discord bot (discord.js v14, slash commands)
/ping— health check with gateway + round-trip latency/version— the release this instance is running ·/about— version, uptime and runtime info/rank·/leaderboard— leveling progress (when the module is enabled)/forget— delete the data Sylo stores about you in the current server/stats battlefield <title> <username> <platform>— player stats as an embed (K/D, win rate, time played, KPM/SPM, best class, …)- Friendly, non-crashing error handling (unknown player, API down, rate-limited)
- Moderation —
/kick,/ban,/unban,/timeout,/untimeout,/purge,/slowmode,/warn(add/list/remove/clear), and/modlogto send every action to a log channel. Role-hierarchy and permission checks, optional DM to the target, and per-command default permissions so Discord hides them from non-moderators. - Extensible game adapters — one file per game, registered in a central registry. Adding a game does not touch bot or web code.
- Per-guild modules — 11 feature groups, each toggled and configured from the
dashboard:
- Moderation — warning thresholds that auto-timeout/kick/ban, one-click unban
- Server logging — member / message / role / channel events to a log channel
- Reaction roles & autoroles — dashboard-built reaction-role embeds; roles on join
- Welcome & leave — join/leave messages with placeholders, optional DM
- Sticky messages — keep a message pinned to the bottom of a channel
- Tickets (modmail) — members DM the bot; staff read and reply from the dashboard (replies arrive as an anonymous "Staff" DM)
- Auto-moderation — invite / link / flood / mass-mention / caps / banned-word filters, each with a delete / warn / timeout action and channel/role exemptions
- Counting — members count upward one number per message in a chosen channel; correct or reset the running number from the dashboard
- Custom commands — text/embed replies triggered by a chat prefix and,
optionally, as
/nameslash commands synced to Discord - Scheduled messages — recurring posts to a channel, every minute to every 4 weeks, with pause/resume
- Leveling — 15–25 XP per message on a MEE6-style curve, level-up
announcements, per-level role rewards,
/rankand/leaderboard
- Command management — disable a command per server or restrict it to channels / roles (admins bypass).
- Operations — per-server config audit log and JSON config export,
/forgetself-service data deletion, automatic data purge on guild removal, a database integrity check at startup, and rate-limited public / auth routes. - Web dashboard (Express + EJS, no frontend framework)
GET /health— JSON status (uptime, guild count, last error) for healthchecks/— bot status, activity stats and module adoption; topbar server switcher/commands— slash-command reference ·/stats— cached lookups/guilds/<id>— per-server control panel: a combined overview, General settings, Commands, Moderation (warnings + bans), Tickets, Message Creator, and a settings panel per module- Discord OAuth2 login (optional) — gate the dashboard to server admins (and configurable staff roles for tickets); runs open on a trusted LAN when unconfigured. See Dashboard authentication.
- SQLite persistence (
better-sqlite3) — guild settings, module config, warnings, tickets and a TTL stats cache, in a single volume-mounted file. - Lean Docker image — multi-stage
node:20-alpine, non-root,HEALTHCHECK.
Project structure
src/
index.js Entrypoint — boots DB, bot, and web in one process
config.js Loads/validates env vars
runtime.js Shared in-memory state (uptime, last error, client)
bot/
index.js Discord client bootstrap (intents, partials)
loadCommands.js / registerCommands.js
commands/ ping, about, version, stats, rank, leaderboard, forget,
kick/ban/unban/timeout/untimeout/purge/slowmode/warn/modlog
events/ ready, interactionCreate, moduleEvents (gateway → modules),
dmTickets (DM → ticket), guildDelete (purge on leave)
embeds/ lib/ embed builders; duration, moderation, modlog, custom-command sync
modules/
registry.js module catalogue (id, intents, defaults)
dispatch.js fans gateway events out to enabled modules
index.js loads module implementations
moderation.js logging.js tickets.js roles.js welcome.js sticky.js
counting.js automod.js customCommands.js scheduledMessages.js leveling.js
adapters/games/ gameAdapter, registry, battlefield
db/
index.js SQLite connection + migrations
cache guildSettings modules commandOverrides warnings tickets
composedMessages counting scheduledMessages leveling
web/
server.js Express app
routes/ health, dashboard, commands, stats, guilds, guildTickets
middleware/ auth (OAuth2), ticketAccess
lib/ views/ public/ helpers; EJS templates; styles.css, app.js
scripts/register-commands.js
test/ battlefield.adapter.test.js, duration.test.js
data/ SQLite file lives here (git-ignored, volume-mounted)
Local setup
Requires Node.js 20+ and a Discord application with a bot.
git clone <this repo>
cd Sylo
npm install
cp .env.example .env # then edit .env (see below)
npm test # optional: run the adapter test suite
npm start
For fast iteration, set DISCORD_GUILD_ID in .env to a test server's ID —
commands then register instantly instead of taking up to ~1 hour globally.
npm run register re-syncs commands without a restart.
Environment variables
| Variable | Required | Default | Description |
|---|---|---|---|
DISCORD_TOKEN |
yes | — | Bot token |
DISCORD_CLIENT_ID |
yes | — | Application (client) ID |
DISCORD_GUILD_ID |
no | — | Register commands to one guild (dev) instead of globally |
WEB_PORT |
no | 3000 |
Dashboard HTTP port |
DISCORD_CLIENT_SECRET |
no | — | Set to require "Log in with Discord" on the dashboard (see below) |
SESSION_SECRET |
no | random | Signs the session cookie; pin it so logins survive restarts |
DASHBOARD_URL |
no | derived | Public dashboard URL; only needed behind a reverse proxy |
INTENT_GUILD_MEMBERS |
no | true |
Request the Server Members privileged intent |
INTENT_MESSAGE_CONTENT |
no | true |
Request the Message Content privileged intent |
GAMETOOLS_API_BASE |
no | https://api.gametools.network |
Stats API base URL |
STATS_CACHE_TTL_MINUTES |
no | 5 |
How long stats lookups are cached |
DATABASE_PATH |
no | ./data/sylo.db |
SQLite file path |
NODE_ENV |
no | development |
Set to production in deployment |
Dashboard authentication
By default the dashboard runs open (no login) — only safe on localhost or a
trusted LAN. To lock it down:
- Discord Developer Portal → your app → OAuth2 → copy the Client Secret
into
DISCORD_CLIENT_SECRET. - Same page → Redirects → add
http://<host>:<WEB_PORT>/auth/discord/callback(e.g.http://192.168.1.10:3000/auth/discord/callback). Behind a reverse proxy, use the public URL and setDASHBOARD_URLto match. - Set a long random
SESSION_SECRETso sessions survive restarts.
With DISCORD_CLIENT_SECRET set, every page except /health requires "Log in
with Discord", and per-server pages require Manage Server (or Administrator /
owner) in that server. /health stays public for the container healthcheck.
Discord application setup
https://discord.com/developers/applications → New Application.
Bot tab → Reset Token → copy into
DISCORD_TOKEN. Under Privileged Gateway Intents enable Server Members and Message Content if you want the member/message-driven modules (logging, welcome, autoroles, leveling, auto-moderation, counting, custom commands); a verified bot may need Discord's approval for Message Content. Otherwise setINTENT_GUILD_MEMBERS=false/INTENT_MESSAGE_CONTENT=false.General Information → copy Application ID into
DISCORD_CLIENT_ID.OAuth2 → URL Generator → scopes
bot+applications.commands. Bot permissions:- Send Messages, Embed Links — always
- Kick Members, Ban Members, Moderate Members, Manage Messages, Manage Channels — moderation
- Manage Roles — reaction roles / autoroles
Open the generated URL to invite the bot. Tickets (modmail) need no extra permission — just leave the bot able to receive DMs.
For moderation to work, drag Sylo's role above the roles of the members it should manage in Server Settings → Roles. The bot can never kick/ban/timeout someone whose highest role sits above its own.
Docker
cp .env.example .env # fill in DISCORD_TOKEN and DISCORD_CLIENT_ID
docker compose up -d --build
docker compose logs -f sylo
The dashboard is then on http://<host>:${WEB_PORT:-3000}. The SQLite database
persists in ./data on the host.
If
better-sqlite3ever fails to build on Alpine for your platform, change the twoFROM node:20-alpinelines in theDockerfiletonode:20-slim.
Backups
All state is in the single SQLite file under the mounted data directory
(./data/sylo.db, plus -wal / -shm sidecars). To back it up, stop the
container briefly and copy the whole data/ folder, or run
sqlite3 data/sylo.db ".backup data/sylo-backup.db" while it runs. Restore by
putting the file back and starting the container — migrations only ever move the
schema forward, and Sylo runs a quick_check on boot and logs if the file is
corrupt. Each server's module configuration can also be exported as JSON from
General → Backup in the dashboard.
Unraid deployment
Option A — docker compose (via the Compose Manager plugin): copy the repo
to /mnt/user/appdata/sylo, add your .env, and docker compose up -d.
Option B — Docker tab → Add Container (manual):
| Field | Value |
|---|---|
| Name | Sylo |
| Repository | your built/pushed image, e.g. ghcr.io/ferdinand99/sylo:latest |
| Network Type | bridge |
| Port | Container 3000 → Host 3000 (WEB_PORT) |
| Path | Container /app/data → Host /mnt/user/appdata/sylo/data (read/write) |
| Variable | DISCORD_TOKEN = your token |
| Variable | DISCORD_CLIENT_ID = your client id |
| Variable | NODE_ENV = production |
| Variable (opt.) | STATS_CACHE_TTL_MINUTES = 5 |
The image's HEALTHCHECK hits /health, so Unraid shows the container health
once it is up. To build the image on the Unraid box itself:
docker build -t sylo:latest /mnt/user/appdata/sylo.
The image starts as root only long enough for its entrypoint to fix ownership of
the mounted data directory, then runs the Node process as an unprivileged user
(sylo, uid 100). So a fresh, root-owned appdata folder works out of the box —
no manual chmod/chown needed. If you still see SQLITE_CANTOPEN, run once:
chown -R 100:101 /mnt/user/appdata/sylo/data.
Option C — Unraid Community Applications template
unraid/sylo.xml is a ready-made CA template with all ports, paths, and
environment variables pre-defined. It needs a published image first:
- Publish the image. CI does this automatically (see
Releases & CI below) — push to
mainand, once the package exists, set its visibility to Public (GitHub → your profile → Packages →sylo→ Package settings). To publish once by hand instead:echo $GITHUB_TOKEN | docker login ghcr.io -u Ferdinand99 --password-stdin docker build -t ghcr.io/ferdinand99/sylo:latest . docker push ghcr.io/ferdinand99/sylo:latest - Add a 256×256 icon at
unraid/sylo-icon.png(the template references it). - Use the template on Unraid — either:
- Docker tab → Add Container → Template: paste the raw URL
https://raw.githubusercontent.com/Ferdinand99/Sylo/main/unraid/sylo.xml, or - drop the file in
/boot/config/plugins/dockerMan/templates-user/and pickSylofrom the User templates dropdown.
- Docker tab → Add Container → Template: paste the raw URL
- To list it in the public CA store, use the submission portal at
ca.unraid.net/submit: sign in with GitHub,
point it at this repo, then run Validate → Scan → Submit for
moderator review. Requirements: a public repo with an OSI license (MIT, at
the root ✓), a root
ca_profile.xmlwith a filled<Profile>(✓), and a template XML with<Repository>,<Registry>,<Overview>,<Support>,<Project>,<Icon>and<TemplateURL>(all inunraid/sylo.xml). Once accepted, template edits pushed tomainpropagate automatically via<TemplateURL>.
Edit Repository, Support, Project, TemplateURL, and Icon in the XML
(and the URLs in ca_profile.xml) if your GitHub username or repo name differ.
Releases & CI
Three GitHub Actions workflows drive the images on GHCR. test.yml runs the
suite (npm test + syntax + template compile) and is a required dependency of
every image build — nothing publishes on a red suite.
| Workflow | Trigger | Publishes |
|---|---|---|
.github/workflows/test.yml |
called by the two below | — (gate only) |
.github/workflows/docker-publish.yml |
every push to main (PRs build only) |
:main, :sha-<short> — rolling dev image |
.github/workflows/release-please.yml |
merging a release PR | :latest, :X.Y.Z, :X.Y — stable release |
Cutting a release is automated with release-please:
- Land changes on
mainusing Conventional Commits (feat: …,fix: …,refactor: …,chore: …,feat!: …/BREAKING CHANGE:for a major bump). - release-please keeps a PR titled "chore(main): release X.Y.Z" open, accumulating a changelog. When you're ready, merge it.
- Merging tags the commit
vX.Y.Z, creates the GitHub Release with notes, bumpspackage.json+CHANGELOG.md, then builds and pushes the versioned images — including:latest, which is what the Unraid template tracks.
No secrets to configure — both workflows use the built-in GITHUB_TOKEN.
Note that a tag/Release created by CI does not trigger other workflows, which
is why release-please.yml builds the image itself rather than relying on the
tag trigger.
For a clean starting point, tag the current commit once so release-please has a
baseline: git tag v1.0.0 && git push origin v1.0.0
(keep .release-please-manifest.json at 1.0.0).
Unraid users who want the bleeding edge can point the container at
ghcr.io/ferdinand99/sylo:main instead of :latest; pin to :X.Y.Z to freeze a
version.
Adding another game
- Create
src/adapters/games/<game>.jsexporting an adapter withid,titles(),platformsFor(title), andgetPlayerStats(username, platform, { title })— throw the typed errors fromgameAdapter.jsfor failure cases. - Register it in
src/adapters/games/index.js(oneimport+register(...)). - Add a subcommand to
src/bot/commands/stats.js. The sharedrunStatsLookup()helper and the cache layer need no changes.
Tests
npm test
Runs the node:test suite for the Battlefield adapter (parsing + error
handling, fetch stubbed — no network).
Legal
For the instances operated by Ferdinand99 (the public Sylo and Sylo - Test Discord applications):
Self-hosted instances are run by their own operators; adjust these documents if you publish your own.
License
MIT © Ferdinand99
Install Sylo on Unraid in a few clicks.
Find Sylo in Community Apps on your Unraid server, review the template, and click Install. Unraid handles the Docker app or plugin setup from the published template.
Categories
Related apps
Explore more like this
Explore allDetails
ghcr.io/ferdinand99/sylo:latestRuntime arguments
- Web UI
http://[IP]:[PORT:3000]/- Network
bridge- Shell
sh- Privileged
- false
Template configuration
Host port for the Sylo dashboard. Only change the host side; the container always listens on 3000.
- Target
- 3000
- Default
- 3000
- Value
- 3000
Where the SQLite database (sylo.db) is stored. Persists across updates/restarts.
- Target
- /app/data
- Default
- /mnt/user/appdata/sylo/data
- Value
- /mnt/user/appdata/sylo/data
Discord bot token (Developer Portal -> your app -> Bot -> Reset Token).
Discord application (client) ID (Developer Portal -> your app -> General Information).
Optional. Set to a server ID to register slash commands to that one guild instantly (useful for testing). Leave blank to register globally (can take up to ~1 hour the first time).
Optional. Set this (Developer Portal -> OAuth2 -> Client Secret) to require 'Log in with Discord' on the dashboard, restricted to server admins. Also add http://[UNRAID-IP]:[HOST-PORT]/auth/discord/callback to the app's OAuth2 Redirects. Leave blank to run the dashboard open (trusted LAN only).
Whether to request the Server Members privileged intent (needed by logging, welcome, roles/autoroles and leveling). Must also be enabled on the Developer Portal Bot page. Set to false to boot without it - those modules then stay unavailable.
- Default
- true
- Value
- true
Whether to request the Message Content privileged intent (needed by logging, auto-moderation and the counting game). Must also be enabled on the Developer Portal Bot page. Set to false to boot without it - those modules then stay unavailable.
- Default
- true
- Value
- true
Optional. Any long random string; signs the dashboard session cookie so logins survive a container restart. Auto-generated when blank.
Optional. Only needed behind a reverse proxy: the public dashboard URL (e.g. https://sylo.example.com) used to build the OAuth2 redirect URI. Leave blank for direct IP access.
How long (minutes) to cache a stats lookup before hitting the API again.
- Default
- 5
- Value
- 5
Base URL of the Battlefield stats API.
- Default
- https://api.gametools.network
- Value
- https://api.gametools.network
Path to the SQLite file inside the container. Must sit inside the Data Directory mount.
- Default
- /app/data/sylo.db
- Value
- /app/data/sylo.db
Port the app listens on inside the container. Leave at 3000 unless you also change the container side of the WebUI Port mapping.
- Default
- 3000
- Value
- 3000
Node environment.
- Default
- production
- Value
- production
IANA timezone name, used for timestamps in ticket transcripts and logs. Defaults to Europe/Oslo.
- Default
- Europe/Oslo
- Value
- Europe/Oslo