BlogGuides

How to self-host Sonarr without Docker on your own VPS

Run Sonarr v4.0.20.3014 on your own Ubuntu server with ox: a .NET 6 build, the webpack UI, SQLite and two variables, from the config that ran.

On this page

To self-host Sonarr you need an Ubuntu server, a fork of the Sonarr repo with an ox.toml and one small Python script added, and two variables for the first admin. In our run the first deploy took about 4 minutes, which covers building the .NET server and its web interface on the server itself, and ox set up no database server, because Sonarr keeps its data in SQLite files.

How we tested On Oct 17, 2026, ox deployed Sonarr v4.0.20.3014 from commit cab419ad on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory. The run then redeployed it, backed it up and rebooted the server. Then a separate test agent, one that did not write the config, signed in, added a root folder and a series, and saw the series with its episodes after a reload, and checked the app's services and its backup.

Sonarr is a PVR for Usenet and BitTorrent users, in its README's words. You give it the TV series you follow. It watches RSS feeds for new episodes and can grab, sort and rename them, and its README lists SABnzbd and NZBGet among its integrations. The code is under the GNU GPL v3. The server is a .NET 6 program, its web interface is built with webpack, and its data sits in two SQLite files.

ox deployed Sonarr v4.0.20.3014 (commit cab419ad) on Oct 17, 2026 on a fresh DigitalOcean server with 8 vCPUs and 16 GB of memory; the first deploy took 4 minutes and a redeploy took 3 minutes.

  • First deploy: 4 minutes, from adding the project until the app passed the run's first checks.
  • Redeploy of the same commit: 3 minutes, until the app passed the checks again.
  • Server: DigitalOcean, 8 vCPUs and 16 GB of memory (s-8vcpu-16gb), with this app alone on it. Every one of the 85 apps we deployed ran on this one size, so it was not picked for this app.
  • Memory in use after the reboot: 685 MB of 15986 MB, for the whole server: Ubuntu, the app, its services, Caddy and the ox agent. On a server with no apps, ox's tests on real servers hold the agent to 40 MB or less and Caddy to 50 MB or less.
  • Source: github.com/Sonarr/Sonarr at v4.0.20.3014, commit cab419ad.
The Sonarr sign-in page
The Sonarr sign-in page
The empty Sonarr series list after signing in
The empty Sonarr series list after signing in
The Sonarr series list with the added series and its 12 episodes
The Sonarr series list with the added series and its 12 episodes

What you need

  • An Ubuntu LTS server with an x86_64 processor, because the config publishes Sonarr for linux-x64. The facts above give the size the run used and the memory it measured after a reboot.

  • A GitHub account to fork Sonarr/Sonarr, and an ox account, which you open by signing in with GitHub.

  • A user name and a password of your choice for the first admin.

  • Internet access from the server. Sonarr's series search goes to its own lookup service, skyhook.sonarr.tv, and the run's server reached it.

  • No mail server and no API key. The run set up no indexer and no download client, and it did not try notifications or Library Import.

The steps

The ox dashboard has an Add a server button. It shows one install command with a single-use token. Run it as root on the Ubuntu server, which gets only the ox agent and Caddy.

Fork Sonarr/Sonarr on GitHub and set your fork's default branch to commit cab419ad, the one in the box above. Add the ox.toml at the repo root and distribution/ox/prepare.py, both shown below, then commit and push. Write a file named sonarr.env with the two keys from the variables section, one KEY=value per line. With the ox CLI signed in, run:

Shell
ox new <you>/Sonarr
ox review sonarr --from-file sonarr.env --wait

The project is named after the repo in lower case, sonarr. The review saves your two values and starts the first deploy, and --wait prints the log until it ends. The quickstart shows the same steps in the browser.

When the deploy is green, ox status sonarr prints the project's address. Open /login on it. The test agent first typed a wrong password and got "Incorrect Username or Password", then signed in with your two values and saw an empty Series list.

To use it, the agent opened Settings, then Media Management, and added a root folder, /srv/ox/sonarr/data/tv on the run's server, which the deploy makes empty. Under Series it chose Add New, typed "The Office" into the search box, picked the show, chose the root folder and pressed Add. After a fresh page load the show was still listed, with its 12 episodes, all missing because no indexer or download client was set up.

The run served Sonarr on its preview address only, so a domain of your own was not part of it. The domains page covers how ox adds one.

The ox.toml, line by line

