Commit Graph

8 Commits

Author SHA1 Message Date
dependabot[bot] d26d649cb5 chore(docker)(deps): bump alpine (#728)
Bumps the docker-minor-and-patch group with 1 update in the / directory: alpine.


Updates `alpine` from 3.23 to 3.24

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.24'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-07-11 01:36:11 -04:00
dependabot[bot] 1c86165b77 chore(docker)(deps): bump alpine (#248)
Bumps the docker-minor-and-patch group with 1 update in the / directory: alpine.


Updates `alpine` from 3.21 to 3.23

---
updated-dependencies:
- dependency-name: alpine
  dependency-version: '3.23'
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: docker-minor-and-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-05-07 23:45:40 -04:00
xarmian 92a4931f44 feat(docker): PUID/PGID entrypoint shim for Unraid + LinuxServer-style hosts (TASK-1168) (#425)
Tiny /bin/sh entrypoint shim that, if invoked as root, reads PUID/PGID
env vars (defaulting to 99/100 — Unraid's nobody:users), remaps the
in-image pad user, chowns /data, and execs the binary via su-exec.
If invoked as non-root (caller passed --user), it just execs directly
— caller knows what they want.

Solves the classic Unraid appdata-ownership-mismatch first-run failure
where the in-image pad user (uid 1000) couldn't write to a host volume
owned by nobody:users (uid 99, gid 100). Reusable on Synology / QNAP /
TrueNAS where the host's appdata user is similarly non-1000.

Behavior changes:
- Container starts as root (USER directive removed). Entrypoint drops
  privileges via su-exec before exec'ing pad — standard PUID/PGID
  pattern. Healthcheck adapts: root → su-exec to pad; non-root →
  direct wget.
- chown -R is always-run (warn-and-continue on per-file failures). A
  shallow stat-only check would silently break pad on a restored
  backup with mixed-ownership inner files.
- Healthcheck start-period bumped 10s → 60s to absorb slow chown -R
  on large attachment stores.
- Compose default 1000/1000 for backward compat with existing deploys
  whose volumes were created under the previous USER pad image.
- Raw `docker run` defaults to 99/100 (Unraid convention).

Validation rejects PUID=0 / PGID=0 (would defeat the unprivileged-user
invariant), empty values, and non-numeric values with clear errors.

Goes through 11 rounds of codex pre-implementation design review,
catching:
- gid bug where groupmod alone leaves /etc/passwd's primary-gid stale
- compose $-interpolation gotcha (needs $$( ) not $())
- getent missing from default alpine BusyBox
- shell ${VAR:-} silently masking explicit empty values
- healthcheck running as root after USER drop
- su-exec failing for --user non-root pass-through

Part of PLAN-1166 (Pad on Unraid — Community Apps launch). Unblocks
TASK-1169 (XML template authoring).
2026-05-06 08:40:33 -04:00
xarmian e57da62917 chore: modernize goreleaser config + wire homebrew-tap token (#256)
* chore: wire HOMEBREW_TAP_GITHUB_TOKEN into release pipeline

The brews block in .goreleaser.yaml targets the separate xarmian/homebrew-tap
repo. Without a token override, goreleaser falls back to the workflow's
GITHUB_TOKEN — which is scoped to xarmian/pad only and cannot push to the
tap repo. At first real tag time the brew publish step would fail with a
permission error.

- Add `repository.token` to the brews block, referencing
  `{{ .Env.HOMEBREW_TAP_GITHUB_TOKEN }}`
- Export `HOMEBREW_TAP_GITHUB_TOKEN` from the workflow `secrets` into the
  goreleaser step env, alongside the existing `GITHUB_TOKEN`
- Comment both edits with the rationale + the fine-grained PAT permissions
  the secret needs (`Contents: write` + `Metadata: read` on the tap repo)

The secret itself is created on the human side (HT-780). Snapshot mode
skips publishing so this isn't testable locally — the gate is HT-782's
v0.0.1-rc.1 dress rehearsal.

Refs: TASK-806, TASK-778 (audit), HT-780 (operator step)

* chore: migrate goreleaser deprecations (archives.formats, dockers_v2, homebrew_casks)

Three v2 deprecations were flagged by `goreleaser check` while wiring the
tap token. Migrating them now (instead of filing tech debt) because we're
already touching the file and these are part of the same release pipeline
that ships at v0.1.0 — no point landing a "wire the token" commit that
still trips deprecation warnings on the very next CI run.

Changes:

- archives: `format: tar.gz` + `format_overrides[].format: zip` →
  `formats: ["tar.gz"]` + `format_overrides[].formats: ["zip"]`
  (single-string is still accepted but the list form is the new spec)

- dockers + docker_manifests → dockers_v2:
  Single block with `images:` + `tags:` + `platforms:` collapses the
  prior per-architecture builds plus separate manifest declarations into
  one declaration. buildx + multi-platform are implicit. Snapshot
  validates: amd64 + arm64 images both build, manifest list assembled,
  binary runs inside the cross-built image.

  goreleaser flags dockers_v2 as "experimental and subject to change" —
  it's the documented forward path for v2 and the project is already
  pinned to `version: "~> v2"`, so we're committed to the roadmap.

- Dockerfile.goreleaser: add `ARG TARGETPLATFORM` and update the COPY to
  `${TARGETPLATFORM}/pad`. dockers_v2 organizes pre-built binaries under
  `linux/amd64/pad`, `linux/arm64/pad` etc; buildx populates
  TARGETPLATFORM per platform during the build.

- brews → homebrew_casks: `directory: Formula` → `directory: Casks`.
  The `brews` keyword is fully phased out in v2.10+; goreleaser's
  homebrew_casks now natively handles pre-compiled binaries (which used
  to require workarounds with the old brews block). End-user UX is
  unchanged: `brew install xarmian/tap/pad` works identically because
  modern Homebrew auto-detects whether a tap entry is a formula or a
  cask. Removed the no-op `test:` stanza that doesn't apply to casks.

Validation: `goreleaser check` clean (zero warnings), `goreleaser
release --snapshot --clean --skip=publish,sign,sbom` builds all six
binaries, six archives, one cask, two cross-platform docker images.
`docker run --rm ghcr.io/xarmian/pad:latest-amd64 --version` returns
the snapshot version as expected.

Refs: TASK-806
2026-04-26 16:38:45 -04:00
xarmian be9a96ba21 chore: run containers as non-root + harden K8s securityContext (TASK-680) (#219)
First thing external reviewers flag on a public repo: "why does this
image run as root?". Fixes both Dockerfiles and the K8s deployment.

Dockerfiles (Dockerfile + Dockerfile.goreleaser):
- Add a non-login uid:1000 "pad" user via adduser
- chown /data so the app can write its SQLite DB as the unprivileged user
- Declare USER pad before the ENTRYPOINT

deploy/k8s/deployment.yaml:
- Pod-level securityContext: runAsNonRoot, runAsUser/Group 1000, fsGroup
  1000 (so the emptyDir volume is group-writable), seccompProfile
  RuntimeDefault
- Container-level securityContext: allowPrivilegeEscalation false,
  readOnlyRootFilesystem true, drop ALL capabilities

Verified:
- docker build succeeds; `docker inspect ... Config.User` = "pad"
- Container running as uid 1000 serves /api/v1/health successfully
- Container runs with --read-only rootfs + writable /data volume with no
  runtime errors (server only writes to /data, never /tmp)
- deploy/k8s/deployment.yaml parses via yq; securityContext block
  structurally correct

Parent: PLAN-644.
2026-04-22 20:23:14 -04:00
xarmian 2b413f5d95 chore: add HEALTHCHECK to both Dockerfiles (TASK-681) (#218)
Before: neither Dockerfile declared HEALTHCHECK. docker-compose.yml
added it at compose level, so "docker run ghcr.io/xarmian/pad:latest"
had no health signal for Docker/Kubernetes/Swarm.

Adds HEALTHCHECK to Dockerfile and Dockerfile.goreleaser probing
GET /api/v1/health (served by internal/server/server.go:353). Uses
wget from busybox (already present in alpine:3.21) so no extra
apk install is needed. Interval 30s / timeout 5s / start-period 10s
/ retries 3 — conservative defaults safe for low-traffic single-user
instances.

Verified the built image reports the expected HEALTHCHECK via
`docker inspect` and that the probe succeeds against a live container.

Parent: PLAN-644.
2026-04-22 20:13:53 -04:00
xarmian 0495098254 fix(docker): correct goreleaser image CMD (TASK-671) (#196)
Dockerfile.goreleaser had CMD ["serve"] but the pad binary exposes
its HTTP server under `pad server start` (no "serve" command exists).
As a result, `docker run ghcr.io/xarmian/pad:latest` exited immediately
with "unknown command \"serve\"". Align with Dockerfile which already
uses CMD ["server", "start"].

Parent: PLAN-644.
2026-04-22 14:11:35 -04:00
xarmian a2e5a6622d Add Docker support and PAD_DATA_DIR config
- Multi-stage Dockerfile for building from source
- Dockerfile.goreleaser for GoReleaser-built release images
- GoReleaser config: publish multi-arch Docker images to ghcr.io
- Add PAD_DATA_DIR environment variable for configuring data directory
- Update README with Docker install option
- Add .dockerignore
2026-03-28 04:34:17 +00:00