Skip to content

Installing

As a container

Every release tag publishes ghcr.io/junkerderprovinz/knightloader for amd64 and arm64. The image carries yt-dlp, ffmpeg and the Java runtime the headless JDownloader needs, so nothing else has to be installed.

docker run -d --name knightloader \
  --restart unless-stopped \
  -p 8749:8749 \
  -v /path/to/appdata:/data \
  -v /path/to/downloads:/data/downloads \
  -e TZ=Europe/Berlin \
  ghcr.io/junkerderprovinz/knightloader:latest

Then open http://<host>:8749.

The container starts as root only for a moment. Folders that Docker has just created for it, which belong to root and are empty, are given to uid and gid 1000; then KnightLoader switches to that account and runs as it from then on. A folder that already holds something keeps its owner, and if KnightLoader cannot write it, the log names the chown to run on the host. -e PUID=<uid> and -e PGID=<gid> choose another account.

On Unraid, install KnightLoader from Community Applications. Its template puts --user 99:100 in Extra Parameters and sets the variable UMASK to 000. Finished files then land as nobody:users with mode 0666 and their folders with 0777, so the account you use over SMB can move, rename and delete them. UMASK is read the way linuxserver.io images read it, as an octal mask such as 000, 002 or 022. With --user the container starts as that account straight away, and PUID and PGID are not read. Settings, Health & diagnostics shows which user and mask are in force.

App key

KL_APP_KEY makes the secrets in the data folder useless without it. The login password is hashed with Argon2id and peppered with the key. The session key, the second factor and its recovery codes, the key that seals the stored accounts, every password, token and command line in settings.json, and the archive and download passwords stored with each download are sealed with it, and so are the backups: the one you download from Settings and the ones written on a schedule. Somebody who copies the appdata folder, through a backup plugin for instance, gets nothing they can use.

Generate a value once and keep it with your other passwords:

openssl rand -hex 32

Pass it as -e KL_APP_KEY=<the 64 characters>, or on Unraid add a variable named KL_APP_KEY to the template, and restart. The value has to stay the same from then on. KnightLoader refuses to start on a data folder sealed with a different key, or with none, instead of overwriting it.

An install that ran without the key seals its files at the first start with it. It copies them to a before-app-key-<date> folder in the data folder first, so a sealing that goes wrong can be undone by hand. The copies are as readable as the files were, so KnightLoader deletes the folder itself once it has run for seven days without a value failing to seal or open; each such failure, a start with the wrong key included, starts the seven days over. Until then the Security page says when the folder goes. The password itself moves to Argon2id the next time you sign in. Without the variable everything works as before. On the Security page the info bubble beside App key says what the key does and how to add one.

A sealed backup ends in .zip.sealed. On an instance with another key or none, enter the key of the instance that made it with the upload; everything in it is then sealed again with the key of the instance it lands on. Backups made before the key still restore. A backup without a key holds every password readable.

Behind a reverse proxy

KnightLoader works on a host name of its own, such as https://kl.example.com, and just as well in a folder of another one, such as https://example.com/kl/. Either way the proxy has to pass the WebSocket upgrade through and, when it terminates TLS, send X-Forwarded-Proto. The reverse proxy check under Settings, Network checks both from your browser.

For a folder, KnightLoader has to know the path. There are two ways to tell it:

  • Set KL_BASE_PATH=/kl in the container's environment. The proxy may pass the prefix on or strip it.
  • Set nothing, and let the proxy strip the prefix and name it in X-Forwarded-Prefix. Traefik's StripPrefix middleware sends that header by itself; in nginx add proxy_set_header X-Forwarded-Prefix /kl;.

If both are there, KL_BASE_PATH wins. The prefix has to be a plain path: letters, digits and -._~ between the slashes. It cannot begin with /api, /relay or /dav, since the instance answers those without the prefix too.

nginx, passing the prefix on, with KL_BASE_PATH=/kl:

location /kl/ {
    proxy_pass http://knightloader:8749;
    proxy_http_version 1.1;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
}

Traefik, stripping it, with nothing set:

