Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@ Work this package's `TODO.md` from top to bottom. Keep `README.md` (architecture

## Inspecting a running install

To run a command inside the service's container (read its generated config, grep app logs), use `start-cli package attach nextcloud -n <subcontainer-name> -- <cmd>`. This package has several subcontainers (`nextcloud`, `cron`, `postgres`, `valkey`), so a selector is **required** — select by **name** with `-n` (the name passed to `SubContainer.of` in `main.ts`, e.g. `-n nextcloud`) or by image with `-i`. Note: `-s/--subcontainer` matches the internal **Guid**, not the name.
To run a command inside the service's container (read its generated config, grep app logs), use `start-cli package attach nextcloud -n <subcontainer-name> -- <cmd>`. This package has several subcontainers (`nextcloud-sub`, `nextcloud-cron`, `postgres-sub`, `valkey`), so a selector is **required** — select by **name** with `-n` (the name passed to `SubContainer.of`, e.g. `-n nextcloud-sub`) or by image with `-i`. Note the two nextcloud subcontainers share one image, so `-i nextcloud` is still ambiguous. Note: `-s/--subcontainer` matches the internal **Guid**, not the name.
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,9 @@ This package runs **four containers** as subcontainers:

Architectures: x86_64, aarch64.

**Startup order:** A `chown` one-shot runs first alongside `postgres` and `valkey`. The `nextcloud` container waits until all three are ready before starting. The `cron` container waits for `nextcloud` to be ready. Version upgrades run earlier, during init (see [Installation and First-Run Flow](#installation-and-first-run-flow)); after `nextcloud` is ready, a `finish-upgrade` one-shot completes any interrupted upstream upgrade as a fallback (see [Health Checks](#health-checks)), and the `long-running-tasks` one-shot runs after it.
**Startup order:** Two one-shots run first — `chown` and `pg-recover`. `postgres` waits on `pg-recover`; `valkey` has no prerequisites. The `nextcloud` container waits until `chown`, `postgres` and `valkey` are all ready before starting. The `cron` container waits for `nextcloud` to be ready. Version upgrades run earlier, during init (see [Installation and First-Run Flow](#installation-and-first-run-flow)); after `nextcloud` is ready, a `finish-upgrade` one-shot completes any interrupted upstream upgrade as a fallback (see [Health Checks](#health-checks)), and the `long-running-tasks` one-shot runs after it.

**Unclean shutdown recovery (`pg-recover`):** PostgreSQL writes `postmaster.pid` into its data directory while running and removes it on a clean exit. A stop that does not complete — a power loss, a forced stop, or an update that fails and rolls back — can leave that file behind. On the next start PostgreSQL reads the PID it names and aborts with `FATAL: lock file "postmaster.pid" already exists` if that PID is alive. Each chain build runs in a fresh PID namespace with a fresh, low PID assignment, so the recorded PID is quite likely to be live and to belong to some unrelated process — the guard misfires. The daemon then crash-loops indefinitely, since `pg_isready` reports `loading` rather than a failure. The `pg-recover` one-shot removes the stale file before `postgres` starts, so the database proceeds to normal WAL crash recovery instead. Removal is unconditional and safe because nothing else can hold the data directory: init and backups both run with the service stopped, and the chain reconciler terminates a daemon before starting its replacement. The SDK's own `Backups.withPgDump` does the same before each `pg_ctl start`.

**ffmpeg:** The nextcloud image is built locally (extends `nextcloud:<version>-apache`) to install `ffmpeg`, which Nextcloud's preview providers shell out to for video thumbnails.

Expand Down Expand Up @@ -87,7 +89,7 @@ Valkey runs without a mounted volume — its cache is ephemeral and rebuilds on

**Nextcloud version upgrades:** When the package is updated to a newer Nextcloud release, the upstream upgrade (sync new code → `occ upgrade` → app bookkeeping) runs during init in `setupOnInit`'s `update` branch, before the service starts. It invokes the stock image's entrypoint in headless `NEXTCLOUD_UPDATE=1` mode with a no-op command, alongside temporary `postgres` and `valkey` daemons, via `runUntilSuccess`. Because init runs inside StartOS's update snapshot, a failed upgrade rolls back cleanly instead of stranding the instance. Nextcloud only supports upgrading one major version at a time; a larger jump is detected up front and rejected with a clear error before any change is made. The `finish-upgrade` one-shot (see [Health Checks](#health-checks)) remains as a fallback for an upgrade triggered by restoring an older backup.

**Upgrade from StartOS 0.3.x:** The migration handles PostgreSQL data directory relocation (Debian path to Docker canonical path), `config.yaml` to `config.php` migration, and admin password migration to the new store format. Users must have run the previous Nextcloud version on 0.3.5x at least once (to complete the PG 15 to 17 upgrade) before upgrading.
**Upgrade from StartOS 0.3.x:** The migration handles PostgreSQL data directory relocation (Debian path to Docker canonical path), `config.yaml` to `config.php` migration, and admin password migration to the new store format. Users must have run the previous Nextcloud version on 0.3.5x at least once (to complete the PG 15 to 17 upgrade) before upgrading. The migration locates the cluster by `PG_VERSION` and refuses to start work — leaving the volume untouched and the migration able to run again — if there is no cluster, if that PG 15 to 17 upgrade did not complete (`15/main` present without 0.3.5x's `.pg17_upgrade_complete` marker), if the cluster is not PostgreSQL 17, or if the destination directory is not empty.

---

Expand Down
2 changes: 1 addition & 1 deletion UPDATING.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ curl -fsSL "https://hub.docker.com/v2/repositories/library/postgres/tags?page_si
| jq -r '.results[].name' | grep -E '^17.*alpine' | head
```

Current pin: `postgres:17-alpine` in `startos/manifest/index.ts` (`images.postgres.source.dockerTag`).
Current pin: `postgres:17-alpine` in `startos/manifest/index.ts` (`images.postgres.source.dockerTag`). Changing the major also means changing `PG_MAJOR` in `startos/versions/from035x.ts`, which the 0.3.5x migration compares against the cluster's `PG_VERSION`.

**Valkey** ([valkey/valkey](https://hub.docker.com/r/valkey/valkey) on Docker Hub):

Expand Down
2 changes: 1 addition & 1 deletion nextcloud.Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
ARG NEXTCLOUD_VERSION=33.0.6
ARG NEXTCLOUD_VERSION=33.0.7
FROM nextcloud:${NEXTCLOUD_VERSION}-apache

RUN apt-get update \
Expand Down
3 changes: 3 additions & 0 deletions startos/i18n/dictionaries/default.ts
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ const dict = {
// bootstrapNextcloud.ts: init progress phases
'Installing Nextcloud': 132,
'Upgrading Nextcloud': 133,

// versions/from035x.ts: 0.3.5x migration progress
'Updating file permissions': 134,
} as const

/**
Expand Down
4 changes: 4 additions & 0 deletions startos/i18n/dictionaries/translations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,7 @@ export default {
131: 'Usuarios',
132: 'Instalando Nextcloud',
133: 'Actualizando Nextcloud',
134: 'Actualizando permisos de archivos',
},
de_DE: {
0: 'Starte Nextcloud...',
Expand Down Expand Up @@ -272,6 +273,7 @@ export default {
131: 'Benutzer',
132: 'Nextcloud wird installiert',
133: 'Nextcloud wird aktualisiert',
134: 'Dateiberechtigungen werden aktualisiert',
},
pl_PL: {
0: 'Uruchamianie Nextcloud...',
Expand Down Expand Up @@ -408,6 +410,7 @@ export default {
131: 'Użytkownicy',
132: 'Instalowanie Nextcloud',
133: 'Aktualizowanie Nextcloud',
134: 'Aktualizowanie uprawnień plików',
},
fr_FR: {
0: 'Démarrage de Nextcloud...',
Expand Down Expand Up @@ -544,5 +547,6 @@ export default {
131: 'Utilisateurs',
132: 'Installation de Nextcloud',
133: 'Mise à niveau de Nextcloud',
134: 'Mise à jour des permissions des fichiers',
},
} satisfies Record<string, LangDict>
12 changes: 10 additions & 2 deletions startos/init/bootstrapNextcloud.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ export const bootstrapNextcloud = sdk.setupOnInit(
),
})
} else if (kind === 'update') {
await upgradeNextcloud(effects, progress)
await runUpstreamUpgrade(effects, progress)
}
},
)
Expand All @@ -108,13 +108,21 @@ export const bootstrapNextcloud = sdk.setupOnInit(
* daemon start via the entrypoint, where an interrupted run stranded the
* instance on "Update needed — use the command line updater".
*
* This is the **upstream application** upgrade, triggered by the bundled
* Nextcloud release being newer than the deployed one — not to be confused with
* the one-time **StartOS layout** migration in
* [`../versions/from035x.ts`](../versions/from035x.ts), which is driven by the
* package version graph. Both run during init; `versionGraph` precedes
* `bootstrapNextcloud` in `sdk.setupInit`, so the 0.3.5x migration has always
* finished before this starts.
*
* `NEXTCLOUD_UPDATE=1` makes the stock entrypoint perform the upgrade with a
* no-op command (`true`) and exit, so it never binds a port. `runUntilSuccess`
* brings up Postgres + Valkey (occ upgrade talks to both), runs the upgrade to
* completion, then tears everything down. On failure or timeout it throws,
* which fails init and triggers the snapshot rollback.
*/
async function upgradeNextcloud(effects: T.Effects, progress: InitProgress) {
async function runUpstreamUpgrade(effects: T.Effects, progress: InitProgress) {
// Read the installed (on-volume) and image Nextcloud versions first.
// version.php on the volume is still the installed version — the entrypoint
// syncs new code only once the upgrade runs.
Expand Down
16 changes: 14 additions & 2 deletions startos/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const uiPort = 80 as const
export const NEXTCLOUD_PATH = '/var/www/html' as const
export const POSTGRES_PATH = '/var/lib/postgresql' as const

const NEXTCLOUD_VOLUME_HOST = '/media/startos/volumes/nextcloud' as const
export const NEXTCLOUD_VOLUME_HOST = '/media/startos/volumes/nextcloud' as const

/**
* Throws `errorMessage` if a Nextcloud app's files are not present on the
Expand Down Expand Up @@ -138,6 +138,18 @@ export function getBaseDaemons(
},
requires: [],
})
.addOneshot('pg-recover', {
subcontainer: postgresSub,
exec: {
// An unclean stop strands postmaster.pid, and Postgres aborts if the
// PID it names is alive — which, in a fresh PID namespace, is usually
// an unrelated process. As root, so ownership can never block the
// removal and wedge the chain on this oneshot.
command: ['rm', '-f', `${PGDATA}/postmaster.pid`],
user: 'root',
},
requires: [],
})
.addDaemon('postgres', {
subcontainer: postgresSub,
exec: {
Expand Down Expand Up @@ -167,7 +179,7 @@ export function getBaseDaemons(
}
},
},
requires: [],
requires: ['pg-recover'],
})
.addDaemon('valkey', {
subcontainer: valkeySub,
Expand Down
Loading