PageSourceSearch

https://docs.linkwarden.app/assets/js/4456e71c.78c6cc3d.js

js linkwarden.app collected 2026-09-24 09:11:18 UTC 7,189 bytes, 1 lines download raw bytes

1"use strict";(self.webpackChunkmy_website=self.webpackChunkmy_website||[]).push([[4006],{17496:(e,s,n)=>{n.r(s),n.d(s,{assets:()=>d,contentTitle:()=>a,default:()=>c,frontMatter:()=>t,metadata:()=>r,toc:()=>l});const r=JSON.parse('{"id":"self-hosting/non-root","title":"Running as a Non-Root User","description":"By default, the processes inside the Linkwarden containers run as root. This guide shows how to run all services as a regular user instead, so that a compromised container has limited permissions and the files in your data folders are owned by your own user instead of root.","source":"@site/docs/self-hosting/non-root.md","sourceDirName":"self-hosting","slug":"/self-hosting/non-root","permalink":"/self-hosting/non-root","draft":false,"unlisted":false,"editUrl":"https://github.com/linkwarden/docs/blob/main/docs/self-hosting/non-root.md","tags":[],"version":"current","sidebarPosition":8,"frontMatter":{"sidebar_position":8},"sidebar":"sidebar","previous":{"title":"User Content Domain","permalink":"/self-hosting/user-content-domain"},"next":{"title":"To Linkwarden v2","permalink":"/self-hosting/upgrading/to-linkwarden-v2"}}');var i=n(74848),o=n(28453);const t={sidebar_position:8},a="Running as a Non-Root User",d={},l=[{value:"How It Works",id:"how-it-works",level:2},{value:"New Installations",id:"new-installations",level:2},{value:"Existing Installations",id:"existing-installations",level:2},{value:"Verify That It Works",id:"verify-that-it-works",level:2}];function h(e){const s={a:"a",code:"code",h1:"h1",h2:"h2",header:"header",li:"li",p:"p",pre:"pre",ul:"ul",...(0,o.R)(),...e.components};return(0,i.jsxs)(i.Fragment,{children:[(0,i.jsx)(s.header,{children:(0,i.jsx)(s.h1,{id:"running-as-a-non-root-user",children:"Running as a Non-Root User"})}),"\n",(0,i.jsx)(s.p,{children:"By default, the processes inside the Linkwarden containers run as root. This guide shows how to run all services as a regular user instead, so that a compromised container has limited permissions and the files in your data folders are owned by your own user instead of root."}),"\n",(0,i.jsx)(s.p,{children:"It assumes:"}),"\n",(0,i.jsxs)(s.ul,{children:["\n",(0,i.jsxs)(s.li,{children:["you are using the Docker Compose setup from the ",(0,i.jsx)(s.a,{href:"/self-hosting/setup",children:"Setup"})," guide"]}),"\n",(0,i.jsx)(s.li,{children:"your user has permission to run Docker commands"}),"\n"]}),"\n",(0,i.jsx)(s.h2,{id:"how-it-works",children:"How It Works"}),"\n",(0,i.jsxs)(s.p,{children:["Linux identifies users by number: a user id (UID) and a group id (GID). The ",(0,i.jsx)(s.code,{children:"user"})," option in Docker Compose starts a container's processes with the given numbers instead of the image's default, which is root. The only requirement is that the mounted folders (",(0,i.jsx)(s.code,{children:"data"}),", ",(0,i.jsx)(s.code,{children:"pgdata"}),", and ",(0,i.jsx)(s.code,{children:"meili_data"}),") are owned by the same numbers, because the kernel grants file access by comparing them."]}),"\n",(0,i.jsxs)(s.p,{children:[(0,i.jsx)(s.code,{children:"1000"})," is the UID of the first regular user created on most Linux systems. Run ",(0,i.jsx)(s.code,{children:"id -u"})," and ",(0,i.jsx)(s.code,{children:"id -g"})," to see your own numbers, and use those everywhere below if they differ."]}),"\n",(0,i.jsx)(s.h2,{id:"new-installations",children:"New Installations"}),"\n",(0,i.jsxs)(s.p,{children:["Follow the ",(0,i.jsx)(s.a,{href:"/self-hosting/setup",children:"Setup"})," guide, but before running ",(0,i.jsx)(s.code,{children:"docker compose up"})," for the first time, create the data folders yourself:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"mkdir -p data pgdata meili_data\n"})}),"\n",(0,i.jsxs)(s.p,{children:["These folders are simply the host side of the volumes defined in ",(0,i.jsx)(s.code,{children:"docker-compose.yml"}),". If there are additional volumes defined, include their folders in the same command."]}),"\n",(0,i.jsx)(s.p,{children:"So the list to create always mirrors your compose file. If a future version of it (or a service you add yourself) mounts more folders, create those the same way before starting the stack."}),"\n",(0,i.jsx)(s.p,{children:"The order matters. If Docker Compose runs first, Docker 
1creates these folders owned by root and the containers won't be able to write to them."}),"\n",(0,i.jsxs)(s.p,{children:["Then add a ",(0,i.jsx)(s.code,{children:'user: "1000:1000"'})," line to every service in ",(0,i.jsx)(s.code,{children:"docker-compose.yml"}),". Example:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-yaml",children:'services:\n  linkwarden:\n    image: ghcr.io/linkwarden/linkwarden:latest\n    user: "1000:1000"\n    # ...rest of the service stays unchanged\n'})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-yaml",children:'postgres:\n  image: postgres:16-alpine\n  user: "1000:1000"\n  # ...\n'})}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-yaml",children:'meilisearch:\n  image: getmeili/meilisearch:v1.12.8\n  user: "1000:1000"\n  # ...\n'})}),"\n",(0,i.jsxs)(s.p,{children:["If you run additional services in the same file, give each of them the same ",(0,i.jsx)(s.code,{children:"user"})," line, and make sure any folder they mount is owned by the same numbers."]}),"\n",(0,i.jsxs)(s.p,{children:["Start the stack as usual with ",(0,i.jsx)(s.code,{children:"docker compose up -d"}),"."]}),"\n",(0,i.jsx)(s.h2,{id:"existing-installations",children:"Existing Installations"}),"\n",(0,i.jsxs)(s.p,{children:["An existing install has data folders owned by root, created by the containers themselves. Hand them to your user once, then add the same ",(0,i.jsx)(s.code,{children:"user"})," lines as above:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"docker compose down\nsudo chown -R 1000:1000 data pgdata meili_data\ndocker compose up -d\n"})}),"\n",(0,i.jsxs)(s.p,{children:["The folders to ",(0,i.jsx)(s.code,{children:"chown"})," are simply the host side of the volumes in your ",(0,i.jsx)(s.code,{children:"docker-compose.yml"}),". If there are additional volumes defined, include their folders in the same command."]}),"\n",(0,i.jsx)(s.h2,{id:"verify-that-it-works",children:"Verify That It Works"}),"\n",(0,i.jsx)(s.p,{children:"Check which user the container is running as:"}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"docker compose exec linkwarden id\n"})}),"\n",(0,i.jsxs)(s.p,{children:["This should print ",(0,i.jsx)(s.code,{children:"uid=1000"})," rather than ",(0,i.jsx)(s.code,{children:"uid=0(root)"}),". Then add a link in the app and confirm that its preserved formats (screenshot, PDF, readable) are generated. The files should show up in ",(0,i.jsx)(s.code,{children:"./data/archives/"})," owned by your user:"]}),"\n",(0,i.jsx)(s.pre,{children:(0,i.jsx)(s.code,{className:"language-bash",children:"ls -l data/archives/\n"})})]})}function c(e={}){const{wrapper:s}={...(0,o.R)(),...e.components};return s?(0,i.jsx)(s,{...e,children:(0,i.jsx)(h,{...e})}):h(e)}}}]);

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.