labels:
  - traefik.http.routers.knightloader.rule=Host(`example.com`) && PathPrefix(`/kl`)
  - traefik.http.routers.knightloader.middlewares=knightloader-strip
  - traefik.http.middlewares.knightloader-strip.stripprefix.prefixes=/kl
  - traefik.http.services.knightloader.loadbalancer.server.port=8749

Caddy, passing it on, with KL_BASE_PATH=/kl. The redir line sends the bare /kl to /kl/, which handle /kl/* would not match. handle_path in place of handle works too: it strips the prefix, and KL_BASE_PATH still names it.

example.com {
    redir /kl /kl/
    handle /kl/* {
        reverse_proxy knightloader:8749
    }
}

Once the path is known, the instance uses it everywhere: the session cookie, the installed web app, its share target and the bookmarklet all include it. Where you name the instance yourself, include the path too: for the Click'n'Load bridge (-bridge https://example.com/kl), for another instance added by address on the Instances page, and as the relay address on the others when this one has "Instance as relay" chosen in the relay card under Settings, Pairing. The phone app and the browser extension reach the instance through the relay with the twelve words and need no address.

The path does not separate the instance from the other applications on the host. They share one origin with it, so a page served by any of them can call KnightLoader's API with your session, whatever path the cookie carries. Only a host name of its own keeps them apart, so give it one unless you trust everything else on that host.

The container's health check and the LAN address http://<host>:8749 keep working without the prefix.

Building the image yourself

docker build --build-arg VERSION=preview --build-arg COMMIT="$(git rev-parse HEAD)" -t knightloader:preview .

Pass both arguments. .dockerignore keeps .git out of the build context, so the toolchain inside the image has no repository to read, and without COMMIT the binary cannot tell which revision it is. VERSION shows under the wordmark, and COMMIT answers as commit on GET /api/health.

As a desktop app

The desktop app is the same binary in a native window instead of a browser tab. The engine, the resolvers, the API and the interface are identical, because the window is served by the same HTTP handler the container serves.

Every release tag builds windows/amd64, windows/arm64, darwin/universal, linux/amd64 and linux/arm64, and attaches each archive to that release. Take x64 for most computers and ARM64 for one with an ARM processor, such as a laptop with a Snapdragon chip. The macOS bundle is universal, so it runs on Intel and on Apple silicon. A running instance offers the same downloads under Settings, Apps, and picks the matching build when the browser reports the processor.

Windows and Linux also get an installer beside each zip. On Windows it puts KnightLoader under C:\Program Files\KnightLoader for everyone on the computer, or into another folder you pick, and asks for an administrator once, while it installs; later updates ask nobody (see Updates). Two switches decide on a Start menu entry and a desktop shortcut, both on at first, and both go to every user. The next install, silent or not, starts from your answer and removes a shortcut you left out. On the way it removes an installation for you alone under AppData\Local\Programs, and it never touches your settings, accounts and private JDownloader in %APPDATA%\KnightLoader. The KnightLoader.exe in the zip is the same program as a portable copy that needs no installing.

On Linux the installer is for you alone and asks for no password. It puts KnightLoader in ~/.local/share/com.junkerderprovinz.knightloader and adds it to the application menu, with a desktop icon if you want one. Right-clicking the menu entry offers to uninstall it. That copy updates itself like the one from the zip. Make the downloaded file executable first (chmod +x knightloader-linux-amd64-installer).

The desktop app brings yt-dlp for video sites: the installer puts it beside the program and the macOS app carries it inside. The Windows and Linux zips hold the program alone, so a copy started from one fetches yt-dlp into its data folder when it starts. Either way the Updates card under Settings, General keeps it current.

ffmpeg and ffprobe, which join video with its audio and convert, are not in the download. On Windows and Linux the desktop app fetches the LGPL build from FFmpeg-Builds into its data folder at the first start when there is no ffmpeg on PATH, checked against that release's checksums, and renews it once a month. On macOS, where that project builds nothing, put ffmpeg on PATH yourself. An ffmpeg already on PATH is always the one used.

The private JDownloader runs on a Java runtime of KnightLoader's own, whatever Java the computer has or lacks. At the first start the desktop app fetches Eclipse Temurin 21 from Adoptium's releases on GitHub, checks it against the SHA-256 that KnightLoader carries for this version and unpacks it into the jre folder of its data folder (%APPDATA%\KnightLoader\jre, ~/Library/Application Support/KnightLoader/jre, ~/.config/KnightLoader/jre). That takes a few minutes, which the JDownloader row on the Updates card shows; a failed download says why there and offers to try again. A KnightLoader update that pins a newer runtime replaces it at its next start. In between, the app asks Adoptium once a week for a newer Temurin 21 while either of JDownloader's update switches is on. With Update automatically it fetches the new runtime, checks it against Adoptium's SHA-256 and moves JDownloader onto it at a moment when none of its downloads runs. The old runtime stays until JDownloader has started on the new one, and comes back if it does not. Windows on Arm gets the x64 runtime, which Windows runs under its emulation, since Adoptium builds no Arm JRE for it. Temurin comes under the GPLv2 with the Classpath Exception. Pointing KL_JD at a JDownloader that runs elsewhere skips all of this.

On Linux the window needs WebKitGTK: libwebkit2gtk-4.1-0.

The desktop app makes its own app key at the first start and keeps it in the operating system's credential store (Windows Credential Manager, the macOS Keychain, the Secret Service on Linux) as app-key under KnightLoader. Its data folder and its backups then open only with that key. To move them to another computer, copy the value and, before KnightLoader first starts there, store it under the same name or set it as KL_APP_KEY. Without a usable credential store the app runs without a key, as before.

While a download is running, or its files are being checked, unpacked or moved, the desktop app keeps the computer from going to sleep, and once nothing is left to do it lets it sleep again. The screen can still turn off. The switch is on the Automation page, at the top of the Idle card. On Linux the app asks logind for a sleep lock, which a local desktop session gets without a password.

The Flatpak

Every release also attaches knightloader-linux-amd64.flatpak and knightloader-linux-arm64.flatpak. Open one in your software center, or install it from a terminal:

flatpak install --user knightloader-linux-amd64.flatpak

Flatpak fetches the GNOME runtime it runs on from Flathub. The app keeps its data in ~/.var/app/io.github.junkerderprovinz.knightloader/config/KnightLoader and fetches yt-dlp, ffmpeg and its Java runtime there, as a copy from the zip does. It can save downloads anywhere in your home folder and on the drives under /media, /run/media and /mnt. A download folder anywhere else needs an override first:

flatpak override --user --filesystem=/srv/downloads io.github.junkerderprovinz.knightloader

The Flatpak never replaces itself, so Update automatically stays off there: install the newer bundle to update it. It is built from the same sources as the other downloads but without the DLC service key, so a .dlc file goes to JDownloader. It keeps the computer awake through the desktop's Inhibit portal, but cannot put it to sleep once the queue is done, so the Idle card does not offer that there. On Wayland the clipboard watch sees a copied link once the window is in front, since the runtime has no wl-paste.

Updates

The desktop app updates itself from GitHub. It asks for the newest release, downloads the zip for your system in the background and checks it against the release's checksums.txt. The new version starts the next time you open KnightLoader, so nothing changes while it runs, and a note in the corner of the window says when it is ready. Pre-releases are never installed. Update automatically on the General page of Settings turns this off; it is on from the start. When Check for updates on the Updates card finds a newer release, Install now fetches it at once instead of waiting for the next daily check.

The installed copy on Windows cannot replace itself, since no user may write to its folder. A scheduled task named KnightLoader Update does it instead, once a day and five minutes after the computer starts, whether KnightLoader is open or not. It replaces KnightLoader.exe, sets the version shown in the list of installed apps and writes what it did, or why it did not, to %ProgramData%\KnightLoader\update.log. There is one installation, so the switch applies to everyone on the computer: it lives in %ProgramData%\KnightLoader\settings.json, which every user may change, and the task skips its run while it is off. An open window notices within ten minutes when the task has put a new version in place and shows the note. Install now does not wait for the task: it downloads the installer of the new release, checks it against checksums.txt and starts it. Windows asks for an administrator, the installer closes KnightLoader and opens the new version when it is done. The replaced program waits beside the new one as KnightLoader.exe.old until a later run removes it.

A portable copy updates itself while it runs: a minute after it starts and once a day after that, in its own folder, and it stays portable. The app on macOS and Linux does the same. A read-only folder, or a macOS app your account cannot change, stays as it is, and the log says why.

Why the task runs as SYSTEM

Program Files belongs to the administrators. A program that replaces itself there without asking anyone needs an account that may write there, and a scheduled task under the system account is how Windows provides one; Firefox's maintenance service and Chrome's updater task work the same way. The task writes to three places and nowhere else: the installation folder, usually C:\Program Files\KnightLoader; its entry in the list of installed apps, under HKLM\Software\Microsoft\Windows\CurrentVersion\Uninstall\KnightLoader; and %ProgramData%\KnightLoader, for its log. It starts no JDownloader, opens no window, listens on no port and reads nothing from any user's profile. The folder under ProgramData belongs to the administrators, and only settings.json in it is open to other users. The installer gives the installation folder the same rights as Program Files. That leaves no file a user could swap for one the system account would run or write to.

You can install into another folder too. Many second drives let everyone rename what sits at their top, and whoever can rename a folder above the installation can swap it. On such a drive the task runs as the person who installed KnightLoader, without administrator rights, and that person may change the installation folder. Updates arrive the same way; only the version in the list of installed apps stays at the one first installed. The task downloads only from GitHub over HTTPS and installs a file only when it matches checksums.txt.

Uninstalling

Uninstalling KnightLoader from the list of installed apps asks for an administrator and removes the program, its shortcuts and its entry, the scheduled task and %ProgramData%\KnightLoader with the switch and the log. The webview cache of the person uninstalling, %APPDATA%\KnightLoader.exe, goes as well. Every user's settings, accounts and private JDownloader in %APPDATA%\KnightLoader stay, so a later install picks them up, and so do the files you downloaded.

A container does not update itself. The same page tells you when a newer release exists; pull the new image the way you deployed this one.

KnightLoader itself updates through the update card on the desktop, and through a new image in a container. yt-dlp, JDownloader and the desktop app's Java keep themselves current between releases; their switches sit on the Components card. UnRAR ships inside KnightLoader: a weekly check in the repository looks for a newer one, and each release brings it along. ffmpeg comes with the container image, and the desktop app renews its own copy once a month.

Building it from source

go install github.com/wailsapp/wails/v3/cmd/wails3@v3.1.0
node scripts/desktop.mjs

The bundle lands in desktop/build/bin; --installer adds the installer for Windows or Linux in desktop/build/installer, built from desktop/installer with PommelPorter. It is a dev build, which never updates itself; only a build stamped with its version, as the release workflow makes, does (see desktop/README.md). Windows and macOS need only their usual toolchains. Linux needs GTK 3 and WebKit: libgtk-3-dev and libwebkit2gtk-4.1-dev to build. There, install the CLI with go install -tags gtk3 ... as well. Without the tag the CLI looks for GTK 4, and the error names a missing package rather than the tag.

The Android app

Install it from F-Droid, which keeps it up to date, or take the APK from the latest release, with the same version number as the server beside it. It reaches an instance on your own network or, with the twelve words, from anywhere else: see Connecting instances and apps.

The browser extension

Chrome, Brave, Opera and Vivaldi install it from the Chrome Web Store, Edge from Edge Add-ons and Firefox from Firefox Add-ons. Settings, Apps on a running instance has a button for each browser that opens its listing, with the steps beside it:

  1. Press Add to Chrome (in Edge Get, in Firefox Add to Firefox) and confirm. Opera first asks for its Install Chrome Extensions add-on.
  2. Pin it. Chrome and Firefox do not put a new extension on the toolbar; it waits behind the puzzle-piece button at the right of the address bar.
  3. Paste your connection phrase into the Remote access card on the options page, which opens by itself on a fresh install.

A Chromium browser without a store, or a build you want to test, takes the ZIP from the latest release: unpack it, open chrome://extensions, switch on Developer mode and choose Load unpacked with the unpacked folder.

Firefox installs only add-ons Mozilla has signed, so a build you want to test loads through about:debugging, This Firefox, Load Temporary Add-on until the next restart. What the extension does is in Bookmarklet, extension and share target.

From a checkout

go run ./cmd/knightloader      # then open http://localhost:8749

The web interface is committed prebuilt in web/dist and embedded into the binary, so a plain go build gives a working server.