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.
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.
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...
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.
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.
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.
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.
# 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