Skip to content

fix(docker): pre-create writable /app/cache and /logs owned by UID 100 - #2172

Merged
0xERR0R merged 2 commits into
mainfrom
docs/container-mount-permissions
Jul 18, 2026
Merged

0xERR0R merged 2 commits into
mainfrom
docs/container-mount-permissions

Conversation

@0xERR0R

@0xERR0R 0xERR0R commented Jul 18, 2026

Copy link
Copy Markdown
Owner

Fixes #2165

Problem

The image runs as USER 100 on a scratch base, so /app/blocky was 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 created root:root 0755 and 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:

Scenario before after
cachePath → fresh named volume ❌ cannot create temp cache file in /app/cache, serving without caching ✅ cache file written, owned by 100
query log → queryLogs:/logs named volume ❌ can't create/open file … permission denied ✅ 2026-07-18_ALL.log written
bind mount, host-owned dir ❌ denied ❌ denied (expected — needs chown/--user, now documented)

Two things this surfaced beyond the original report:

  1. Named volumes were never a workaround for cachePath permission denied when using Docker bind mounts #2165. The control run reproduces the reporter's exact error with a named volume, so that suggestion wouldn't have helped.
  2. The query-log breakage predates cachePath. Our own advanced docker-compose example mounts queryLogs:/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/cache and /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 needing chown 100:100 or a matching --user). Adds uid=100,gid=100 to the CIFS driver_opts, which mounts root-owned without it.
  • docs/configuration.md — warning next to cachePath making 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 + regenerated docs/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 cachePath default is left empty. Pointing it at /app/cache would 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/... — pass
  • mkdocs build --strict — passes; the new cross-page anchor resolves
  • make generate-check — passes
  • Manual end-to-end: both images built and run against a local list server and live DNS queries, results in the table above

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
Copilot AI review requested due to automatic review settings July 18, 2026 20:19
@0xERR0R 0xERR0R added 🐞 bug Something isn't working 📙 documentation Improvements or additions to documentation labels Jul 18, 2026
@0xERR0R 0xERR0R added this to the v0.34.0 milestone Jul 18, 2026
@codecov

codecov Bot commented Jul 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 87.94%. Comparing base (0a20671) to head (0b36711).
⚠️ Report is 1 commits behind head on main.

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@0xERR0R
0xERR0R enabled auto-merge (squash) July 18, 2026 20:21

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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/cache and /logs in the final Docker image with ownership 100:100 to 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.cachePath degrades 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.

Comment thread docs/installation.md
Comment on lines +93 to +95
- run as the host user that owns the directory:
`docker run -u "$(id -u):$(id -g)" ...`, or in compose
`user: "1000:1000"`.
Comment thread config/config.go
Comment on lines +626 to +628
// 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.
Comment thread docs/config.schema.json
"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."
Comment thread docs/config.schema.json
"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."
@0xERR0R
0xERR0R merged commit d55cd4f into main Jul 18, 2026
23 checks passed
@0xERR0R
0xERR0R deleted the docs/container-mount-permissions branch July 18, 2026 20:26
dadezzz pushed a commit to dadezzz/infra_docker-compose that referenced this pull request Jul 28, 2026
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 ([#&#8203;2190](0xERR0R/blocky#2190)) ([@&#8203;maximilize](https://github.com/maximilize))
- [`0a20671`](0xERR0R/blocky@0a20671): fix(cache): set DO bit on prefetch reloads to keep DNSSEC-signed entry valid ([#&#8203;2158](0xERR0R/blocky#2158)) ([@&#8203;logingood](https://github.com/logingood))
- [`d55cd4f`](0xERR0R/blocky@d55cd4f): fix(docker): pre-create writable /app/cache and /logs owned by UID 100 ([#&#8203;2172](0xERR0R/blocky#2172)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`dc958ef`](0xERR0R/blocky@dc958ef): fix(lists): skip a lone "/" blocklist line instead of panicking ([#&#8203;2156](0xERR0R/blocky#2156)) ([@&#8203;maximilize](https://github.com/maximilize))
- [`1d450af`](0xERR0R/blocky@1d450af): fix(resolver): match custom DNS PTR queries case insensitively ([#&#8203;2191](0xERR0R/blocky#2191)) ([@&#8203;maximilize](https://github.com/maximilize))
- [`472bac9`](0xERR0R/blocky@472bac9): fix(stats): attribute filtered and non-FQDN queries to the client name ([#&#8203;2167](0xERR0R/blocky#2167)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`c07bd55`](0xERR0R/blocky@c07bd55): fix(stats): count only true blocks (BLOCKED) as blocked ([#&#8203;2161](0xERR0R/blocky#2161)) ([@&#8203;maximilize](https://github.com/maximilize))
- [`8435342`](0xERR0R/blocky@8435342): fix(stats): give DNS rebinding protection its own REBIND response type ([#&#8203;2166](0xERR0R/blocky#2166)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Build and dependencies

- [`c163c75`](0xERR0R/blocky@c163c75): build(deps): bump actions/setup-go from 6 to 7 ([#&#8203;2168](0xERR0R/blocky#2168)) ([@&#8203;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 ([#&#8203;2181](0xERR0R/blocky#2181)) ([@&#8203;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 ([#&#8203;2157](0xERR0R/blocky#2157)) ([@&#8203;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 ([#&#8203;2154](0xERR0R/blocky#2154)) ([@&#8203;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 ([#&#8203;2183](0xERR0R/blocky#2183)) ([@&#8203;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 ([#&#8203;2164](0xERR0R/blocky#2164)) ([@&#8203;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 ([#&#8203;2170](0xERR0R/blocky#2170)) ([@&#8203;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 ([#&#8203;2153](0xERR0R/blocky#2153)) ([@&#8203;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 ([#&#8203;2155](0xERR0R/blocky#2155)) ([@&#8203;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 ([#&#8203;2162](0xERR0R/blocky#2162)) ([@&#8203;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 ([#&#8203;2185](0xERR0R/blocky#2185)) ([@&#8203;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 ([#&#8203;2193](0xERR0R/blocky#2193)) ([@&#8203;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 ([#&#8203;2192](0xERR0R/blocky#2192)) ([@&#8203;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 ([#&#8203;2159](0xERR0R/blocky#2159)) ([@&#8203;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 ([#&#8203;2195](0xERR0R/blocky#2195)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))

##### Misc

- [`541860d`](0xERR0R/blocky@541860d): ci: automate sponsor list in README ([#&#8203;2173](0xERR0R/blocky#2173)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`493142d`](0xERR0R/blocky@493142d): ci: build sponsor avatar URLs from the login to avoid HTML escaping ([#&#8203;2177](0xERR0R/blocky#2177)) ([@&#8203;0xERR0R](https://github.com/0xERR0R))
- [`d9e2827`](0xERR0R/blocky@d9e2827): ci: fail the sponsors job instead of silently wiping the sponsor list ([#&#8203;2175](0xERR0R/blocky#2175)) ([@&#8203;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-->
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐞 bug Something isn't working 📙 documentation Improvements or additions to documentation

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cachePath permission denied when using Docker bind mounts

2 participants