Indefinite Studies

LabLog: linkding

This is the first 'lablog' post on the site. I'll use this as an identifier for all my self-hosted / homelab posts, as I go through the process of setting up and improving my services and equipment.

So what is it?

linkding is a self-hosted bookmark manager, available at linkding.link. They've got a demo there, so you can give it a try. It looks great - simple in all the right ways, but it looks like it'll do everything I need. I'm an inveterate hoarder of interesting-looking links, some of which I come back to, some of which I don't.

Step one - trying it out

I'm still at an early stage in my self-hosting journey, so I'm currently using a big single docker-compose.yml file for all my services. That's fine for now, but it makes trying out a new service a bit of a pain.

So, before I add something to the big file, I try it out on my laptop in a small, self-contained compose file. The linkding Installation page provides a sample compose file to get started with, plus an env file.

There are a couple of things I want to tweak, so I've put together this version:


services:

  linkding:
    container_name: gardevoir
    image: sissbruecker/linkding:1.37.0
    ports:
      - 9090:9090
    volumes:
      - ./data:/etc/linkding/data
    environment:
      LD_DB_ENGINE: postgres
      LD_DB_DATABASE: linkding
      LD_DB_USER: pg_spoink
      LD_DB_PASSWORD: $LAB_SPOINK_PWD
      LD_DB_HOST: spoink
      LD_SUPERUSER_NAME: ld_gardevoir
      LD_SUPERUSER_PASSWORD: $LAB_GARDEVOIR_PWD
    restart: unless-stopped

  linkding-db:
    image: postgres:17.2
    container_name: spoink
    volumes:
      - ./pg_data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD", "pg_isready", "-d", "outline", "-U", "pg_spoink"]
      interval: 30s
      timeout: 20s
      retries: 3
    environment:
      POSTGRES_USER: pg_spoink
      POSTGRES_PASSWORD: $LAB_SPOINK_PWD
      POSTGRES_DB: linkding
    

I've added a postgres container (because I avoid sqlite wherever possible), found current version numbers and locked to those, and set initial login details. Don't judge me on the use of environment variables for secrets - that's on the list to improve! It all works nicely, so now to add it to the main compose file...

Step two - getting it all plugged in

Now, to add it to my main compose file and get it running as a proper service, I want to make a few improvements over my little local version.

Domain name & TLS

I run Traefik as a reverse proxy in my compose file, and it's set up with a wildcard certificate managed by certbot, so this is as easy as it comes. On the linkding service, I just need to add the following labels:


labels: 
  - traefik.http.routers.gardevoir.rule=Host(`gardevoir.mylab.example`)
  - traefik.http.services.gardevoir.loadbalancer.server.port=9090
    

For DNS, I use the Local DNS feature of my network Pi-hole (a local DNS proxy which provides network-wide ad blocking - very much recommended! pi-hole.net). I just need to add a CNAME record from the domain name I configured in Traefik to the hostname of the machine it's running on (in the case of my example, from gardevoir.mylab.example to gordon.machine.internal).

I'll write a separate post about my Traefik and certbot setup. It took quite a bit of work to get it set up, so I may save someone else some time with a write-up.

Single sign-on via OIDC

I see from the linkding options page that OIDC auth is supported, so I definitely want to implement it. I've got more than enough passwords to remember already, and SSO just lowers the barrier of usage, making me more likely to use a new service (and to share it with friends and family).

In the Keycloak UI, setting up a new client for my homelab realm is straightforward. I'll write a separate post about setting up Keycloak and how I've integrated it with Simplelogin for external access.

Configuration for the docker service was just a matter of setting all the required options as environment variables on the linkding container. It did take a bit of trial and error, though, for one reason:

Alert There's a gotcha here, docmented in the project's GitHub issues. When you add the `LD_ENABLE_OIDC` property to the environment, make sure you enclose in quotes - it needs to be a string, not a boolean! So, `"True"`, not `true` or `True`.

The OIDC implementation is a little bit limited at the moment - no customisation of the log in button, no automatic sign-in - but it works well, and I'm honestly just glad it's there. Lots of projects intended for self-hosting don't include OIDC support, so it's awesome to have it here.

Wrap up

OK, so it's up & running. I've added a browser plugin from my laptop and a Share shortcut on my phone, and it's working great. I've also imported my browser bookmarks, so I've got a bit to sort out, but overall I'm really happy with it. I still need to set up external access and db backups, but this was an easy app to get running, and it's a nice addition to my little online ecosystem.

Final compose file entries


# LinkDing - a shared bookmarks manager
# https://linkding.link/
# Port 9090
linkding:
  container_name: gardevoir
  image: sissbruecker/linkding:1.37.0
  volumes:
    - /labdata/gardevoir:/etc/linkding/data
  environment:
    LD_DB_ENGINE: postgres
    LD_DB_DATABASE: linkding
    LD_DB_USER: pg_spoink
    LD_DB_PASSWORD: $LAB_SPOINK_PWD
    LD_DB_HOST: spoink
    LD_ENABLE_OIDC: "True"
    OIDC_OP_AUTHORIZATION_ENDPOINT: https://nidorina.mylab.example/realms/mylab/protocol/openid-connect/auth
    OIDC_OP_TOKEN_ENDPOINT: https://nidorina.mylab.example/realms/mylab/protocol/openid-connect/token
    OIDC_OP_USER_ENDPOINT: https://nidorina.mylab.example/realms/mylab/protocol/openid-connect/userinfo
    OIDC_OP_JWKS_ENDPOINT: https://nidorina.mylab.example/realms/mylab/protocol/openid-connect/certs
    OIDC_RP_CLIENT_ID: gardevoir
    OIDC_RP_CLIENT_SECRET: $LAB_GARDEVOIR_CLIENT_ID
    OIDC_USERNAME_CLAIM: preferred_username
  restart: unless-stopped
  depends_on:
    - linkding-db
  labels: 
    - diun.enable=true
    - traefik.http.routers.gardevoir.rule=Host(`gardevoir.mylab.example`)
    - traefik.http.services.gardevoir.loadbalancer.server.port=9090

# PostgresDB for LinkDing
# Port 5432
linkding-db:
  image: postgres:17.2
  container_name: spoink
  volumes:
    - /labdata/spoink:/var/lib/postgresql/data
  healthcheck:
    test: ["CMD", "pg_isready", "-d", "outline", "-U", "pg_spoink"]
    interval: 30s
    timeout: 20s
    retries: 3
  environment:
    POSTGRES_USER: pg_spoink
    POSTGRES_PASSWORD: $LAB_SPOINK_PWD
    POSTGRES_DB: linkding
  labels: 
    - diun.enable=true