Self-hosted service
sissbruecker/linkding avatar
sissbruecker/linkding

linkding's image tracks latest, and its sample env file is where the real decisions live

Self-hosted bookmark manager that is designed be to be minimal, fast, and easy to set up using Docker.

11,423 stars696 forksPythonMIT

At a glance

What is it?
linkding is a single-container Django bookmark manager, MIT licensed, at version 1.47.0 with its last push on 2026-10-26. The README points to full documentation at linkding.link, and the decisions that matter for a first deployment sit in docker-compose.yml, .env.sample, the Makefile and pyproject.toml.
Who is it for?
linkding fits a self-hoster who wants tags, notes and sharing behind a single container and is willing to read .env.sample before the first start. It does not fit a team that needs a documented API contract, or a pinned image tag, because neither is specified in the repository.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 1 day ago.
What is it written in?
Mainly Python, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on November 14, 2026, and from our analysis. They are not legal advice.

From daily snapshots

Star history

  • sissbruecker/linkding11,423 ★ · +462
Star history
Repository2026-07-282026-11-14
sissbruecker/linkding11k11.4k

462 Stars in the last 109 days, measured from the daily GitHub snapshot. Compare with other projects

Editorial analysis

The compose file pulls latest, so a restart can be a version jump

docker-compose.yml defines one service and nothing else.

yaml
services:
  linkding:
    container_name: "${LD_CONTAINER_NAME:-linkding}"
    image: sissbruecker/linkding:latest
    ports:
      - "${LD_HOST_PORT:-9090}:9090"
    volumes:
      - "${LD_HOST_DATA_DIR:-./data}:/etc/linkding/data"
    env_file:
      - .env
    restart: unless-stopped

Two details in those lines cause trouble. The image tag is latest, so a restart after an image pull can put different code in front of a data volume you created months earlier, and the repository does not document what lives in /etc/linkding/data or how it moves between releases. Pinning the tag to a release version yourself (the current version is 1.47.0) is the cheap fix. Second, env_file names .env with no optional flag, so that file must exist before the service starts. Only .env.sample is in the repository, so copying it is the first step that has to happen outside any documented command.

Initial superuser settings and other login paths

The sample environment file treats the first login as configuration rather than setup.

code
# Username of the initial superuser to create, leave empty to not create one
LD_SUPERUSER_NAME=
# Password for the initial superuser, leave empty to disable credentials authentication and rely on proxy authentication instead
LD_SUPERUSER_PASSWORD=

If a username is set but the password is empty, the newly created superuser has no usable password; this does not disable credentials for every user. With both fields empty, the startup process creates no initial superuser. You can create one separately with the documented `docker exec -it linkding python manage.py createsuperuser` command, or configure an authentication proxy. If you set the password in the dotenv file, protect that file as a secret. The same file offers LD_DISABLE_LOGIN_FORM for enforcing OIDC, and mozilla-django-oidc is a runtime dependency. The options documentation says that automatic creation does nothing when the named user already exists.

LD_CONTEXT_PATH is documented with a trailing slash and nothing else

Serving linkding from a subpath takes one variable, and the sample gives one sentence about it.

code
# Can be used to run linkding under a context path, for example: linkding/
# Must end with a slash `/`
LD_CONTEXT_PATH=

There is no example reverse proxy configuration, no note about which headers must carry the prefix, and no list of the paths that get rewritten. The sample states that the value must end with a slash. The same file carries more deployment variables: LD_ENABLE_AUTH_PROXY, LD_AUTH_PROXY_USERNAME_HEADER for the request header the proxy uses to identify the user, and LD_AUTH_PROXY_LOGOUT_URL for where a logout lands, alongside LD_CSRF_TRUSTED_ORIGINS for origins allowed to POST. All but LD_ENABLE_AUTH_PROXY point at docs/Options.md in .env.sample, so a proxy deployment means reading that document.

PostgreSQL needs a driver the default install does not carry

SQLite is the default and needs no configuration at all. Moving to PostgreSQL means setting LD_DB_ENGINE to postgres and supplying LD_DB_DATABASE, LD_DB_USER and LD_DB_PASSWORD. For source builds, pyproject.toml handles the driver deliberately in a separate dependency group.

code
# For PostgreSQL support, use the binary release for development so that not
# everyone needs to build from source. For production, use a separate dependency
# group that builds the driver from source. uv also needs to build it from
# source to update the lockfile, which requires libpq. On macOS:
# - brew install libpq
# - export PATH="/opt/homebrew/opt/libpq/bin:$PATH"
# - uv add --group postgres psycopg[c]

The dev group carries psycopg[binary] so that not everyone has to compile, while a separate postgres group carries psycopg[c] for production builds. The recorded consequence is that uv must build the driver from source even to refresh the lockfile, which needs libpq, so on macOS you install a system library and extend PATH before uv will touch the lock at all. One variable in a dotenv file is the smallest part of that switch.

