Repository navigation
fix(docker): pre-create writable /app/cache and /logs owned by UID 100 - #2172
Conversation
The image runs as UID 100 on a `scratch` base, so no directory other than
/app existed. Docker only propagates ownership into a fresh named volume
when the image already has content at the mount path - with nothing there,
the volume is created root:root 0755 and UID 100 cannot write to it.
That made *every* writable mount fail, named volumes included:
- loading.downloads.cachePath logged
"cannot create temp cache file in /cache, serving without caching"
and served uncached (#2165)
- the CSV query log writer logged "can't create/open file ... permission
denied" and dropped entries - this affected the `queryLogs:/logs`
named volume in our own advanced docker-compose example
Copy an empty dir into the final stage twice with --chown=100:100 to seed
/app/cache and /logs. A fresh named volume mounted over either path now
inherits UID 100 ownership and is writable with no host-side setup.
Bind mounts keep the host directory's ownership and still need
`chown 100:100` or a matching `--user`; document that, along with the
UID 100 requirement, in installation.md and next to cachePath. Also add
uid=100,gid=100 to the CIFS example, which mounts root-owned by default.
Fixes #2165
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #2172 +/- ##
=======================================
Coverage 87.94% 87.94%
=======================================
Files 124 124
Lines 9828 9828
=======================================
Hits 8643 8643
Misses 922 922
Partials 263 263 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
Pull request overview
This PR addresses Docker permission failures for writable mounts when running the scratch image as USER 100, by ensuring mount targets exist and are owned by UID 100 (so fresh named volumes become writable), and by documenting expected behavior/requirements for bind mounts and cachePath.
Changes:
- Seed
/app/cacheand/logsin the final Docker image with ownership100:100to make fresh named volumes writable. - Document writable-mount permission requirements (UID 100) and CIFS mount options in installation docs.
- Clarify that an unwritable
loading.downloads.cachePathdegrades performance (cache disabled) rather than breaking DNS, and propagate that note into schema/docs.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| Dockerfile | Pre-creates seed dir and copies it into final stage with --chown=100:100 to seed writable mount points. |
| docs/installation.md | Adds UID 100 writable-mount note and CIFS uid/gid guidance. |
| docs/configuration.md | Adds warning that unwritable cachePath disables on-disk caching but continues serving DNS. |
| config/config.go | Extends cachePath field documentation about writability requirements. |
| docs/config.schema.json | Regenerates schema descriptions for cachePath with the new caveat. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| - run as the host user that owns the directory: | ||
| `docker run -u "$(id -u):$(id -g)" ...`, or in compose | ||
| `user: "1000:1000"`. |
| // serves unchanged/unavailable sources from this directory. Must be writable by | ||
| // the user blocky runs as (UID 100 in the container image), otherwise caching is | ||
| // silently skipped and downloads stay stateless. |
| "cachePath": { | ||
| "type": "string", | ||
| "description": "Directory for the on-disk download cache. When empty (default), downloads are\nfully stateless: nothing is written to disk and every source is downloaded in\nfull on every refresh. When set, blocky uses HTTP conditional requests and\nserves unchanged/unavailable sources from this directory." | ||
| "description": "Directory for the on-disk download cache. When empty (default), downloads are\nfully stateless: nothing is written to disk and every source is downloaded in\nfull on every refresh. When set, blocky uses HTTP conditional requests and\nserves unchanged/unavailable sources from this directory. Must be writable by\nthe user blocky runs as (UID 100 in the container image), otherwise caching is\nsilently skipped and downloads stay stateless." |
| "cachePath": { | ||
| "type": "string", | ||
| "description": "Directory for the on-disk download cache. When empty (default), downloads are\nfully stateless: nothing is written to disk and every source is downloaded in\nfull on every refresh. When set, blocky uses HTTP conditional requests and\nserves unchanged/unavailable sources from this directory." | ||
| "description": "Directory for the on-disk download cache. When empty (default), downloads are\nfully stateless: nothing is written to disk and every source is downloaded in\nfull on every refresh. When set, blocky uses HTTP conditional requests and\nserves unchanged/unavailable sources from this directory. Must be writable by\nthe user blocky runs as (UID 100 in the container image), otherwise caching is\nsilently skipped and downloads stay stateless." |
This PR contains the following updates: | Package | Update | Change | |---|---|---| | [ghcr.io/0xerr0r/blocky](https://github.com/0xERR0R/blocky) | minor | `v0.33.0` → `v0.34.0` | --- ### Release Notes <details> <summary>0xERR0R/blocky (ghcr.io/0xerr0r/blocky)</summary> ### [`v0.34.0`](https://github.com/0xERR0R/blocky/releases/tag/v0.34.0) [Compare Source](0xERR0R/blocky@v0.33.0...v0.34.0) #### Changelog ##### Bug fixes - [`91a8f44`](0xERR0R/blocky@91a8f44): fix(bootstrap): fall back to other resolved addresses when a dial fails ([#​2190](0xERR0R/blocky#2190)) ([@​maximilize](https://github.com/maximilize)) - [`0a20671`](0xERR0R/blocky@0a20671): fix(cache): set DO bit on prefetch reloads to keep DNSSEC-signed entry valid ([#​2158](0xERR0R/blocky#2158)) ([@​logingood](https://github.com/logingood)) - [`d55cd4f`](0xERR0R/blocky@d55cd4f): fix(docker): pre-create writable /app/cache and /logs owned by UID 100 ([#​2172](0xERR0R/blocky#2172)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`dc958ef`](0xERR0R/blocky@dc958ef): fix(lists): skip a lone "/" blocklist line instead of panicking ([#​2156](0xERR0R/blocky#2156)) ([@​maximilize](https://github.com/maximilize)) - [`1d450af`](0xERR0R/blocky@1d450af): fix(resolver): match custom DNS PTR queries case insensitively ([#​2191](0xERR0R/blocky#2191)) ([@​maximilize](https://github.com/maximilize)) - [`472bac9`](0xERR0R/blocky@472bac9): fix(stats): attribute filtered and non-FQDN queries to the client name ([#​2167](0xERR0R/blocky#2167)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`c07bd55`](0xERR0R/blocky@c07bd55): fix(stats): count only true blocks (BLOCKED) as blocked ([#​2161](0xERR0R/blocky#2161)) ([@​maximilize](https://github.com/maximilize)) - [`8435342`](0xERR0R/blocky@8435342): fix(stats): give DNS rebinding protection its own REBIND response type ([#​2166](0xERR0R/blocky#2166)) ([@​0xERR0R](https://github.com/0xERR0R)) ##### Build and dependencies - [`c163c75`](0xERR0R/blocky@c163c75): build(deps): bump actions/setup-go from 6 to 7 ([#​2168](0xERR0R/blocky#2168)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`958760d`](0xERR0R/blocky@958760d): build(deps): bump github.com/breml/rootcerts from 0.3.6 to 0.3.7 ([#​2181](0xERR0R/blocky#2181)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`d94652f`](0xERR0R/blocky@d94652f): build(deps): bump github.com/go-chi/chi/v5 from 5.3.0 to 5.3.1 ([#​2157](0xERR0R/blocky#2157)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`d784ac5`](0xERR0R/blocky@d784ac5): build(deps): bump github.com/jedib0t/go-pretty/v6 from 6.8.1 to 6.8.2 ([#​2154](0xERR0R/blocky#2154)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`23575a4`](0xERR0R/blocky@23575a4): build(deps): bump github.com/jedib0t/go-pretty/v6 from 6.8.2 to 6.8.3 ([#​2183](0xERR0R/blocky#2183)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`b1f8a72`](0xERR0R/blocky@b1f8a72): build(deps): bump github.com/oapi-codegen/runtime from 1.4.2 to 1.5.0 ([#​2164](0xERR0R/blocky#2164)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`4fda662`](0xERR0R/blocky@4fda662): build(deps): bump github.com/oapi-codegen/runtime from 1.5.0 to 1.6.0 ([#​2170](0xERR0R/blocky#2170)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`7c8aeb4`](0xERR0R/blocky@7c8aeb4): build(deps): bump github.com/pires/go-proxyproto from 0.12.0 to 0.13.0 ([#​2153](0xERR0R/blocky#2153)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`77c115b`](0xERR0R/blocky@77c115b): build(deps): bump github.com/pires/go-proxyproto from 0.13.0 to 0.14.0 ([#​2155](0xERR0R/blocky#2155)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`6154af2`](0xERR0R/blocky@6154af2): build(deps): bump github.com/pires/go-proxyproto from 0.14.0 to 0.15.0 ([#​2162](0xERR0R/blocky#2162)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`bd9e3ae`](0xERR0R/blocky@bd9e3ae): build(deps): bump github.com/prometheus/client\_golang from 1.23.2 to 1.24.0 ([#​2185](0xERR0R/blocky#2185)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`6fd79dd`](0xERR0R/blocky@6fd79dd): build(deps): bump github.com/prometheus/client\_golang from 1.24.0 to 1.24.1 ([#​2193](0xERR0R/blocky#2193)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`82777ca`](0xERR0R/blocky@82777ca): build(deps): bump github.com/quic-go/quic-go from 0.60.0 to 0.61.0 ([#​2192](0xERR0R/blocky#2192)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`59f0b0f`](0xERR0R/blocky@59f0b0f): build(deps): bump golang.org/x/net from 0.56.0 to 0.57.0 ([#​2159](0xERR0R/blocky#2159)) ([@​dependabot](https://github.com/dependabot)\[bot]) - [`288ff55`](0xERR0R/blocky@288ff55): build(deps): bump oapi-codegen to 2.7.1 and kin-openapi to 0.144.0 ([#​2195](0xERR0R/blocky#2195)) ([@​0xERR0R](https://github.com/0xERR0R)) ##### Misc - [`541860d`](0xERR0R/blocky@541860d): ci: automate sponsor list in README ([#​2173](0xERR0R/blocky#2173)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`493142d`](0xERR0R/blocky@493142d): ci: build sponsor avatar URLs from the login to avoid HTML escaping ([#​2177](0xERR0R/blocky#2177)) ([@​0xERR0R](https://github.com/0xERR0R)) - [`d9e2827`](0xERR0R/blocky@d9e2827): ci: fail the sponsors job instead of silently wiping the sponsor list ([#​2175](0xERR0R/blocky#2175)) ([@​0xERR0R](https://github.com/0xERR0R)) *** #### ❤️ Support Blocky Blocky is free and open source, developed in my spare time — no telemetry, no ads, no hidden filtering. If it's useful to you, please consider supporting its continued development: [GitHub Sponsors](https://github.com/sponsors/0xERR0R) · [thanks.dev](https://thanks.dev/u/gh/0xERR0R) · [Liberapay](https://liberapay.com/spx01) · [Ko-fi](https://ko-fi.com/0xerr0r) · [PayPal](https://paypal.me/spx01) Thank you to everyone supporting Blocky! 🙏 </details> --- ### Configuration 📅 **Schedule**: (UTC) - Branch creation - At any time (no schedule defined) - Automerge - At any time (no schedule defined) 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this PR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this PR, check this box --- This PR has been generated by [Mend Renovate](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My4yODEuMSIsInVwZGF0ZWRJblZlciI6IjQzLjI4MS4xIiwidGFyZ2V0QnJhbmNoIjoibWFpbiIsImxhYmVscyI6W119-->
Fixes #2165
Problem
The image runs as
USER 100on ascratchbase, so/app/blockywas the only path that existed. Docker only propagates ownership into a fresh named volume when the image already has content at the mount path — with nothing there, the volume is createdroot:root 0755and UID 100 cannot write to it.This made every writable mount fail, named volumes included. Verified by building the image before/after and running real containers:
cachePath→ fresh named volumecannot create temp cache file in /app/cache, serving without caching100queryLogs:/logsnamed volumecan't create/open file … permission denied2026-07-18_ALL.logwrittenchown/--user, now documented)Two things this surfaced beyond the original report:
cachePath. Our own advanced docker-compose example mountsqueryLogs:/logs, which has been silently dropping query logs.Change
Dockerfile— copy an empty dir into the final stage twice with--chown=100:100, seeding/app/cacheand/logs. A fresh named volume mounted over either path now inherits UID 100 ownership and is writable with no host-side setup.docs/installation.md— permission note under Run Blocky (UID 100; named volume vs. bind mount, the latter needingchown 100:100or a matching--user). Addsuid=100,gid=100to the CIFSdriver_opts, which mounts root-owned without it.docs/configuration.md— warning next tocachePathmaking clear this is degraded, not fatal: blocky keeps resolving and only the on-disk cache is skipped. The reporter's log line reads like a hard failure.config/config.go+ regenerateddocs/config.schema.json— same caveat on the field doc.Bind mounts still keep host ownership by design; that's documented rather than worked around.
Not included
The
cachePathdefault is left empty. Pointing it at/app/cachewould switch on-disk caching on by default — a behavior change beyond this issue. The docs recommend that path for containers instead.Verification
go build ./...,go test ./config/... ./lists/...— passmkdocs build --strict— passes; the new cross-page anchor resolvesmake generate-check— passes