[Blog](https://deploywithox.com/blog) [Guides](https://deploywithox.com/blog#c-guides) # How to self-host linkding without Docker on your own VPS Run linkding v1.47.0, the bookmark manager, on your own Ubuntu server with ox and no Docker: Postgres, uWSGI and a task worker, from the config that ran. Saurav Sharma Oct 17, 2026 10 min read On this page - [What you need](https://deploywithox.com/blog/self-host-linkding#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-linkding#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-linkding#config) - [Postgres and the worker](https://deploywithox.com/blog/self-host-linkding#services) - [Updates and backups](https://deploywithox.com/blog/self-host-linkding#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-linkding#faq) To self-host linkding you need an Ubuntu server, a fork of the linkding repo with two small files added, and 22 variables, of which only two are yours to choose. In our run the first deploy took about 4 minutes, and ox set up Postgres and the background task worker beside the app. **How we tested** On Oct 17, 2026, ox deployed linkding v1.47.0 from commit [`24b5ad6c`](https://github.com/sissbruecker/linkding/commit/24b5ad6cc9bde497b5d1b1e86aed5a2fb7d25c2f) 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 bookmark with a tag and saw both after a reload, and checked the app's services and its backup. [linkding](https://github.com/sissbruecker/linkding) is a bookmark manager you host yourself. You save a link, tag it, and find it again with the search box. It also has Markdown notes, a read-it-later list, sharing with other users or guests, a REST API, browser extensions, and bookmark import and export. The code is under the MIT license. It is a Django app. esbuild builds its script and themes, uWSGI serves the pages, and a Huey worker runs the background jobs that load icons and preview images. Data goes to SQLite by default, or to Postgres when you ask for it. ox deployed linkding v1.47.0 (commit 24b5ad6c) 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 65 seconds. - **First deploy:** 4 minutes, from adding the project until the app passed the run's first checks. - **Redeploy of the same commit:** 65 seconds, 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:** 943 MB of 15986 MB, for the whole server: Ubuntu, the app and its staging copy, 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. - **Services ox runs for it:** `postgres`. - **Background processes:** `jobs`, each its own systemd unit beside the web process. - **Source:** [github.com/sissbruecker/linkding](https://github.com/sissbruecker/linkding) at v1.47.0, commit [`24b5ad6c`](https://github.com/sissbruecker/linkding/commit/24b5ad6cc9bde497b5d1b1e86aed5a2fb7d25c2f). The linkding login page The empty linkding bookmarks page after signing in The linkding bookmarks page with a new bookmark tagged oxverify ## What you need - An Ubuntu LTS server where you can run one command as root. The run used the size in the facts above, with linkding alone on it. ox works on any server from 1 GB up and tells you when memory is why an app stopped. - A GitHub account to fork `sissbruecker/linkding`, and an ox account, which you open by signing in with GitHub. - No outside account. linkding has no email settings, and loading a site's icon needs the server to reach the web. ## The steps Open Servers in the ox dashboard, press Add a server, and run the command it shows as root on the machine. It installs the ox agent and Caddy, and nothing else of ox's. Fork `sissbruecker/linkding`, check out the commit named in the box above (v1.47.0), and add `ox.toml` and `ox-uwsgi.ini` at the repo's root, as shown below. Commit and push. Then write `linkding.env` on your laptop with the 22 keys from the variables section. You choose the admin's name and password, and the run used `admin` and a made-up password. With the [ox CLI](https://deploywithox.com/docs/cli) signed in, run: Shell ```bash ox new /linkding ox review linkding --from-file linkding.env --wait ``` The project is named after the repo. The review saves your values and starts the first deploy, and `--wait` prints the run until it ends. In the browser the same steps are New project and the review screen, as the [quickstart](https://deploywithox.com/docs/quickstart) shows. When the deploy is green, open the project's address and go to `/login/`, because linkding has no sign-up page. The test agent also tried a wrong password, and linkding refused it. Then press Add bookmark, enter a web address and a tag, and save. The test agent reloaded the page and found both. Browser extensions, the REST API, file import and page snapshots were not tested. The run served linkding on its preview address only. A domain of your own was not part of it, and the [domains page](https://deploywithox.com/docs/operate/domains) shows how ox adds one. ## The ox.toml, line by line TOML ```toml # linkding v1.47.0 (Django + a huey task worker, uwsgi, postgres), run outside # Docker the way bootstrap.sh and its Dockerfile run it. The frontend is built # with esbuild, the Postgres driver from source (the `postgres` group). # psycopg[c] (the `postgres` group below) is built from source and needs # libpq's headers, which detection does not read from a dependency group. packages = ["libpq-dev"] [app] # bootstrap.sh's uwsgi, with the ini's static-map lines for the folders ox # keeps. The upstream ini drops to www-data, writes /tmp/linkding.pid and # opens a stats port, and two releases overlap during a deploy (D-8), so the # overlay keeps a copy without those three. start = "exec uv run uwsgi --http 127.0.0.1:$PORT ox-uwsgi.ini" health = "/health" [build] # The Postgres client is built from source, as the Dockerfile's postgres # group does. install = "uv sync --frozen --no-dev --group postgres" # `npm run build` makes bookmarks/static/bundle.js and the themes, which # collectstatic then gathers. # The huey queue (data/tasks.sqlite3) is opened when settings load, in # collectstatic too: ox makes the kept data folder before the build. commands = [ "npm ci && npm run build", "uv run python manage.py collectstatic --noinput", ] # bootstrap.sh's order: the data folders and the secret key (kept in the data # folder), migrations, the first user from LD_SUPERUSER_*, and the move of # legacy tasks to huey. migrate = "mkdir -p data/favicons data/previews data/assets && uv run python manage.py generate_secret_key && uv run python manage.py migrate --noinput && uv run python manage.py create_initial_superuser && uv run python manage.py migrate_tasks" [workers] # The background task processor bootstrap.sh starts under supervisord. jobs = "exec uv run python manage.py run_huey -f" [storage] # data/ holds the secret key, the task queue, favicons, previews and assets. keep = ["data"] [tools] # The Dockerfile's Node major; uv reads requires-python for the rest. node = "22" [services] postgres = {} ``` The config follows linkding's `bootstrap.sh` and Dockerfile, without the container. - `packages` adds `libpq-dev`. The `postgres` dependency group builds the Postgres driver from source, which needs libpq's headers. - `start` runs uWSGI under `uv` on the loopback address and the port in `$PORT`, with Caddy in front. - `health` is linkding's own `/health`. It opens a database connection and answers healthy, or 500 when it cannot. ox switches to a new release only when this page answers. - `install` installs what `uv.lock` pins, without the dev tools, plus the `postgres` group. - The build `commands` bundle the script and themes with esbuild, then run `collectstatic`. linkding's settings open the task queue file in `data/` as they load, so ox makes the kept folder before the build. - `migrate` keeps the order of `bootstrap.sh`: make the icon, preview and asset folders, write the secret key to `data/secretkey.txt` unless it exists, migrate Postgres, make the first user, and move tasks left by an older linkding to Huey. The user step does nothing when the name exists, so every deploy can run it. - `[workers]` runs Huey as a unit of its own, where the Docker image uses supervisord. The test agent saw its journal report two threads and a scheduler that wakes every 10 seconds. - `[storage]` keeps `data` across releases: the secret key, the task queue, icons, previews and assets. - `[tools]` pins Node 22, the Dockerfile's major. For Python, `uv` reads `requires-python`, which asks for 3.13 or newer. - `[services]` asks for Postgres, and ox makes the database and its user. `ox-uwsgi.ini` Code ``` [uwsgi] module = bookmarks.wsgi:application env = DJANGO_SETTINGS_MODULE=bookmarks.settings.prod route = ^/static/ addheader:Content-Security-Policy: sandbox static-map = /static=static static-map = /static=data/favicons static-map = /static=data/previews static-map = /robots.txt=static/robots.txt processes = 2 threads = 2 vacuum = True buffer-size = 8192 die-on-term = true ``` This is linkding's `uwsgi.ini` minus three things: the drop to `www-data`, a pid file under `/tmp`, and a statistics port. ox runs the old and new release side by side during a switch, so a shared pid file and a fixed port would collide, and the app already runs as its own user, `ox-linkding`. The upstream blocks for `LD_CONTEXT_PATH`, `LD_REQUEST_TIMEOUT`, `LD_REQUEST_MAX_CONTENT_LENGTH`, `LD_LOG_X_FORWARDED_FOR` and `LD_DISABLE_REQUEST_LOGS` are left out too, so the uWSGI side of those options does nothing here. The run used none of them. Shell ```bash DJANGO_SETTINGS_MODULE LD_DB_ENGINE LD_DB_HOST LD_DB_PORT LD_DB_DATABASE LD_DB_USER LD_DB_PASSWORD LD_SUPERUSER_NAME LD_SUPERUSER_PASSWORD LD_CSRF_TRUSTED_ORIGINS HOST_NAME LD_CONTAINER_NAME LD_HOST_PORT LD_HOST_DATA_DIR LD_CONTEXT_PATH LD_DISABLE_BACKGROUND_TASKS LD_DISABLE_URL_VALIDATION LD_ENABLE_AUTH_PROXY LD_AUTH_PROXY_USERNAME_HEADER LD_AUTH_PROXY_LOGOUT_URL LD_DISABLE_LOGIN_FORM LD_DB_OPTIONS ``` `DJANGO_SETTINGS_MODULE` is `bookmarks.settings.prod`, because `manage.py` otherwise picks the development settings. `LD_DB_ENGINE` is `postgres`. `LD_DB_HOST`, `LD_DB_PORT`, `LD_DB_DATABASE`, `LD_DB_USER` and `LD_DB_PASSWORD` are references like `${postgres.host}` that ox fills in from the database it made. `LD_SUPERUSER_NAME` and `LD_SUPERUSER_PASSWORD` are yours. `LD_CSRF_TRUSTED_ORIGINS` is `${PUBLIC_URL}`, the project's own address, which linkding trusts for POSTs such as the login form. `HOST_NAME` is `${PUBLIC_HOST}`: when it is set, the production settings let in only that host. The run saved a value for every other key in linkding's `.env.sample`. Three are for Docker Compose and mean nothing here: `LD_CONTAINER_NAME`, `LD_HOST_PORT` and `LD_HOST_DATA_DIR`. The rest are linkding's options at their defaults: no context path, background tasks on, URL validation on, no authentication proxy, the login form shown, and no extra database options. The [variables page](https://deploywithox.com/docs/operate/variables) explains references. ## Postgres and the worker Postgres is shared with the server's other apps, each with its own database and user. The run also deployed a staging copy beside production, so the memory figure above counts two copies of the app and its worker. The worker loads icons and preview images only when the matching switches in linkding's Settings are on. The test agent turned on Enable Favicons, and the worker wrote an icon file for the bookmark's site. linkding gets icons from a Google service unless you set `LD_FAVICON_PROVIDER`, which the run left unset. ## Updates and backups To move to a new release, merge its commit into your fork and push, or run `ox deploy linkding --wait`. ox builds beside the running release, runs the migrations, and switches when the new one answers. It dumps Postgres before each migrate, and the run's redeploy left that dump. Read linkding's `CHANGELOG.md` first. Backups need no setup. The run's backups held a Postgres dump, an archive of `data` and a copy of the task queue file, daily and from Back up now. List them with `ox backups linkding`. linkding's own `full_backup` command is written for SQLite, and its docs send Postgres users to back up by hand. A restore was not tested. The [backups page](https://deploywithox.com/docs/operate/backups) covers an S3 bucket, and [the self-hosting list](https://deploywithox.com/self-hosting) shows the other apps ox has run. ## Frequently asked questions ### Do I need Docker to self-host linkding? No. linkding's own docs lead with Docker, but the app is plain Django. ox builds it on the server from your fork, runs uWSGI and the Huey worker as system services, and installs Postgres beside them. An update is a push to your fork or one deploy command. ### How do I create the first linkding user? Set LD_SUPERUSER_NAME and LD_SUPERUSER_PASSWORD before the first deploy. The migrate step makes that user, and you sign in at /login/. Leave the password empty and the user gets no usable password, so only an authentication proxy can sign it in. ### Does linkding need Postgres, or is SQLite enough? linkding supports both, and SQLite is its default. The run used Postgres, by setting LD_DB_ENGINE to postgres and pointing the other LD_DB keys at the database ox made. Running linkding on SQLite under ox was not tested, so this guide covers the Postgres setup only. ### How much memory does self-hosted linkding use? After the reboot, the whole server held the memory the facts above show: production and staging copies of linkding with their workers, Postgres, Caddy and the ox agent. The build runs npm and compiles the Postgres driver on the server too. ox takes any server from 1 GB up and says so when memory is why an app stopped. ### Why does linkding say CSRF verification failed at login? linkding answers 403 when a form's Origin header does not match the Host header it received, which a proxy that rewrites Host can cause. Its docs say to list your address in LD_CSRF_TRUSTED_ORIGINS. The run set that key to the project's own address, and sign-in worked on the preview address. New to running apps on your own server? [How ox self-hosts your apps](https://deploywithox.com/self-hosting) shows what ox sets up on your server. On this page - [What you need](https://deploywithox.com/blog/self-host-linkding#what-you-need) - [The steps](https://deploywithox.com/blog/self-host-linkding#steps) - [The ox.toml, line by line](https://deploywithox.com/blog/self-host-linkding#config) - [Postgres and the worker](https://deploywithox.com/blog/self-host-linkding#services) - [Updates and backups](https://deploywithox.com/blog/self-host-linkding#updates) - [Frequently asked questions](https://deploywithox.com/blog/self-host-linkding#faq) - [Do I need Docker to self-host linkding?](https://deploywithox.com/blog/self-host-linkding#faq-docker) - [How do I create the first linkding user?](https://deploywithox.com/blog/self-host-linkding#faq-user) - [Does linkding need Postgres, or is SQLite enough?](https://deploywithox.com/blog/self-host-linkding#faq-database) - [How much memory does self-hosted linkding use?](https://deploywithox.com/blog/self-host-linkding#faq-memory) - [Why does linkding say CSRF verification failed at login?](https://deploywithox.com/blog/self-host-linkding#faq-csrf) Written by Saurav Sharma Builds ox. Writes about running apps on servers you own, without Docker and without a platform bill. [Older post How to self-host Linkwarden (bookmarks) on your own VPS](https://deploywithox.com/blog/self-host-linkwarden) [Newer post How to self-host Langflow on your own VPS, no Docker](https://deploywithox.com/blog/self-host-langflow) ## Keep reading - Guides [Self-Hosted Open-Source Apps Deployed with ox: All 85](https://deploywithox.com/blog/self-hosted-open-source-apps-deployed-with-ox) ox deployed 85 open-source apps from pinned commits on fresh servers, with no Docker. The stacks, services, deploy times and a table of every app. Saurav Sharma Oct 17, 2026 16 min read - Guides [How to self-host AFFiNE on your own VPS without Docker](https://deploywithox.com/blog/self-host-affine) Run AFFiNE v0.27.4 on your own Ubuntu server with ox: Postgres with pgvector, Redis, a Rust and Node build, and an admin made from two variables. Saurav Sharma Oct 17, 2026 12 min read - Guides [How to self-host Akaunting on your own VPS without Docker](https://deploywithox.com/blog/self-host-akaunting) Run Akaunting 3.2.4 on your own Ubuntu server with ox: PHP, MariaDB, a scheduler and the first company and admin made on deploy, from the config that ran. Saurav Sharma Oct 17, 2026 10 min read ## 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](https://deploywithox.com/login/github) [Read the docs](https://deploywithox.com/docs)