Skip to content

Commit b29f57e

Browse files
committed
feat: add custom nextcloud dockerfile and github action to publish docker image
1 parent 3450d62 commit b29f57e

14 files changed

Lines changed: 863 additions & 0 deletions

.github/workflows/publish.yml

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
name: Publish Docker Image
2+
3+
on:
4+
workflow_dispatch:
5+
inputs:
6+
nextcloud_version:
7+
description: "Nextcloud version (e.g. 30.0.4)"
8+
required: true
9+
type: string
10+
enterprise_url:
11+
description: "Enterprise archive URL (leave empty for community edition)"
12+
required: false
13+
default: ""
14+
type: string
15+
16+
env:
17+
REGISTRY: ghcr.io
18+
IMAGE_NAME: ${{ github.repository_owner }}/nextcloud
19+
20+
jobs:
21+
build-and-push:
22+
runs-on: ubuntu-latest
23+
permissions:
24+
contents: read
25+
packages: write
26+
27+
steps:
28+
- name: Checkout repository
29+
uses: actions/checkout@v6
30+
31+
- name: Determine edition
32+
id: edition
33+
run: |
34+
if [ -n "${{ inputs.enterprise_url }}" ]; then
35+
echo "name=enterprise" >> "$GITHUB_OUTPUT"
36+
else
37+
echo "name=community" >> "$GITHUB_OUTPUT"
38+
fi
39+
40+
- name: Log in to GitHub Container Registry
41+
uses: docker/login-action@v4
42+
with:
43+
registry: ${{ env.REGISTRY }}
44+
username: ${{ github.actor }}
45+
password: ${{ secrets.GITHUB_TOKEN }}
46+
47+
- name: Set up Docker Buildx
48+
uses: docker/setup-buildx-action@v4
49+
50+
- name: Lower case docker image name
51+
id: docker_image
52+
uses: ASzc/change-string-case-action@v6
53+
with:
54+
string: ${{ github.repository }}
55+
56+
- name: Generate image tags
57+
id: tags
58+
run: |
59+
VERSION="${{ inputs.nextcloud_version }}"
60+
EDITION="${{ steps.edition.outputs.name }}"
61+
IMAGE="${{ env.REGISTRY }}/${{ steps.docker_image.outputs.lowercase }}"
62+
63+
if [ "$EDITION" = "enterprise" ]; then
64+
TAGS="${IMAGE}:${VERSION}-enterprise"
65+
else
66+
TAGS="${IMAGE}:${VERSION}"
67+
fi
68+
69+
echo "tags=$TAGS" >> "$GITHUB_OUTPUT"
70+
71+
- name: Build and push Docker image
72+
uses: docker/build-push-action@v6
73+
with:
74+
context: ./nextcloud
75+
push: true
76+
tags: ${{ steps.tags.outputs.tags }}
77+
build-args: |
78+
NEXTCLOUD_VERSION=${{ inputs.nextcloud_version }}
79+
ENTERPRISE_ARCHIVE_URL=${{ inputs.enterprise_url }}
80+
cache-from: type=gha
81+
cache-to: type=gha,mode=max