TOML
# Sonarr v4 (a .NET 6 server with a webpack UI and a sqlite data folder),
# built from source the way its build.sh does for Linux: the install is
# yarn's, which its build uses (ox detects pnpm from the stale
# pnpm-lock.yaml beside yarn.lock, and Volta's yarn pin is not the repo's
# choice); the build runs webpack, publishes the server for linux-x64 and
# puts the UI beside it, where Sonarr looks. The SDK is global.json's pin.
# Sonarr reads Sonarr__Server__* from the environment over its config.xml.

# The first admin. Sonarr has no sign-in until someone sets one, and the
# first visitor would get to set it: before Sonarr's first start,
# distribution/ox/prepare.py names this admin in config.xml, which Sonarr
# adds as a user and then removes from the file.
required_variables = ["ADMIN_USERNAME", "ADMIN_PASSWORD"]

[tools]
dotnet = "6.0.405"
yarn = "1"

[build]
install = "yarn install --frozen-lockfile --production=false"
commands = [
  "yarn run build --env production",
  "dotnet msbuild -restore src/Sonarr.sln -p:Configuration=Release -p:Platform=Posix -p:RuntimeIdentifiers=linux-x64 -t:PublishAllRids",
  "cp -r _output/UI _output/net6.0/linux-x64/publish/UI",
]

[app]
start = "python3 -I distribution/ox/prepare.py && cd _output/net6.0/linux-x64/publish && Sonarr__Server__Port=$PORT Sonarr__Server__BindAddress=127.0.0.1 exec dotnet Sonarr.dll -nobrowser -data=$OX_DATA_DIR"
health = "/ping"

The config repeats the steps of Sonarr's own build.sh for Linux and puts the pieces where Sonarr looks for them.

  • required_variables names the two admin keys. A fresh Sonarr shows its first visitor a dialog to set the login, so whoever found the address first would pick the admin.

  • [tools] installs .NET SDK 6.0.405, the version in the repo's global.json, and Yarn 1, which the web build uses.

  • install runs yarn install --frozen-lockfile, as build.sh does. The repo has a pnpm-lock.yaml beside yarn.lock, so the config names Yarn outright. --production=false keeps the development packages, because webpack is one of them.

  • The first build command runs webpack in production mode and writes the interface to _output/UI. The second publishes the server for linux-x64. The third copies the interface into the publish folder as UI, beside Sonarr.dll, where Sonarr serves it from.

  • start runs the script, changes to the publish folder and runs dotnet Sonarr.dll. Sonarr reads Sonarr__Server__* environment variables over its config.xml. Sonarr__Server__Port takes the port ox gives the app, and Sonarr__Server__BindAddress=127.0.0.1 lets only Caddy reach it. -nobrowser stops it from trying to open a browser on the server, and -data=$OX_DATA_DIR puts config.xml, sonarr.db and logs.db in the project's data folder, which every release shares. exec makes the .NET process the service's main process.

  • health = "/ping" is Sonarr's own check. It needs no sign-in and answers OK once Sonarr can read its database. ox switches traffic to a new release only after it answers.

There is no [services] table, no [storage] table, no worker and no cron job. Sonarr runs its own tasks inside the one app process.

distribution/ox/prepare.py

Code
"""Runs before Sonarr at each start on ox ([app] start in ox.toml).

The first admin: Sonarr keeps its users in sonarr.db, and at every start,
when that database has no user yet, it adds the user config.xml names in
<Username> and <Password> (hashing the password) and then deletes both
elements from the file (UserService.Handle and the config file's
DeleteOldValues, both on ApplicationStartedEvent). So while sonarr.db holds
no user, this writes ADMIN_USERNAME and ADMIN_PASSWORD there with Forms
sign-in, and Sonarr makes the admin as it starts. Once a user exists it
changes nothing: a password or sign-in method changed in Settings stays.

The TV folder: a series needs a root folder that exists and Sonarr can
write, so $OX_DATA_DIR/tv is made for one. It is not backed up: a TV
library is far too big for the daily backup.

The values are read from the environment, never from a command line, and
the password is never printed.
"""

import os
import pathlib
import sqlite3
import sys
import xml.etree.ElementTree as ET

data = pathlib.Path(os.environ["OX_DATA_DIR"])
db = data / "sonarr.db"
config = data / "config.xml"


def users():
    """The number of users in sonarr.db: 0 before Sonarr's first start."""
    if not db.exists():
        return 0
    con = sqlite3.connect(db.as_uri() + "?mode=ro", uri=True)
    try:
        return con.execute('SELECT COUNT(*) FROM "Users"').fetchone()[0]
    except sqlite3.OperationalError as e:
        # A first start that stopped before its migrations made the table.
        if "no such table" in str(e):
            return 0
        raise
    finally:
        con.close()


