catalog / docker.io/library/nextcloud

docker.io/library/nextcloud

validated  pin current  tags: 32

nextcloud — minimum Linux capabilities, derived by drop-test against the
default in-stack invocation (apache variant, MYSQL_* env against a mariadb
database, NEXTCLOUD_ADMIN_* auto-install). This profile trims the full
Docker default cap set 14 -> 6 — the same set as sibling wordpress, with
rsync-flavored evidence:

CHOWN + DAC_OVERRIDE + FOWNER — the entrypoint rsyncs ~700MB of sources
into the fresh /var/www/html VOLUME preserving www-data ownership,
permissions and times; any of the three dropped fails the copy
("rsync error: some files/attrs were not transferred (code 23)") and
the container exits.
SETGID + SETUID — the apache master/worker drop to www-data (SETUID
dropped = MPM fails AH02818; SETGID dropped = install never
completes, unixd alert).
NET_BIND_SERVICE — the :80 bind, ONLY under the pinned
net.ipv4.ip_unprivileged_port_start=1024 posture (docker defaults 0;
recorded structurally in run_config.sysctls — schema 1.4 — and in the
criteria doc).

Derived with the in-stack dependent-tier model (deps: the cataloged
mariadb:11.4). Trial lifecycle quirk encoded in the harness: a
fresh-docroot container cannot adopt an installed database — see
criteria/docker.io/library/nextcloud.md.
filesystem: read_only:true, tmpfs:[/var/run/apache2]. nextcloud (Apache+PHP)
rsyncs its source + stores config/data in /var/www/html, a declared VOLUME
(persistent, never tmpfs). Under a read-only rootfs the only rootfs write is
Apache's pid/lock dir /var/run/apache2; /var/log/apache2 and /tmp were
drop-tested and are NOT required. Derived by drop-test (in-stack mariadb dep).

Use it

services:
  nextcloud:
    image: docker.io/library/nextcloud:32
    cap_drop: [ALL]
    cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, NET_BIND_SERVICE, SETGID, SETUID]
    security_opt: ["no-new-privileges:true"]
    read_only: true
    tmpfs:
      - /var/run/apache2

This profile has multiple dimensions and is applied as a unit. Dimensions can interact — a capability can be required only because of a sibling read-only/tmpfs recommendation — so where they do, the minimum was derived under the sibling dimension's context. See the criteria doc and each dimension's recorded invocation below before applying them separately.

Dimension: capabilities

cap_drop: ALL + cap_add: CHOWN, DAC_OVERRIDE, FOWNER, NET_BIND_SERVICE, SETGID, SETUID

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/library/nextcloud@sha256:90fdc9f99fab…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/nextcloud.sh
workload sha256da8d4529fb8f…
ig versionv0.51.0

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
CHOWNrequiredcontainer exited: rsync error: some files/attrs were not transferred (code 23)
DAC_OVERRIDErequiredcontainer exited: rsync error: some files/attrs were not transferred (code 23)
FSETIDremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
FOWNERrequiredcontainer exited: rsync error: some files/attrs were not transferred (code 23)
MKNODremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
NET_RAWremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
SETGIDrequiredinstall never completed after DB reset: [unixd:alert] setgid failure
SETUIDrequiredcontainer exited during install: [:emerg] AH02818: MPM run failed
SETFCAPremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
SETPCAPremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
NET_BIND_SERVICErequiredcontainer exited during install: AH00015: Unable to open logs
SYS_CHROOTremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
KILLremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
AUDIT_WRITEremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)

Recorded invocation (run_config)

The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.

security_optno-new-privileges:true
sysctlsnet.ipv4.ip_unprivileged_port_start=1024
envMYSQL_HOST
MYSQL_DATABASE
MYSQL_USER
MYSQL_PASSWORD
NEXTCLOUD_ADMIN_USER
NEXTCLOUD_ADMIN_PASSWORD
NEXTCLOUD_TRUSTED_DOMAINS
Workload script (profiles/workloads/nextcloud.sh)
#!/usr/bin/env bash
# Workload exerciser for the nextcloud reference image (apache variant,
# in-stack against a mariadb database per MYSQL_* env, admin auto-installed
# via NEXTCLOUD_ADMIN_*).
#
# Real function: wait for `occ status` to report installed, then a WebDAV
# PUT/GET round-trip as the admin — a real file write into nextcloud's data
# dir through the full apache -> php -> storage path — asserting the payload
# reads back byte-identical.
#
# Probe hygiene: occ runs as --user www-data (a root probe pollutes a caps
# minimum); WebDAV goes through a curl sidecar; responses are captured then
# matched (never `curl | grep -q` under pipefail — SIGPIPE false-negatives).
# Derivation-time only: a fresh-docroot container cannot adopt an installed
# database (it boots "installed: false"); the csd drop-test correctness check
# resets the dep database and restarts the target — see the criteria doc.
#
# Required env: NEXTCLOUDCONTAINER (target container name or id).
set -euo pipefail