nextcloud/Dockerfile

Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
# syntax=docker/dockerfile:1
2+
3+
# ---------------------------------------------------------------------------
4+
# Build arguments
5+
#
6+
# NEXTCLOUD_VERSION : version to download from the community CDN
7+
# ENTERPRISE_ARCHIVE_URL : if set, download the enterprise zip from this URL instead of the community tarball.
8+
# ---------------------------------------------------------------------------
9+
ARG NEXTCLOUD_VERSION=
10+
ARG ENTERPRISE_ARCHIVE_URL=""
11+
12+
# ---------------------------------------------------------------------------
13+
# Stage: source
14+
# Downloads and extracts the Nextcloud source once, at build time.
15+
# No rsync, no runtime downloads.
16+
# ---------------------------------------------------------------------------
17+
FROM alpine:3.23 AS source
18+
19+
ARG NEXTCLOUD_VERSION
20+
ARG ENTERPRISE_ARCHIVE_URL
21+
22+
RUN apk add --no-cache \
23+
curl \
24+
gnupg \
25+
bzip2 \
26+
unzip
27+
28+
# Community: download + GPG verify + extract
29+
# Enterprise: download zip (URL already embeds the customer token) + extract
30+
RUN set -ex; \
31+
mkdir -p /var/www/html; \
32+
if [ -n "${ENTERPRISE_ARCHIVE_URL}" ]; then \
33+
echo "==> Downloading Nextcloud Enterprise from ${ENTERPRISE_ARCHIVE_URL}"; \
34+
curl -fsSL -o /tmp/nextcloud.zip "${ENTERPRISE_ARCHIVE_URL}"; \
35+
unzip -q /tmp/nextcloud.zip -d /tmp/src/; \
36+
mv /tmp/src/nextcloud/* /var/www/html/; \
37+
rm /tmp/nextcloud.zip; \
38+
else \
39+
echo "==> Downloading Nextcloud Community ${NEXTCLOUD_VERSION}"; \
40+
curl -fsSL -o /tmp/nextcloud.tar.bz2 \
41+
"https://github.com/nextcloud-releases/server/releases/download/v${NEXTCLOUD_VERSION}/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2"; \
42+
curl -fsSL -o /tmp/nextcloud.tar.bz2.asc \
43+
"https://github.com/nextcloud-releases/server/releases/download/v${NEXTCLOUD_VERSION}/nextcloud-${NEXTCLOUD_VERSION}.tar.bz2.asc"; \
44+
export GNUPGHOME="$(mktemp -d)"; \
45+
gpg --batch --keyserver keyserver.ubuntu.com \
46+
--recv-keys 28806A878AE423A28372792ED75899B9A724937A; \
47+
gpg --batch --verify /tmp/nextcloud.tar.bz2.asc /tmp/nextcloud.tar.bz2; \
48+
gpgconf --kill all; \
49+
rm -rf "${GNUPGHOME}"; \
50+
tar -xjf /tmp/nextcloud.tar.bz2 -C /tmp/src/; \
51+
mv /tmp/src/nextcloud /var/www/html; \
52+
rm /tmp/nextcloud.tar.bz2 /tmp/nextcloud.tar.bz2.asc; \
53+
fi; \
54+
# Remove the built-in web updater — updates happen via image rebuild
55+
rm -rf /var/www/html/updater; \
56+
# Ensure these dirs exist for first-run volume mount detection
57+
mkdir -p \
58+
/var/www/html/data \
59+
/var/www/html/custom_apps \
60+
/var/www/html/config \
61+
/var/www/html/themes; \
62+
chmod +x /var/www/html/occ
63+
64+
# Copy default config files into the source tree so they land in the image
65+
COPY config/ /var/www/html/config/
66+
67+
# ---------------------------------------------------------------------------
68+
# Stage: final
69+
# ---------------------------------------------------------------------------
70+
FROM php:8.4-apache-trixie
71+
72+
ENV PHP_MEMORY_LIMIT=512M \
73+
PHP_UPLOAD_LIMIT=512M \
74+
PHP_OPCACHE_MEMORY_CONSUMPTION=128 \
75+
APACHE_BODY_LIMIT=1073741824
76+
77+
RUN set -ex; \
78+
savedAptMark="$(apt-mark showmanual)"; \
79+
apt-get update; \
80+
apt-get install -y --no-install-recommends \
81+
# entrypoint.sh and cron.sh dependencies
82+
busybox-static \
83+
bzip2 \
84+
libldap-common \
85+
libmagickcore-7.q16-10-extra \
86+
# install the PHP extensions we need
87+
# see https://docs.nextcloud.com/server/stable/admin_manual/installation/source_installation.html
88+
libcurl4-openssl-dev \
89+
libevent-dev \
90+
libfreetype6-dev \
91+
libgmp-dev \
92+
libicu-dev \
93+
libjpeg-dev \
94+
libldap2-dev \
95+
liblz4-dev \
96+
libmagickwand-dev \
97+
libmemcached-dev \
98+
libpng-dev \
99+
libpq-dev \
100+
libwebp-dev \
101+
libxml2-dev \
102+
libzip-dev \
103+
; \
104+
debMultiarch="$(dpkg-architecture --query DEB_BUILD_MULTIARCH)"; \
105+
docker-php-ext-configure ftp --with-ftp-ssl; \
106+
docker-php-ext-configure gd --with-freetype --with-jpeg --with-webp; \
107+
docker-php-ext-configure ldap --with-libdir="lib/${debMultiarch}"; \
108+
docker-php-ext-install -j "$(nproc)" \
109+
bcmath \
110+
exif \
111+
ftp \
112+
gd \
113+
gmp \
114+
intl \
115+
ldap \
116+
opcache \
117+
pcntl \
118+
pdo_mysql \
119+
pdo_pgsql \
120+
sysvsem \
121+
zip \
122+
; \
123+
# pecl will claim success even if one install fails, so we need to perform each install separately
124+
pecl install APCu-5.1.28; \
125+
pecl install igbinary-3.2.16; \
126+
pecl install imagick-3.8.1; \
127+
pecl install --configureoptions 'enable-memcached-igbinary="yes"' \
128+
memcached-3.4.0; \
129+
pecl install --configureoptions 'enable-redis-igbinary="yes" enable-redis-zstd="yes" enable-redis-lz4="yes"' \
130+
redis-6.3.0; \
131+
docker-php-ext-enable \
132+
apcu \
133+
igbinary \
134+
imagick \
135+
memcached \
136+
opcache \
137+
redis \
138+
; \
139+
rm -r /tmp/pear; \
140+
# reset apt-mark's "manual" list so that "purge --auto-remove" will remove all build dependencies
141+
apt-mark auto '.*' > /dev/null; \
142+
apt-mark manual $savedAptMark; \
143+
ldd "$(php -r 'echo ini_get("extension_dir");')"/*.so \
144+
| awk '/=>/ { so = $(NF-1); if (index(so, "/usr/local/") == 1) { next }; gsub("^/(usr/)?", "", so); print so }' \
145+
| sort -u \
146+
| xargs -rt dpkg-query --search \
147+
# https://manpages.debian.org/trixie/dpkg/dpkg-query.1.en.html#S (we ignore diversions and it'll be really unusual for more than one package to provide any given .so file)
148+
| awk 'sub(":$", "", $1) { print $1 }' \
149+
| sort -u \
150+
| xargs -rt apt-mark manual; \
151+
apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false; \
152+
apt-get clean; \
153+
rm -rf /var/lib/apt/lists/*
154+
155+
# set recommended PHP.ini settings
156+
# see https://docs.nextcloud.com/server/latest/admin_manual/installation/server_tuning.html#enable-php-opcache
157+
RUN { \
158+
echo 'opcache.enable=1'; \
159+
echo 'opcache.interned_strings_buffer=32'; \
160+
echo 'opcache.max_accelerated_files=10000'; \
161+
echo 'opcache.memory_consumption=${PHP_OPCACHE_MEMORY_CONSUMPTION}'; \
162+
echo 'opcache.save_comments=1'; \
163+
echo 'opcache.revalidate_freq=60'; \
164+
echo 'opcache.jit=1255'; \
165+
echo 'opcache.jit_buffer_size=8M'; \
166+
} > "${PHP_INI_DIR}/conf.d/opcache-recommended.ini"; \
167+
\
168+
echo 'apc.enable_cli=1' >> "${PHP_INI_DIR}/conf.d/docker-php-ext-apcu.ini"; \
169+
\
170+
{ \
171+
echo 'apc.serializer=igbinary'; \
172+
echo 'session.serialize_handler=igbinary'; \
173+
} >> "${PHP_INI_DIR}/conf.d/docker-php-ext-igbinary.ini"; \
174+
\
175+
{ \
176+
echo 'memory_limit=${PHP_MEMORY_LIMIT}'; \
177+
echo 'upload_max_filesize=${PHP_UPLOAD_LIMIT}'; \
178+
echo 'post_max_size=${PHP_UPLOAD_LIMIT}'; \
179+
} > "${PHP_INI_DIR}/conf.d/nextcloud.ini";
180+
181+
# ---- Apache ----------------------------------------------------------------
182+
RUN a2enmod headers rewrite remoteip; \
183+
{ \
184+
echo 'RemoteIPHeader X-Real-IP'; \
185+
echo 'RemoteIPInternalProxy 10.0.0.0/8'; \
186+
echo 'RemoteIPInternalProxy 172.16.0.0/12'; \
187+
echo 'RemoteIPInternalProxy 192.168.0.0/16'; \
188+
} > /etc/apache2/conf-available/remoteip.conf; \
189+
a2enconf remoteip; \
190+
{ \
191+
echo 'LimitRequestBody ${APACHE_BODY_LIMIT}'; \
192+
} > /etc/apache2/conf-available/apache-limits.conf; \
193+
a2enconf apache-limits
194+
195+
# ---- cron ------------------------------------------------------------------
196+
RUN mkdir -p /var/spool/cron/crontabs; \
197+
echo '*/5 * * * * php -f /var/www/html/cron.php' > /var/spool/cron/crontabs/www-data
198+
199+
# ---- hook directories ------------------------------------------------------
200+
RUN mkdir -p \
201+
/docker-entrypoint-hooks.d/pre-installation \
202+
/docker-entrypoint-hooks.d/post-installation \
203+
/docker-entrypoint-hooks.d/pre-upgrade \
204+
/docker-entrypoint-hooks.d/post-upgrade \
205+
/docker-entrypoint-hooks.d/before-starting
206+
207+
# ---- Nextcloud source (from build stage) -----------------------------------
208+
# The code is baked into the image — no VOLUME on /var/www/html.
209+
# Only user-data directories are declared as volumes so they persist across
210+
# image updates without touching the application code.
211+
COPY --from=source --chown=www-data:root /var/www/html /var/www/html
212+
213+
RUN chmod -R g=u /var/www/html
214+
215+
# Persistent data: only what the user actually modifies at runtime.
216+
# The application code in /var/www/html is part of the image and is
217+
# replaced atomically on every `docker pull` + restart — no rsync needed.
218+
VOLUME /var/www/html/config
219+
VOLUME /var/www/html/data
220+
VOLUME /var/www/html/custom_apps
221+
VOLUME /var/www/html/themes
222+
223+
COPY docker-entrypoint.sh /docker-entrypoint.sh
224+
COPY cron.sh /cron.sh
225+
RUN chmod +x /docker-entrypoint.sh /cron.sh
226+
227+
ARG NEXTCLOUD_VERSION
228+
ARG ENTERPRISE_ARCHIVE_URL
229+
ENV NEXTCLOUD_VERSION=${NEXTCLOUD_VERSION}
230+
# Expose whether this is an enterprise build for informational purposes
231+
ENV NEXTCLOUD_EDITION=${ENTERPRISE_ARCHIVE_URL:+enterprise}
232+
ENV NEXTCLOUD_EDITION=${NEXTCLOUD_EDITION:-community}
233+
234+
ENTRYPOINT ["/docker-entrypoint.sh"]
235+
CMD ["apache2-foreground"]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
$CONFIG = array (
3+
'htaccess.RewriteBase' => '/',
4+
);