def name_admin(username, password):
    """Write the admin and Forms sign-in into config.xml, keeping the rest."""
    if config.exists():
        tree = ET.parse(config)
        root = tree.getroot()
    else:
        root = ET.Element("Config")
        tree = ET.ElementTree(root)
    for key, value in (
        ("AuthenticationMethod", "Forms"),
        ("Username", username),
        ("Password", password),
    ):
        el = root.find(key)
        if el is None:
            el = ET.SubElement(root, key)
        el.text = value
    tmp = config.with_name(config.name + ".ox-new")
    fd = os.open(tmp, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
    with os.fdopen(fd, "wb") as f:
        tree.write(f, encoding="utf-8", xml_declaration=True)
    os.replace(tmp, config)


def main():
    (data / "tv").mkdir(exist_ok=True)
    n = users()
    if n:
        print(f"Sonarr has {n} user(s): the admin exists; ADMIN_USERNAME and ADMIN_PASSWORD are not read")
        return
    username = os.environ.get("ADMIN_USERNAME", "").strip()
    password = os.environ.get("ADMIN_PASSWORD", "")
    if not username or not password:
        project = os.environ.get("OX_PROJECT", "<project>")
        sys.exit(f"Sonarr has no user yet and ADMIN_USERNAME or ADMIN_PASSWORD is empty: set both (ox vars set {project} --generate ADMIN_PASSWORD) and deploy again")
    name_admin(username, password)
    print(f"Sonarr has no user yet: config.xml names {username.lower()} (ADMIN_USERNAME), whom Sonarr adds as it starts; sign in at /login")


main()

This script runs before Sonarr on every start and does two jobs.

  • It makes the empty tv folder in the data folder, so there is a place to add as a root folder. The run's backups left it out, since a TV library is too big for a daily backup.

  • It makes the first admin. It opens sonarr.db read-only and counts the users. With none, it writes the Forms sign-in method, the user name and the password into config.xml, a file only the app's user can read. As Sonarr starts, it adds that user with a hashed password and removes the two elements from the file. Once a user exists, the script changes nothing, so a password you change in Settings stays.

The script never prints the password. If either value is empty when a user is needed, the deploy stops and names both keys.

Shell
ADMIN_USERNAME
ADMIN_PASSWORD

ADMIN_USERNAME and ADMIN_PASSWORD are the first admin's login, and you choose both. Sonarr stores the user name in lower case. The script reads them only while Sonarr has no user, so changing them later does not change the account. See variables for how ox stores values.

What it runs on

Sonarr needed no service from ox. After the reboot the run found one systemd unit, the app, active with no restarts. sonarr.db is the main database and has the users in it, and logs.db holds Sonarr's log.

Updates and backups

To move to a newer Sonarr, put its commit in your fork and push, or run ox deploy sonarr --wait. The run redeployed the same commit in the time shown above. A newer Sonarr was not tested.

Backups need no setup. The run's backups held a compressed copy of sonarr.db and one of logs.db, both daily and from Back up now. Run ox backups sonarr to list them, or use the Backups tab. A restore was not tested. The backups page covers copies in an S3 bucket.

Frequently asked questions

Do I need Docker to self-host Sonarr?

No. Sonarr's README lists Windows, Linux, macOS and Raspberry Pi as supported platforms and does not mention Docker. ox builds the .NET server and its web interface from your fork on the server and runs the result as a system service. Nothing is pulled as an image, and an update is a push or one deploy command.

Which database does Sonarr use?

By default Sonarr keeps two SQLite files in its data folder, sonarr.db and logs.db, and ox backs up both. The source also reads Sonarr__Postgres__* settings for a Postgres database. The run used SQLite only, so Postgres was not tested here.

How do I create the first Sonarr login?

Set ADMIN_USERNAME and ADMIN_PASSWORD before the first deploy. The start script gives Sonarr that user before the site is reachable, so no stranger can claim the account first. Sign in at /login. Sonarr has a single account and no sign-up page, and later changes to the two variables do not touch it.

How much memory does self-hosted Sonarr use?

The facts above show what the run measured: the whole server after a reboot, with Sonarr, Caddy and the ox agent on it. The build also runs on the server. ox works on any server from 1 GB up and tells you when memory is why an app stopped.

Does Sonarr download episodes as soon as it is installed?

No. Sonarr looks for releases through indexers and hands them to a download client. The run added neither, and Sonarr's System page warned about both, which is normal on a new install. The series was listed with its 12 episodes marked missing. Setting up an indexer and a download client was not tested.

New to running apps on your own server? How ox self-hosts your apps shows what ox sets up on your server.

Keep reading

Deploy to a server you own

ox sets up systemd, Caddy and PostgreSQL on your Ubuntu server and deploys on every push. Free for your first project.

Sign up with GitHub Read the docs