: "${NEXTCLOUDCONTAINER:?NEXTCLOUDCONTAINER must be set}"
C="${NEXTCLOUDCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }

deadline=$((SECONDS + 180))
until docker exec --user www-data "$C" php occ status 2>/dev/null | grep -q "installed: true"; do
    (( SECONDS >= deadline )) && { echo "nextcloud never reported installed (fresh-docroot + installed DB needs a DB reset — see criteria)" >&2; exit 1; }
    sleep 5
done

sc -sf --max-time 15 -u csdadmin:CsdProbe-Pw-12345 -X PUT \
    --data-binary "csd-payload-42" \
    http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt >/dev/null
got="$(sc -s --max-time 15 -u csdadmin:CsdProbe-Pw-12345 \
    http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt 2>/dev/null)"
if [ "$got" != "csd-payload-42" ]; then
    echo "WebDAV round-trip mismatch (got: ${got:0:60})" >&2
    exit 1
fi

Dimension: filesystem

read_only: true + tmpfs: /var/run/apache2

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/library/nextcloud@sha256:90fdc9f99fab…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/nextcloud.sh
workload sha256da8d4529fb8f…
ig versionv0.51.0

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
/var/run/apache2requiredcontainer exited during install: AH00558: apache2: Could not reliably determine the server's fully qualified d
/var/log/apache2removablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)
/tmpremovablecorrect: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)

Recorded invocation (run_config)

The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.

envMYSQL_HOST
MYSQL_DATABASE
MYSQL_USER
MYSQL_PASSWORD
NEXTCLOUD_ADMIN_USER
NEXTCLOUD_ADMIN_PASSWORD
NEXTCLOUD_TRUSTED_DOMAINS
Workload script (profiles/workloads/nextcloud.sh)
#!/usr/bin/env bash
# Workload exerciser for the nextcloud reference image (apache variant,
# in-stack against a mariadb database per MYSQL_* env, admin auto-installed
# via NEXTCLOUD_ADMIN_*).
#
# Real function: wait for `occ status` to report installed, then a WebDAV
# PUT/GET round-trip as the admin — a real file write into nextcloud's data
# dir through the full apache -> php -> storage path — asserting the payload
# reads back byte-identical.
#
# Probe hygiene: occ runs as --user www-data (a root probe pollutes a caps
# minimum); WebDAV goes through a curl sidecar; responses are captured then
# matched (never `curl | grep -q` under pipefail — SIGPIPE false-negatives).
# Derivation-time only: a fresh-docroot container cannot adopt an installed
# database (it boots "installed: false"); the csd drop-test correctness check
# resets the dep database and restarts the target — see the criteria doc.
#
# Required env: NEXTCLOUDCONTAINER (target container name or id).
set -euo pipefail

: "${NEXTCLOUDCONTAINER:?NEXTCLOUDCONTAINER must be set}"
C="${NEXTCLOUDCONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }

deadline=$((SECONDS + 180))
until docker exec --user www-data "$C" php occ status 2>/dev/null | grep -q "installed: true"; do
    (( SECONDS >= deadline )) && { echo "nextcloud never reported installed (fresh-docroot + installed DB needs a DB reset — see criteria)" >&2; exit 1; }
    sleep 5
done

sc -sf --max-time 15 -u csdadmin:CsdProbe-Pw-12345 -X PUT \
    --data-binary "csd-payload-42" \
    http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt >/dev/null
got="$(sc -s --max-time 15 -u csdadmin:CsdProbe-Pw-12345 \
    http://localhost:80/remote.php/dav/files/csdadmin/csd-probe.txt 2>/dev/null)"
if [ "$got" != "csd-payload-42" ]; then
    echo "WebDAV round-trip mismatch (got: ${got:0:60})" >&2
    exit 1
fi

Evidence & provenance