nextcloud/config/apcu.config.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
<?php
2+
$CONFIG = array (
3+
'memcache.local' => '\OC\Memcache\APCu',
4+
);

nextcloud/config/apps.config.php

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
<?php
2+
$CONFIG = array (
3+
'apps_paths' => array (
4+
0 => array (
5+
'path' => OC::$SERVERROOT.'/apps',
6+
'url' => '/apps',
7+
'writable' => false,
8+
),
9+
1 => array (
10+
'path' => OC::$SERVERROOT.'/custom_apps',
11+
'url' => '/custom_apps',
12+
'writable' => true,
13+
),
14+
),
15+
);

nextcloud/config/autoconfig.php

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
$autoconfig_enabled = false;
4+
5+
if (getenv('SQLITE_DATABASE')) {
6+
$AUTOCONFIG['dbtype'] = 'sqlite';
7+
$AUTOCONFIG['dbname'] = getenv('SQLITE_DATABASE');
8+
$autoconfig_enabled = true;
9+
} elseif (getenv('MYSQL_DATABASE_FILE') && getenv('MYSQL_USER_FILE') && getenv('MYSQL_PASSWORD_FILE') && getenv('MYSQL_HOST')) {
10+
$AUTOCONFIG['dbtype'] = 'mysql';
11+
$AUTOCONFIG['dbname'] = trim(file_get_contents(getenv('MYSQL_DATABASE_FILE')));
12+
$AUTOCONFIG['dbuser'] = trim(file_get_contents(getenv('MYSQL_USER_FILE')));
13+
$AUTOCONFIG['dbpass'] = trim(file_get_contents(getenv('MYSQL_PASSWORD_FILE')));
14+
$AUTOCONFIG['dbhost'] = getenv('MYSQL_HOST');
15+
$autoconfig_enabled = true;
16+
} elseif (getenv('MYSQL_DATABASE') && getenv('MYSQL_USER') && getenv('MYSQL_PASSWORD') && getenv('MYSQL_HOST')) {
17+
$AUTOCONFIG['dbtype'] = 'mysql';
18+
$AUTOCONFIG['dbname'] = getenv('MYSQL_DATABASE');
19+
$AUTOCONFIG['dbuser'] = getenv('MYSQL_USER');
20+
$AUTOCONFIG['dbpass'] = getenv('MYSQL_PASSWORD');
21+
$AUTOCONFIG['dbhost'] = getenv('MYSQL_HOST');
22+
$autoconfig_enabled = true;
23+
} elseif (getenv('POSTGRES_DB_FILE') && getenv('POSTGRES_USER_FILE') && getenv('POSTGRES_PASSWORD_FILE') && getenv('POSTGRES_HOST')) {
24+
$AUTOCONFIG['dbtype'] = 'pgsql';
25+
$AUTOCONFIG['dbname'] = trim(file_get_contents(getenv('POSTGRES_DB_FILE')));
26+
$AUTOCONFIG['dbuser'] = trim(file_get_contents(getenv('POSTGRES_USER_FILE')));
27+
$AUTOCONFIG['dbpass'] = trim(file_get_contents(getenv('POSTGRES_PASSWORD_FILE')));
28+
$AUTOCONFIG['dbhost'] = getenv('POSTGRES_HOST');
29+
$autoconfig_enabled = true;
30+
} elseif (getenv('POSTGRES_DB') && getenv('POSTGRES_USER') && getenv('POSTGRES_PASSWORD') && getenv('POSTGRES_HOST')) {
31+
$AUTOCONFIG['dbtype'] = 'pgsql';
32+
$AUTOCONFIG['dbname'] = getenv('POSTGRES_DB');
33+
$AUTOCONFIG['dbuser'] = getenv('POSTGRES_USER');
34+
$AUTOCONFIG['dbpass'] = getenv('POSTGRES_PASSWORD');
35+
$AUTOCONFIG['dbhost'] = getenv('POSTGRES_HOST');
36+
$autoconfig_enabled = true;
37+
}
38+
39+
if ($autoconfig_enabled) {
40+
$AUTOCONFIG['directory'] = getenv('NEXTCLOUD_DATA_DIR') ?: '/var/www/html/data';
41+
}

0 commit comments

Comments
 (0)