Compare commits

..

2 Commits

Author SHA1 Message Date
copilot-swe-agent[bot]
6a88b33138 Refactor reverse-proxy.md to be shorter and easier to follow
Co-authored-by: szaimen <42591237+szaimen@users.noreply.github.com>
2026-03-20 12:47:05 +00:00
copilot-swe-agent[bot]
f921ed7e4c Initial plan 2026-03-20 12:39:58 +00:00
22 changed files with 351 additions and 803 deletions

View File

@@ -36,7 +36,7 @@ jobs:
line-length: warning
- name: Install the latest version of uv
uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
uses: astral-sh/setup-uv@5a095e7a2014a4212f075830d4f7277575a9d098 # v7.3.1
- name: Check GitHub actions
run: uvx zizmor --min-severity medium .github/workflows/*.yml

View File

@@ -15,7 +15,7 @@
}
https://{$ADDITIONAL_TRUSTED_DOMAIN}:443,
http://{$APACHE_HOST}.nextcloud-aio:23973, # For Collabora callback and WOPI requests, see containers.json
http://{$APACHE_HOST}:23973, # For Collabora callback and WOPI requests, see containers.json
{$PROTOCOL}://{$NC_DOMAIN}:{$APACHE_PORT} {
header -Server
header -X-Powered-By

View File

@@ -1,6 +1,6 @@
# syntax=docker/dockerfile:latest
# From a file located probably somewhere here: https://github.com/CollaboraOnline/online/blob/master/docker/from-packages/Dockerfile
FROM collabora/code:25.04.9.4.1
FROM collabora/code:25.04.9.3.1
USER root
ARG DEBIAN_FRONTEND=noninteractive

View File

@@ -17,8 +17,7 @@
}
servers {
# Only h1 is allowed as we prevent `ERR_NETWORK_CHANGED` from happening
protocols h1
protocols h1 h2 h2c
}
on_demand_tls {
@@ -40,6 +39,7 @@ https://:8443 {
abort @denied
root * /var/www/docker-aio/php/public
encode
php_fastcgi unix//run/php.sock
file_server

View File

@@ -13,8 +13,7 @@
}
servers {
# Only h1 is allowed as we prevent `ERR_NETWORK_CHANGED` from happening
protocols h1
protocols h1 h2
}
skip_install_trust
@@ -28,6 +27,7 @@ https://:8080 {
abort @denied
root * /var/www/docker-aio/php/public
encode
php_fastcgi unix//run/php.sock
file_server

View File

@@ -1,11 +1,4 @@
# syntax=docker/dockerfile:latest
FROM docker.io/library/golang:alpine AS aio-container-tools-builder
# hadolint ignore=DL3022
COPY --from=aio-container-tools . /tmp/aio-container-tools/
WORKDIR /tmp/aio-container-tools
RUN go build -o /usr/local/bin/aio-pg-healthcheck ./cmd/aio-pg-healthcheck
FROM php:8.3.30-fpm-alpine3.23
ENV PHP_MEMORY_LIMIT=512M
@@ -24,7 +17,6 @@ COPY --chmod=775 Containers/nextcloud/*.sh /
COPY --chmod=774 Containers/nextcloud/upgrade.exclude /upgrade.exclude
COPY Containers/nextcloud/config/*.php /
COPY Containers/nextcloud/supervisord.conf /supervisord.conf
COPY --from=aio-container-tools-builder /usr/local/bin/aio-pg-healthcheck /usr/local/bin/aio-pg-healthcheck
# AIO cloning start # Do not remove or change this line!
COPY app /usr/src/nextcloud/apps/nextcloud-aio
@@ -234,6 +226,7 @@ RUN set -ex; \
openssl \
gnupg \
git \
postgresql-client \
tzdata \
sudo \
grep \

View File

@@ -25,7 +25,7 @@ fi
# Fix false database connection on old instances
if [ -f "/var/www/html/config/config.php" ]; then
sleep 2
while ! sudo -E -u www-data /usr/local/bin/aio-pg-healthcheck; do
while ! sudo -E -u www-data psql -d "postgresql://$POSTGRES_USER:$POSTGRES_PASSWORD@$POSTGRES_HOST:$POSTGRES_PORT/$POSTGRES_DB" -c "select now()"; do
echo "Waiting for the database to start..."
sleep 5
done

View File

@@ -1,18 +1,8 @@
# syntax=docker/dockerfile:latest
FROM docker.io/library/golang:alpine AS aio-container-tools-builder
# hadolint ignore=DL3022
COPY --from=aio-container-tools . /tmp/aio-container-tools/
WORKDIR /tmp/aio-container-tools
RUN go build -o /usr/local/bin/aio-pg-init ./cmd/aio-pg-init \
&& go build -o /usr/local/bin/aio-pg-healthcheck ./cmd/aio-pg-healthcheck
# From https://github.com/docker-library/postgres/blob/master/17/alpine3.23/Dockerfile
FROM postgres:17.9-alpine
COPY --chmod=775 start.sh /start.sh
COPY --from=aio-container-tools-builder /usr/local/bin/aio-pg-init /usr/local/bin/aio-pg-init
COPY --from=aio-container-tools-builder /usr/local/bin/aio-pg-healthcheck /usr/local/bin/aio-pg-healthcheck
COPY --chmod=775 healthcheck.sh /healthcheck.sh
COPY --chmod=775 init-user-db.sh /docker-entrypoint-initdb.d/init-user-db.sh

View File

@@ -2,4 +2,6 @@
test -f "/mnt/data/backup-is-running" && exit 0
POSTGRES_PORT=11000 /usr/local/bin/aio-pg-healthcheck debug || exec /usr/local/bin/aio-pg-healthcheck
psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()" && exit 0
psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:5432/$POSTGRES_DB" -c "select now()" || exit 1

View File

@@ -3,7 +3,12 @@ set -ex
touch "$DUMP_DIR/initialization.failed"
POSTGRES_DB_OWNER="oc_$POSTGRES_USER" /usr/local/bin/aio-pg-init
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER "oc_$POSTGRES_USER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB;
ALTER DATABASE "$POSTGRES_DB" OWNER TO "oc_$POSTGRES_USER";
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "oc_$POSTGRES_USER";
GRANT ALL PRIVILEGES ON SCHEMA public TO "oc_$POSTGRES_USER";
EOSQL
rm "$DUMP_DIR/initialization.failed"

View File

@@ -4,7 +4,6 @@
DATADIR="/var/lib/postgresql/data"
export DUMP_DIR="/mnt/data"
DUMP_FILE="$DUMP_DIR/database-dump.sql"
# TODO: Do we need this? It's not used anywhere visible
export PGPASSWORD="$POSTGRES_PASSWORD"
# Don't start database as long as backup is running
@@ -86,7 +85,7 @@ if ( [ -f "$DATADIR/PG_VERSION" ] && [ "$PG_MAJOR" != "$(cat "$DATADIR/PG_VERSIO
exec docker-entrypoint.sh postgres &
# Wait for creation
while ! env POSTGRES_PORT=11000 POSTGRES_USER="oc_$POSTGRES_USER" /usr/local/bin/aio-pg-healthcheck; do
while ! psql -d "postgresql://oc_$POSTGRES_USER:$POSTGRES_PASSWORD@127.0.0.1:11000/$POSTGRES_DB" -c "select now()"; do
echo "Waiting for the database to start."
sleep 5
done
@@ -108,7 +107,12 @@ if ( [ -f "$DATADIR/PG_VERSION" ] && [ "$PG_MAJOR" != "$(cat "$DATADIR/PG_VERSIO
exit 1
elif [ "$DB_OWNER" != "oc_$POSTGRES_USER" ]; then
DIFFERENT_DB_OWNER=1
POSTGRES_DB_OWNER="$DB_OWNER" /usr/local/bin/aio-pg-init
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL
CREATE USER "$DB_OWNER" WITH PASSWORD '$POSTGRES_PASSWORD' CREATEDB;
ALTER DATABASE "$POSTGRES_DB" OWNER TO "$DB_OWNER";
GRANT ALL PRIVILEGES ON DATABASE "$POSTGRES_DB" TO "$DB_OWNER";
GRANT ALL PRIVILEGES ON SCHEMA public TO "$DB_OWNER";
EOSQL
fi
# Restore database

View File

@@ -1,42 +0,0 @@
# aio-container-tools
Standalone tools for Nextcloud AIO containers, for tasks that shouldn't be executed in a shell environment
(e.g. due to string handling issues).
Golang was chosen because it doesn't require additional runtimes in the containers, and has a pretty easy
syntax that is comprehensible even for people without much experience with the language.
The tools should be built in the container image build process, so they are built for the correct target
platform in multi-arch builds. See below for an example.
## Build process
To include the binary of `aio-pg-healhcheck` into your container image, include such a snippet into your Containerfile:
```dockerfile
FROM docker.io/library/golang:alpine AS golang-builder
# hadolint ignore=DL3022
COPY --from=aio-container-tools . /tmp/aio-container-tools/
RUN cd /tmp/aio-container-tools \
&& go build -o /usr/local/bin/aio-pg-healthcheck ./cmd/aio-pg-healthcheck
FROM your-base-image
COPY --from=golang-builder /usr/local/bin/aio-pg-healthcheck /usr/local/bin/
```
To build it you now have to pass the aio-container-tools directory as additional, named build-context like this:
```bash
docker build \
--build-context aio-container-tools=/path/to/all-in-one/aio-container-tools \
.
```
#### Remote git variant (without local clone of this repo)
```bash
docker build \
--build-context aio-container-tools="https://github.com/nextcloud-releases/all-in-one.git#main:aio-container-tools" \
.
```

View File

@@ -1,92 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"os"
"strconv"
"github.com/jackc/pgx/v5"
"github.com/nextcloud/aio-container-tools/internal/util"
)
// tryConnect opens a TCP connection to the given database host:port and runs SELECT 1.
// Returns nil on success, an error otherwise.
func tryConnect(ctx context.Context, host string, port uint16, user, password, database string) error {
util.Debugf("attempting connection: host=%s port=%d user=%s database=%s", host, port, user, database)
cfg, err := pgx.ParseConfig("")
if err != nil {
return err
}
cfg.Host = host
cfg.Port = port
cfg.User = user
cfg.Password = password
cfg.Database = database
conn, err := pgx.ConnectConfig(ctx, cfg)
if err != nil {
util.Debugf("connection failed: %v", err)
return err
}
defer conn.Close(ctx)
util.Debugf("connection established, running SELECT 1")
var result string
if err := conn.QueryRow(ctx, "SELECT 1").Scan(&result); err != nil {
util.Debugf("SELECT 1 failed: %v", err)
return err
}
util.Debugf("SELECT 1 returned %q", result)
return nil
}
// envOrDefault returns the value of the named environment variable,
// or the provided default if the variable is unset or empty.
func envOrDefault(key, defaultVal string) string {
if v := os.Getenv(key); v != "" {
util.Debugf("env %s = %q", key, v)
return v
}
util.Debugf("env %s not set, using default %q", key, defaultVal)
return defaultVal
}
func main() {
debug := flag.Bool("debug", false, "enable debug output")
flag.Parse()
util.SetDebug(*debug)
util.Debugf("reading required environment variables")
pgUser := util.RequireEnv("POSTGRES_USER")
pgPassword := util.RequireEnv("POSTGRES_PASSWORD")
pgDB := util.RequireEnv("POSTGRES_DB")
ctx := context.Background()
pgHost := envOrDefault("POSTGRES_HOST", "127.0.0.1")
var pgPort uint16 = 5432
if portStr := os.Getenv("POSTGRES_PORT"); portStr != "" {
util.Debugf("env POSTGRES_PORT = %q", portStr)
p, err := strconv.ParseUint(portStr, 10, 16)
if err != nil {
fmt.Fprintf(os.Stderr, "invalid POSTGRES_PORT %q: %v\n", portStr, err)
os.Exit(1)
}
pgPort = uint16(p)
} else {
util.Debugf("env POSTGRES_PORT not set, using default port %d", pgPort)
}
util.Debugf("connecting to: host=%s port=%d user=%s", pgHost, pgPort, pgUser)
if err := tryConnect(ctx, pgHost, pgPort, pgUser, pgPassword, pgDB); err == nil {
util.Debugf("connection succeeded, exiting 0")
os.Exit(0)
}
util.Debugf("connection failed, exiting 1")
os.Exit(1)
}

View File

@@ -1,78 +0,0 @@
package main
import (
"context"
"flag"
"fmt"
"strings"
"github.com/jackc/pgx/v5"
"github.com/nextcloud/aio-container-tools/internal/util"
)
// quoteLiteral safely quotes a string as a PostgreSQL string literal.
// Single quotes are escaped by doubling them. This is safe with
// standard_conforming_strings=on (default since PostgreSQL 9.1).
func quoteLiteral(s string) string {
return "'" + strings.ReplaceAll(s, "'", "''") + "'"
}
// main reimplements init-user-db.sh:
// - Creates $POSTGRES_DB_OWNER (falling back to $POSTGRES_USER) with $POSTGRES_PASSWORD and CREATEDB
// - Transfers ownership of $POSTGRES_DB to that user
// - Grants all privileges on the database and public schema
// - Connects using $POSTGRES_USER in all cases
func main() {
debug := flag.Bool("debug", false, "enable debug output")
flag.Parse()
util.SetDebug(*debug)
util.Debugf("reading required environment variables")
pgUser := util.RequireEnv("POSTGRES_USER")
pgPassword := util.RequireEnv("POSTGRES_PASSWORD")
pgDB := util.RequireEnv("POSTGRES_DB")
pgDBOwner := util.OptionalEnv("POSTGRES_DB_OWNER", pgUser)
util.Debugf("building connection config: host=/var/run/postgresql port=5432 user=%s database=%s", pgUser, pgDB)
cfg, err := pgx.ParseConfig("")
if err != nil {
util.ErrorOut(fmt.Errorf("building connection config: %w", err))
}
cfg.Host = "/var/run/postgresql"
cfg.Port = 5432
cfg.User = pgUser
cfg.Password = pgPassword
cfg.Database = pgDB
ctx := context.Background()
util.Debugf("connecting to postgres via unix socket")
conn, err := pgx.ConnectConfig(ctx, cfg)
if err != nil {
util.ErrorOut(fmt.Errorf("connecting to postgres: %w", err))
}
defer conn.Close(ctx)
util.Debugf("connected successfully")
dbOwner := pgDBOwner
util.Debugf("dbOwner = %q (from POSTGRES_DB_OWNER=%q, POSTGRES_USER=%q)", dbOwner, pgDBOwner, pgUser)
// pgx.Identifier.Sanitize() double-quotes and escapes the identifier safely.
dbOwnerIdent := pgx.Identifier{dbOwner}.Sanitize()
dbIdent := pgx.Identifier{pgDB}.Sanitize()
util.Debugf("quoted dbOwnerIdent = %s, dbIdent = %s", dbOwnerIdent, dbIdent)
statements := []string{
fmt.Sprintf("CREATE USER %s WITH PASSWORD %s CREATEDB", dbOwnerIdent, quoteLiteral(pgPassword)),
fmt.Sprintf("ALTER DATABASE %s OWNER TO %s", dbIdent, dbOwnerIdent),
fmt.Sprintf("GRANT ALL PRIVILEGES ON DATABASE %s TO %s", dbIdent, dbOwnerIdent),
fmt.Sprintf("GRANT ALL PRIVILEGES ON SCHEMA public TO %s", dbOwnerIdent),
}
for i, stmt := range statements {
util.Debugf("executing statement %d/%d: %s", i+1, len(statements), stmt)
if _, err := conn.Exec(ctx, stmt); err != nil {
util.ErrorOut(fmt.Errorf("executing statement: %w", err))
}
util.Debugf("statement %d/%d succeeded", i+1, len(statements))
}
util.Debugf("all statements executed successfully")
}

View File

@@ -1,10 +0,0 @@
module github.com/nextcloud/aio-container-tools
go 1.25.1
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/pgx/v5 v5.8.0 // indirect
golang.org/x/text v0.29.0 // indirect
)

View File

@@ -1,15 +0,0 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
github.com/jackc/pgx/v5 v5.8.0 h1:TYPDoleBBme0xGSAX3/+NujXXtpZn9HBONkQC7IEZSo=
github.com/jackc/pgx/v5 v5.8.0/go.mod h1:QVeDInX2m9VyzvNeiCJVjCkNFqzsNb43204HshNSZKw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -1,49 +0,0 @@
package util
import (
"fmt"
"log"
"os"
)
var debugEnabled bool
// SetDebug enables or disables debug output.
func SetDebug(enabled bool) {
debugEnabled = enabled
}
// Debugf prints a formatted debug message to stdout when debug mode is enabled.
func Debugf(format string, args ...any) {
if debugEnabled {
fmt.Printf("[debug] "+format+"\n", args...)
}
}
// RequireEnv returns the value of the named environment variable.
// It writes an error to stderr and exits with code 1 if the variable is unset or empty.
func RequireEnv(key string) string {
v := os.Getenv(key)
if v == "" {
fmt.Fprintf(os.Stderr, "required environment variable %q is not set\n", key)
os.Exit(1)
}
Debugf("env %s = %q", key, v)
return v
}
// OptionalEnv returns the value of the named environment variable, or fallback if it is unset or empty.
func OptionalEnv(key, fallback string) string {
v := os.Getenv(key)
if v == "" {
Debugf("env %s unset, using fallback %q", key, fallback)
return fallback
}
Debugf("env %s = %q", key, v)
return v
}
// ErrorOut logs the error with a standard prefix and exits with code 1.
func ErrorOut(err error) {
log.Fatalf("error: %v", err)
}

View File

@@ -379,7 +379,7 @@
],
"internal_port": "9980",
"environment": [
"aliasgroup1=https://%NC_DOMAIN%:443,http://nextcloud-aio-apache.nextcloud-aio:23973",
"aliasgroup1=https://%NC_DOMAIN%:443,http://nextcloud-aio-apache:23973",
"extra_params=--o:ssl.enable=false --o:ssl.termination=true --o:logging.disable_server_audit=true --o:logging.level=warning --o:logging.level_startup=warning --o:welcome.enable=false --o:fetch_update_check=0 --o:allow_update_popup=false %COLLABORA_SECCOMP_POLICY% --o:remote_font_config.url=https://%NC_DOMAIN%/apps/richdocuments/settings/fonts.json --o:net.post_allow.host[0]=.+",
"dictionaries=%COLLABORA_DICTIONARIES%",
"TZ=%TIMEZONE%",
@@ -389,7 +389,7 @@
"restart": "unless-stopped",
"nextcloud_exec_commands": [
"echo 'Activating Collabora config...'",
"php /var/www/html/occ richdocuments:activate-config --wopi-url='http://nextcloud-aio-apache.nextcloud-aio:23973' --callback-url='http://nextcloud-aio-apache.nextcloud-aio:23973'"
"php /var/www/html/occ richdocuments:activate-config --wopi-url='http://nextcloud-aio-apache:23973' --callback-url='http://nextcloud-aio-apache:23973'"
],
"profiles": [
"collabora"

View File

@@ -353,9 +353,6 @@
<input type="hidden" name="{{csrf.keys.name}}" value="{{csrf.name}}">
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input id="base_path" type="hidden" name="base_path" value="">
{% if bypass_container_update == true %}
<input type="hidden" name="bypass_container_update" value="true">
{% endif %}
<input type="submit" value="Start containers" />
</form>
{% else %}
@@ -364,7 +361,7 @@
<input type="hidden" name="{{csrf.keys.value}}" value="{{csrf.value}}">
<input id="base_path" type="hidden" name="base_path" value="">
{% if bypass_container_update == true %}
<input type="hidden" name="bypass_container_update" value="true">
<input type="hidden" name="bypass_container_update" value="{{bypass_container_update}}">
{% endif %}
<input class="button " type="submit" value="Start and update containers" onclick="return confirm('Start and update containers? You should consider creating a backup first.')" />
</form>

0
php/tests/tests/initial-setup.spec.js Executable file → Normal file
View File

0
php/tests/tests/restore-instance.spec.js Executable file → Normal file
View File

File diff suppressed because it is too large Load Diff