Raw profile YAML
# nextcloud — minimum Linux capabilities, derived by drop-test against the
# default in-stack invocation (apache variant, MYSQL_* env against a mariadb
# database, NEXTCLOUD_ADMIN_* auto-install). This profile trims the full
# Docker default cap set 14 -> 6 — the same set as sibling wordpress, with
# rsync-flavored evidence:
#
#   CHOWN + DAC_OVERRIDE + FOWNER — the entrypoint rsyncs ~700MB of sources
#       into the fresh /var/www/html VOLUME preserving www-data ownership,
#       permissions and times; any of the three dropped fails the copy
#       ("rsync error: some files/attrs were not transferred (code 23)") and
#       the container exits.
#   SETGID + SETUID — the apache master/worker drop to www-data (SETUID
#       dropped = MPM fails AH02818; SETGID dropped = install never
#       completes, unixd alert).
#   NET_BIND_SERVICE — the :80 bind, ONLY under the pinned
#       net.ipv4.ip_unprivileged_port_start=1024 posture (docker defaults 0;
#       recorded structurally in run_config.sysctls — schema 1.4 — and in the
#       criteria doc).
#
# Derived with the in-stack dependent-tier model (deps: the cataloged
# mariadb:11.4). Trial lifecycle quirk encoded in the harness: a
# fresh-docroot container cannot adopt an installed database — see
# criteria/docker.io/library/nextcloud.md.
# filesystem: read_only:true, tmpfs:[/var/run/apache2]. nextcloud (Apache+PHP)
# rsyncs its source + stores config/data in /var/www/html, a declared VOLUME
# (persistent, never tmpfs). Under a read-only rootfs the only rootfs write is
# Apache's pid/lock dir /var/run/apache2; /var/log/apache2 and /tmp were
# drop-tested and are NOT required. Derived by drop-test (in-stack mariadb dep).
#
schema_version: "1.5"
image: docker.io/library/nextcloud
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/library/nextcloud.html
applies_to:
  tags: ["32"]
status: validated
dimensions:
  capabilities:
    cap_drop: [ALL]
    cap_add: [CHOWN, DAC_OVERRIDE, FOWNER, NET_BIND_SERVICE, SETGID, SETUID]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/library/nextcloud@sha256:90fdc9f99fab6f8dcfb16f6010a2a615775f01ed8e9265fa10beed6b2f10cbae
      validated_date: "2026-07-16"
      duration_seconds: 900
      confidence: high
      workload: profiles/workloads/nextcloud.sh
      workload_sha256: "da8d4529fb8f89576cd324840cac3f62ea71e3284bea62d722c68287f3623ba9"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: []
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: ["no-new-privileges:true"]
        sysctls: ["net.ipv4.ip_unprivileged_port_start=1024"]
        mounts: []
        env: [MYSQL_HOST, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD,
              NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD, NEXTCLOUD_TRUSTED_DOMAINS]
      drop_test:
        checks:
          - {removed: CHOWN, required: true, observed: "container exited: rsync error: some files/attrs were not transferred (code 23)"}
          - {removed: DAC_OVERRIDE, required: true, observed: "container exited: rsync error: some files/attrs were not transferred (code 23)"}
          - {removed: FSETID, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: FOWNER, required: true, observed: "container exited: rsync error: some files/attrs were not transferred (code 23)"}
          - {removed: MKNOD, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: NET_RAW, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: SETGID, required: true, observed: "install never completed after DB reset: [unixd:alert] setgid failure"}
          - {removed: SETUID, required: true, observed: "container exited during install: [:emerg] AH02818: MPM run failed"}
          - {removed: SETFCAP, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: SETPCAP, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: NET_BIND_SERVICE, required: true, observed: "container exited during install: AH00015: Unable to open logs"}
          - {removed: SYS_CHROOT, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: KILL, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: AUDIT_WRITE, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
  filesystem:
    read_only: true
    tmpfs: [/var/run/apache2]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/library/nextcloud@sha256:90fdc9f99fab6f8dcfb16f6010a2a615775f01ed8e9265fa10beed6b2f10cbae
      validated_date: "2026-07-16"
      duration_seconds: 77
      confidence: high
      workload: profiles/workloads/nextcloud.sh
      workload_sha256: "da8d4529fb8f89576cd324840cac3f62ea71e3284bea62d722c68287f3623ba9"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: []
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: []
        mounts: []
        env: [MYSQL_HOST, MYSQL_DATABASE, MYSQL_USER, MYSQL_PASSWORD, NEXTCLOUD_ADMIN_USER, NEXTCLOUD_ADMIN_PASSWORD, NEXTCLOUD_TRUSTED_DOMAINS]
      drop_test:
        checks:
          - {removed: /var/run/apache2, required: true, observed: "container exited during install: AH00558: apache2: Could not reliably determine the server's fully qualified d"}
          - {removed: /var/log/apache2, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}
          - {removed: /tmp, required: false, observed: "correct: WebDAV PUT/GET round-trip ok, worker uid=33 (non-root)"}