This image packages Nextcloud with Swissmakers configuration presets in a hardened container.
264
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.
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:
:rocky9.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.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.NC_DB_PASSWORD / NC_ADMIN_PASSWORD (or *_FILE). Installed instances are not affected.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.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.config/ are handed to the web user on start, because Nextcloud's updater refuses to run otherwise.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.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.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.
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:
| Tag | Base | CPU (amd64) |
|---|---|---|
latest, rocky10, rocky10-<commit> | Rocky Linux 10 | x86-64-v3 |
rocky9, rocky9-<commit> | Rocky Linux 9 | x86-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
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":
.htaccess and adds missing DB indicesStartup sync for network settings (every start after install, merge semantics):
NC_TRUSTED_DOMAINS, NC_TRUSTED_PROXIES and NC_ALLOWED_ADMIN_RANGES are present in config.phpocc or the admin UIoverwrite.* settings from reverse-proxy env varsInside 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_dataImportant: For best security, keep data on a dedicated volume or storage backend and back it up regularly, together with
config.phpand the database.
When using Podman with SELinux, use :Z:
- nc_html:/var/www/nextcloud:Z
- nc_data:/var/ncdata:Z
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/
| Variable | Default | Description |
|---|---|---|
NC_WEBROOT | /var/www/nextcloud | Path to Nextcloud web root |
NC_CONFIG_DIR | $NC_WEBROOT/config | Config directory |
NC_DATA_DIR | /var/ncdata | Data directory |
NC_USER | apache | Unix user running Nextcloud |
NC_GROUP | apache | Unix group running Nextcloud |
| Variable | Default | Notes |
|---|---|---|
NC_AUTOINSTALL | false | Auto-run occ maintenance:install on first start |
NC_DB_TYPE | mysql | mysql (MariaDB/MySQL) or pgsql |
NC_DB_HOST | nextcloud-db | Database host |
NC_DB_PORT | 3306 for MySQL; 5432 for PostgreSQL | DB port (e.g. 5432 for PostgreSQL) |
NC_DB_NAME | nextcloud | Database name |
NC_DB_USER | nextcloud | DB user |
NC_DB_PASSWORD | (required for auto-install) | DB password |
NC_ADMIN_USER | admin | Initial 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]
10.11 / 11.4 / 11.8 / 12.38.414 – 18The database itself is not in this container. You should use a separate MariaDB or PostgreSQL container / service.
| Variable | Default | Description |
|---|---|---|
NC_TRUSTED_DOMAINS | localhost | Comma- 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_HTTPS | false | Backward-compatible shortcut to set overwriteprotocol=https |
NC_ALLOWED_ADMIN_RANGES | (empty) | Comma- or space-separated CIDRs/IPs for admin login restrictions |
https://127.0.0.1:8080/...)If the installer completion redirect still points to loopback/host-local addresses:
NC_OVERWRITE_CLI_URL=https://your-public-hostNC_OVERWRITE_HOST=your-public-hostNC_OVERWRITE_PROTOCOL=httpsNC_TRUSTED_DOMAINS contains the public hostname.NC_TRUSTED_PROXIES contains your proxy IP/CIDR.config.php.Swissmakers-opinionated defaults (overridable via env):
| Variable | Default | Description |
|---|---|---|
NC_DEFAULT_PHONE_REGION | CH | Default phone region |
NC_DEFAULT_LOCALE | de_CH | Default locale |
NC_DEFAULT_TIMEZONE | Europe/Zurich | Default PHP / UI timezone |
NC_DEFAULT_LOGTIMEZONE | Europe/Zurich | Log timezone |
NC_DEFAULT_APP | files | Default 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.
On first post-install hardening, the entrypoint can install/enable and disable apps automatically.
| Variable | Default value | Description |
|---|---|---|
NC_APPS_INSTALL | empty - (for example, see below) | Comma/space-separated apps to install & enable |
NC_APPS_DISABLE | empty - (for example, see below) | Comma/space-separated apps to disable |
NC_APPS_INSTALL_DEFAULT | user_ldap,twofactor_totp,geoblocker,side_menu | Built-in default install list if NC_APPS_INSTALL unset |
NC_APPS_DISABLE_DEFAULT | survey_client,weather_status,nextcloud_announcements,recommendations,support,app_api,federation,dashboard | Built-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
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/.
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.ht* access denied; no AddType for .php (PHP runs exclusively via PHP-FPM)NC_FORCE_HTTPS=true or an HTTPS overwrite URL/protocol) -> otherwise handle HSTS at the reverse proxyLocal 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:
NC_TRUSTED_PROXIES accordingly.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.
Content type
Image
Digest
sha256:678a70fc1…
Size
361.3 MB
Last updated
about 4 hours ago
docker pull swissmakers/nextcloud