Sign inSign up

swissmakers/nextcloud

By swissmakers

•Updated about 4 hours ago

This image packages Nextcloud with Swissmakers configuration presets in a hardened container.

Image
Security
Web servers
Databases & storage
0

264

swissmakers/nextcloud repository overview

⁠Swissmakers Nextcloud (Rocky Linux 9/ 10)

Maintainer: Swissmakers GmbH Base: Rocky Linux 10 (:latest, :rocky10); the same image on Rocky Linux 9 as :rocky9 for CPUs without x86-64-v3 Services in container: Apache 2.4 + PHP-FPM, Valkey (local Redis-compatible cache), Client Push, internal cron worker Features: SMB (Samba) support, Redis caching, APCu, Imagick, sodium, MariaDB/PostgreSQL ready.

This image packages Nextcloud with the Swissmakers configuration presets and hardened container deployment. It follows the official Nextcloud system requirements for the latest stable release (first installations download the latest stable Nextcloud release). See the [Nextcloud documentation][1] for details.


Rocky Linux 10 requires an x86-64-v3 CPU on amd64 hosts, including the CPU features exposed to a VM. Check customer hardware before replacing a Rocky 9 image; see Rocky's release notes⁠. Hosts without x86-64-v3 use the :rocky9 variant: the same sources, features and tests on a Rocky Linux 9 base (x86-64-v2), with ImageMagick 6 instead of 7. Runtime testing here uses amd64. Client Push selects the bundled x86_64/aarch64 binary.

⁠Migrating from the Rocky 9 image

Run the preflight check on every customer host before switching the image. It checks the CPU level, whether the new image accepts the running container's environment and its config.php (the same checks a start performs, without network or volumes; passwords are not passed as environment, and config.php is piped into the throw-away check container, never written on the host), and whether anything in the code volume would block Nextcloud's updater:

deployment/preflight.sh --image registry.swissmakers.ch/infra/nextcloud:latest --container <nextcloud-container>
# or, before a container exists:
deployment/preflight.sh --env-file customer.env
# the image itself: podman run --rm --env-file customer.env <image> preflight