Six make targets, and three esbuild outputs with hard browser floors

Development runs through a Makefile with six documented targets. make init creates the uv virtual environment, installs the NPM dependencies and runs migrations to create the initial database. make frontend compiles the frontend, make serve starts Django on http://localhost:8000, make test runs everything under pytest, make lint runs ruff, and make format runs ruff for Python, djlint for Django templates and prettier for JavaScript. The frontend step is three esbuild passes chained by build.

json
"build-js": "esbuild bookmarks/frontend/index.js --bundle --minify --sourcemap --format=iife --outfile=bookmarks/static/bundle.js",
"build-theme-light": "esbuild bookmarks/styles/theme-light.css --bundle --minify --external:*.svg --target=chrome109,firefox115,safari16 --outfile=bookmarks/static/theme-light.css",

Outputs land inside the bookmarks app. The JavaScript is bundled, minified and wrapped as an iife rather than an ES module, and each theme stylesheet is compiled for chrome109, firefox115 and safari16 with svg files left external. Those floors decide who can use the result. The frontend libraries are lit, @hotwired/turbo and @floating-ui/dom, each on a caret range, so a fresh npm install can move versions without any commit here.

The devcontainer drops make init, and the reason sits in the uv config

The DevContainers section changes the starting sequence. Once a checkout is open in a devcontainer, only three commands are required: uv run manage.py createsuperuser --username=joe [email protected], make frontend and make serve. make init is not among them, and the README does not say why. Separately, pyproject.toml sets python-preference to system under tool.uv, with a comment that system Python is preferred for now, with less complications when copying the venv in the Docker build. Work outside the devcontainer and you are back to make init plus uv and Node.js as separate prerequisites. The stated prerequisite is Python 3.13, and pyproject enforces it with requires-python set to greater than or equal to 3.13.

One container runs the request handler and the task queue together

The image is built around uwsgi and supervisor: uwsgi.ini sits at the root next to supervisord-all.conf and supervisord-tasks.conf, and both uwsgi and supervisor are runtime dependencies rather than something you install alongside. That is why there is no separate web server to put in front and no queue service to run, since huey is a runtime dependency too and the background work happens in the same process tree. The cost is that LD_DISABLE_BACKGROUND_TASKS is a single switch for that worker. The feature list promises automatic titles, descriptions and icons for bookmarked sites, and automatic archiving either as a local HTML file or on Internet Archive, with waybackpy and requests among the dependencies.

Sharing, administration and the documented REST API

Sharing bookmarks with users or guests, an admin panel, and a REST API are listed in the feature overview. The separate API documentation describes endpoints and token authentication; consult it for request details and check the sharing settings before exposing bookmarks to guests. Import and export are scoped to Netscape HTML, the only format named. Browser coverage comes from Firefox and Chrome extensions plus a bookmarklet, and the app installs as a Progressive Web App. The contributing section is unusually blunt about scope: the maintainer writes that larger features should open an issue first, and that pull requests may be ignored when a feature does not align with the project's goals or that the maintainer does not want to keep maintaining. Design against the documented feature set.

Editorial conclusion

linkding fits a self-hoster who wants tags, notes and sharing behind a single container and is willing to read .env.sample before the first start. It does not fit a team that needs a documented API contract, or a pinned image tag, because neither is specified in the repository. Before deploying, copy .env.sample to .env, set LD_SUPERUSER_PASSWORD, replace the latest tag with a pinned release version (currently 1.47.0), and decide whether SQLite on the mounted volume is enough.

Frequently asked questions

Is there a self-hosted bookmark manager available?

linkding is one: MIT licensed, written in Django, and distributed as a Docker image with a single service in docker-compose.yml. The feature list covers tags, bulk editing, Markdown notes, read it later, sharing with other users or guests, and Netscape HTML import and export.

linkding vs linkwarden

This repository does not compare the two. What it states is linkding's own scope: a Django application in the bookmarks folder, Python 3.13, uwsgi with supervisor, SQLite by default with PostgreSQL optional, OIDC or proxy authentication, a REST API, and a community projects page for third party clients.

linkding vs karakeep

The README does not mention Karakeep, so it offers no comparison. The verifiable side is linkding's own footprint: version 1.47.0 under MIT, a last push on 2026-10-26, full documentation hosted at linkding.link with sources in the docs folder, and browser extensions for Firefox and Chrome.

Official sources

  1. Official documentation
  2. Official README
  3. Project repository
  4. Release notes
Community

Where developers are discussing it

Posts on Hacker News, dev.to and Lobsters that link to this repository, found by our scan of those communities. 1 in the last 30 days; newest first.

Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/sissbruecker-linkding.svg)](https://hysenlabs.com/projects/sissbruecker-linkding)

Star history chart for your README

markdown
[![Star history](https://hysenlabs.com/chart/sissbruecker-linkding.svg)](https://hysenlabs.com/projects/sissbruecker-linkding)

Was this useful?