What changes for an existing instance:

  • CPU: see the x86-64-v3 note above. The Rocky 10 image does not start at all on older CPUs or VMs with a generic CPU model; the preflight check reports this and points to :rocky9.
  • Client Push (NC_ENABLE_PUSH=true) is the default: existing instances get the notify_push app on the next start. The front proxy must pass WebSocket upgrades for /push/ws (Apache⁠: ProxyPass ... upgrade=websocket; nginx⁠). If it drops them, the container does not advertise push and logs what to fix; clients keep polling as before. ENABLE_REDIS=false disables push automatically. A failed push setup never stops Nextcloud; set NC_ENABLE_PUSH=false to opt out.
  • Environment values keep the Rocky 9 meaning in the default merge mode: empty variables (for example NC_OVERWRITE_PROTOCOL=) are ignored instead of clearing stored settings; proxy host names or other invalid entries in NC_TRUSTED_PROXIES/NC_ALLOWED_ADMIN_RANGES are dropped with a warning; NC_OVERWRITE_CLI_URL without http(s):// is ignored with a warning; True, yes, 1, on (and False, no, 0, off) are accepted for boolean settings; NC_OVERWRITE_PROTOCOL and the URL scheme are case-insensitive; postgres/postgresql/mariadb are accepted for NC_DB_TYPE. Values that cannot be interpreted safely (for example an invalid port) still stop the start with a clear message.
  • Built-in default passwords are gone: auto-install needs explicit NC_DB_PASSWORD / NC_ADMIN_PASSWORD (or *_FILE). Installed instances are not affected.
  • Client addresses: Apache no longer uses mod_remoteip; Nextcloud resolves the client address from trusted_proxies itself. The access log still starts with the client address (see logging⁠), so fail2ban filters keep working.
  • Local cache: Valkey (Redis compatible) instead of Redis. Stored redis settings in config.php are kept. ENABLE_REDIS=false removes only the settings of the image's own local cache and leaves an administrator-managed external Redis alone.
  • Marker files that the Rocky 9 image created as root in config/ are handed to the web user on start, because Nextcloud's updater refuses to run otherwise.
  • The health check stays healthy during web setup, maintenance mode and pending upgrades.
  • A config.php with config_is_read_only is no longer a crash loop, even when the file itself is read-only: environment settings are then not applied and a warning is logged.
  • Values already stored in config.php never stop a start: lists stored as a plain string (occ config:system:set trusted_proxies --value=... without an index), a Redis timeout of 0 (Nextcloud's sample value), an upper-case overwriteprotocol and a configfilemode set without --type=integer are accepted, normalized or left alone with a warning. Only invalid environment values are rejected.
  • Forwarded headers: with trusted proxies, HTTP_X_FORWARDED_FOR is added to forwarded_for_headers with the lowest priority, so a header you chose (for example HTTP_X_REAL_IP) keeps precedence.

Rollback: the previous image stays available by digest (registry.swissmakers.ch/infra/nextcloud@sha256:23494051dd1d5bc6cc9da9951584b7e832215c4dbd40174728a876f422d8a465). Before starting it on the same volumes, run occ app:disable notify_push, because the Rocky 9 image does not start the push daemon.

⁠Available Images

The image is built automatically by Gitea CI and published to the Swissmakers Harbor registry and here on Docker Hub. Every tested push to main publishes both variants:

TagBaseCPU (amd64)
latest, rocky10, rocky10-<commit>Rocky Linux 10x86-64-v3
rocky9, rocky9-<commit>Rocky Linux 9x86-64-v2

latest always follows the Rocky 10 build. Pin rocky9 on hosts whose CPU (or VM CPU model) lacks x86-64-v3, and a -<commit> tag to stay on one exact build. The same tags are mirrored to docker.io/swissmakers/nextcloud and ghcr.io/swissmakers/nextcloud.

podman pull registry.swissmakers.ch/infra/nextcloud:latest
# or
docker pull registry.swissmakers.ch/infra/nextcloud:latest

Use this image name in your Compose files and manual runs:

registry.swissmakers.ch/infra/nextcloud:latest

⁠Key Features

  • Rocky Linux 10 hardened base (Rocky Linux 9 variant :rocky9)

  • PHP 8.4 from the Remi module (php:remi-8.4), supported by current Nextcloud releases

  • Nextcloud code is downloaded on first run from https://download.nextcloud.com/server/releases/latest.tar.bz2, with required SHA-256 and publisher-signature verification. Existing customer code volumes are never replaced by the image.

  • Apache 2.4 + PHP-FPM with a secure default vhost, listening on port 3000 (the stock port-80 vhost and welcome page are removed)

  • Valkey, Rocky 10’s Redis-compatible server, running inside the container over a Unix socket as apache (external Redis is also supported)

  • Client Push (notify_push) enabled by default, with a supervised daemon and Apache HTTP/WebSocket routing under /push/

  • APCu for local caching (CLI enabled as well, for cron and occ)

  • Imagick and Ghostscript for previews (PDF, PS/EPS and images), with document coders limited to reading and image resource limits

  • SMB / CIFS support via libsmbclient + php-smbclient

  • Sodium PHP extension for Argon2 password hashing

  • Internal cron worker runs cron.php every 5 minutes (no cron daemon needed)

  • MariaDB and PostgreSQL support (no DB in this container)

  • Nextcloud, PHP-FPM workers and Redis run as the apache user (the entrypoint itself runs as root and drops privileges per process)

  • Apache logs to stdout/stderr (Podman/Docker logging compatible); the Nextcloud application log lives in the data directory (nextcloud.log). Access log lines use the combined layout and start with the client address, as in the Rocky 9 image, so fail2ban filters keep working: for a request from a proxy listed in NC_TRUSTED_PROXIES it is the last X-Forwarded-For entry (the one that proxy added), followed by proxy=<address> at the end of the line; any other peer is logged with its socket address and cannot spoof the field. With several proxy hops, list only the proxy directly in front of the container. Proxies added to config.php by hand are not used for the log.

  • Container HEALTHCHECK verifies that Apache, PHP-FPM and Nextcloud answer status.php, and in normal operation that the post-install worker has finished. An unfinished web installer, maintenance mode and a pending occ upgrade are reported healthy on purpose, so proxies keep routing to the installer/updater and auto-heal tooling never kills a running upgrade. Failures of Apache, PHP-FPM, the local cache or the required background/cron worker stop the container; push and repair failures do not.

  • One-time "Swissmakers post-install hardening":

    • Applies sane defaults (locale, timezone, default app)
    • Enables APCu + Redis caching and file locking
    • Regenerates .htaccess and adds missing DB indices
    • Optionally installs/enables or disables apps by default
  • Startup sync for network settings (every start after install, merge semantics):

    • Ensures all values from NC_TRUSTED_DOMAINS, NC_TRUSTED_PROXIES and NC_ALLOWED_ADMIN_RANGES are present in config.php
    • Never removes entries you added manually via occ or the admin UI
    • Applies overwrite.* settings from reverse-proxy env vars
    • Leaves proxy validation to Nextcloud using the original socket peer address
    • Uses relative DAV redirects to retain the public scheme/hostname/port
    • Generates HSTS for explicitly configured HTTPS deployments

⁠Directory Layout & Volumes

Inside the container:

  • Web root (code + apps + config) /var/www/nextcloud (env: NC_WEBROOT)

  • Config directory Defaults to: /var/www/nextcloud/config (env: NC_CONFIG_DIR, optional)

  • Data directory /var/ncdata (env: NC_DATA_DIR)

Recommended (named) volumes:

volumes:
  nc_html:   # Nextcloud code, apps, config
  nc_data:   # Nextcloud data

Mounted in the container:

  • /var/www/nextcloud ← nc_html
  • /var/ncdata ← nc_data

Important: For best security, keep data on a dedicated volume or storage backend and back it up regularly, together with config.php and the database.

When using Podman with SELinux, use :Z:

- nc_html:/var/www/nextcloud:Z
- nc_data:/var/ncdata:Z

⁠Exposed Ports

  • 3000/tcp – HTTP (TLS termination is expected to be handled by a reverse proxy such as nginx, Traefik or HAProxy)

Example port mapping if you want it on port 8080 instead of 3000:

-p 8080:3000

You would then access Nextcloud at: http://localhost:8080/⁠


⁠Environment Variables

⁠Core paths
VariableDefaultDescription
NC_WEBROOT/var/www/nextcloudPath to Nextcloud web root
NC_CONFIG_DIR$NC_WEBROOT/configConfig directory
NC_DATA_DIR/var/ncdataData directory
NC_USERapacheUnix user running Nextcloud
NC_GROUPapacheUnix group running Nextcloud

⁠Database & installation
VariableDefaultNotes
NC_AUTOINSTALLfalseAuto-run occ maintenance:install on first start
NC_DB_TYPEmysqlmysql (MariaDB/MySQL) or pgsql
NC_DB_HOSTnextcloud-dbDatabase host
NC_DB_PORT3306 for MySQL; 5432 for PostgreSQLDB port (e.g. 5432 for PostgreSQL)
NC_DB_NAMEnextcloudDatabase name
NC_DB_USERnextcloudDB user
NC_DB_PASSWORD(required for auto-install)DB password
NC_ADMIN_USERadminInitial Nextcloud admin username
NC_ADMIN_PASSWORD(required for auto-install)Initial Nextcloud admin password
NC_ADMIN_EMAIL(empty)Optional admin email passed to the installer

Supported DB engines (per current Nextcloud system requirements) include: [Nextcloud Docs][1]

  • MariaDB 10.11 / 11.4 / 11.8 / 12.3
  • MySQL 8.4
  • PostgreSQL 14 – 18

The database itself is not in this container. You should use a separate MariaDB or PostgreSQL container / service.


⁠HTTP & trusted domains
VariableDefaultDescription
NC_TRUSTED_DOMAINSlocalhostComma- or space-separated list of trusted domains / IPs
NC_TRUSTED_PROXIES(empty)Comma- or space-separated reverse-proxy IPs/CIDRs; also adds HTTP_X_FORWARDED_FOR to forwarded_for_headers (lowest priority, existing headers keep precedence)
NC_OVERWRITE_CLI_URL(empty)Value for overwrite.cli.url in config.php
NC_OVERWRITE_HOST(empty)Browser-facing host (maps to overwritehost)
NC_OVERWRITE_PROTOCOL(empty)Browser-facing protocol (https or http, maps to overwriteprotocol)
NC_OVERWRITE_WEBROOT(empty)Browser-facing webroot (maps to overwritewebroot, e.g. /nextcloud). Derived from a path in NC_OVERWRITE_CLI_URL; a URL without a path keeps a stored value, one ending in / clears it
NC_OVERWRITE_CONDADDR(empty)Optional condition for overwrite settings (maps to overwritecondaddr)
NC_FORCE_HTTPSfalseBackward-compatible shortcut to set overwriteprotocol=https
NC_ALLOWED_ADMIN_RANGES(empty)Comma- or space-separated CIDRs/IPs for admin login restrictions

⁠Redirect troubleshooting (e.g. https://127.0.0.1:8080/...)

If the installer completion redirect still points to loopback/host-local addresses:

  1. Set explicit reverse-proxy URL vars:
    • NC_OVERWRITE_CLI_URL=https://your-public-host
    • NC_OVERWRITE_HOST=your-public-host
    • NC_OVERWRITE_PROTOCOL=https
  2. Ensure NC_TRUSTED_DOMAINS contains the public hostname.
  3. Ensure NC_TRUSTED_PROXIES contains your proxy IP/CIDR.
  4. For existing installs, restart the container so the startup sync merges the env-driven settings into config.php.

⁠Locale, timezone & defaults

Swissmakers-opinionated defaults (overridable via env):

VariableDefaultDescription
NC_DEFAULT_PHONE_REGIONCHDefault phone region
NC_DEFAULT_LOCALEde_CHDefault locale
NC_DEFAULT_TIMEZONEEurope/ZurichDefault PHP / UI timezone
NC_DEFAULT_LOGTIMEZONEEurope/ZurichLog timezone
NC_DEFAULT_APPfilesDefault app after login

These remain your Swissmakers defaults. They fill missing settings during initialization; existing administrator-set locale, timezone and default-app values are preserved.


⁠App install/disable (one-time)

On first post-install hardening, the entrypoint can install/enable and disable apps automatically.

VariableDefault valueDescription
NC_APPS_INSTALLempty - (for example, see below)Comma/space-separated apps to install & enable
NC_APPS_DISABLEempty - (for example, see below)Comma/space-separated apps to disable
NC_APPS_INSTALL_DEFAULTuser_ldap,twofactor_totp,geoblocker,side_menuBuilt-in default install list if NC_APPS_INSTALL unset
NC_APPS_DISABLE_DEFAULTsurvey_client,weather_status,nextcloud_announcements,recommendations,support,app_api,federation,dashboardBuilt-in default disable list if NC_APPS_DISABLE unset

Example:

NC_APPS_INSTALL=calendar,contacts,deck,forms,notes
NC_APPS_DISABLE=firstrunwizard,recommendations,weather_status,photos

⁠Secret files and included deployment

NC_DB_PASSWORD_FILE, NC_ADMIN_PASSWORD_FILE and NC_REDIS_PASSWORD_FILE read credentials from files; the corresponding value and file variables are mutually exclusive. There are no built-in database/admin passwords.

The included compose.yml⁠ uses MariaDB 11.8, database health checks, secret files, an isolated database network, a loopback-only HTTP binding, a read-only application root filesystem, disk-backed upload temporary space, reduced capabilities, a PID limit of 2048 and an explicit application health check (which also works for images built by podman-compose in OCI format). Prepare it with:

cp .env.example .env
# Edit .env: public URL/hostname and exact trusted proxy address.
mkdir -p secrets
chmod 700 secrets
(umask 077; openssl rand -base64 36 > secrets/db_password)
(umask 077; openssl rand -base64 36 > secrets/db_root_password)
(umask 077; openssl rand -base64 36 > secrets/admin_password)
# Podman needs Docker-format metadata to retain HEALTHCHECK.
podman build --format=docker -t localhost/nextcloud-hardened:rocky10 .
podman compose up -d
# With Docker instead:
# docker compose up -d --build

The generated initial admin password is in secrets/admin_password. Keep that directory private; .gitignore and .dockerignore exclude it. The Compose secrets use Podman SELinux relabel extensions⁠: z for the database password shared by both services, and Z for service-specific passwords. Docker Compose ignores these extension fields; an SELinux-enabled Docker host needs suitable labels on the source files. Configure your TLS proxy using deployment/apache.conf.example⁠ or deployment/nginx.conf.example⁠, and set NC_TRUSTED_PROXIES to the address actually observed by the application. Do not trust an entire private network for convenience.

For a public subpath such as /nextcloud, set it in NC_OVERWRITE_CLI_URL. Configure the outer proxy to strip that prefix before passing requests to port 3000, including /nextcloud/push/, and redirect root-level DAV discovery to /nextcloud/remote.php/dav/.


⁠Security & Hardening Notes

Compared to the stock Nextcloud container, this image focuses on:

  • PHP 8.4 from the Remi repository, supported by current Nextcloud releases.

  • APCu + Redis configured according to Nextcloud’s recommendations for caching and locking. See [Nextcloud PHP configuration docs][3].

  • Sodium enabled for modern password hashing.

  • Nextcloud tarball SHA-256 and publisher signature verified before first-run extraction; both checks are mandatory.

  • Apache hardened:

    • ServerTokens Prod, ServerSignature Off, TraceEnable off
    • Stock port-80 vhost, welcome page, autoindex and userdir configs removed -> only the hardened vhost on port 3000 listens
    • .ht* access denied; no AddType for .php (PHP runs exclusively via PHP-FPM)
    • Security headers preconfigured; HSTS is emitted only when the deployment is HTTPS (NC_FORCE_HTTPS=true or an HTTPS overwrite URL/protocol) -> otherwise handle HSTS at the reverse proxy
    • Nextcloud validates forwarded headers against its trusted proxies using the original connection address
  • Local Valkey listens only on a Unix socket, with its TCP port disabled.

  • Nextcloud, PHP-FPM workers and Redis run as apache; the entrypoint runs as root and drops privileges per process.

  • Cron is used for background jobs instead of AJAX, as recommended (internal worker, every 5 minutes).

  • Locale/timezone/phone defaults fill missing settings while preserving existing choices. Caching, file locking and .htaccess are reconciled at startup; app presets, missing DB indices and expensive repairs use success markers.

  • Startup network-settings sync (merge by default): env values are guaranteed present in config.php; manually added trusted domains, proxies and admin ranges are retained unless you opt into replace mode.

For even more security in production, Swissmakers GmbH recommends:

  • Put this container behind a reverse proxy handling TLS (nginx, Traefik, HAProxy) -> and set NC_TRUSTED_PROXIES accordingly.
  • Use separate containers for database and (optionally) Redis.
  • Use a read-only root filesystem where possible (see the "Runtime-generated configuration" section for the paths that need writable mounts).
  • Restrict access to SMB servers strictly (firewall, network ACLs).
  • Regularly rebuild the image for Rocky / PHP / dependency updates, and update the persistent Nextcloud installation separately.

⁠Quick Run (Podman, external MariaDB)

podman run -d \
  --name nextcloud \
  -p 127.0.0.1:8080:3000 \
  -e NC_DB_TYPE=mysql \
  -e NC_DB_HOST=10.0.0.5 \
  -e NC_DB_PORT=3306 \
  -e NC_DB_NAME=nextcloud \
  -e NC_DB_USER=nextcloud \
  -e NC_DB_PASSWORD=supersecret \
  -e NC_AUTOINSTALL=true \
  -e NC_ADMIN_USER=admin \
  -e NC_ADMIN_PASSWORD=change-me-please \
  -e NC_TRUSTED_DOMAINS=localhost \
  -e NC_OVERWRITE_CLI_URL=http://localhost:8080 \
  -v nc_html:/var/www/nextcloud:Z \
  -v nc_data:/var/ncdata:Z \
  registry.swissmakers.ch/infra/nextcloud:latest

Open: http://localhost:8080/⁠ in your browser.

Tag summary

Content type

Image

Digest

sha256:678a70fc1…

Size

361.3 MB

Last updated

about 4 hours ago

docker pull swissmakers/nextcloud