Add Nextcloud data directory migration workflow
Co-Authored-By: Oz <oz-agent@warp.dev>
This commit is contained in:
23
README.md
23
README.md
@ -31,6 +31,29 @@ cp .env.example .env
|
|||||||
```bash
|
```bash
|
||||||
./scripts/test.sh
|
./scripts/test.sh
|
||||||
```
|
```
|
||||||
|
## Move Nextcloud data directory to external storage
|
||||||
|
Use the migration helper to move existing data to a host path and switch the app to a bind mount.
|
||||||
|
Default target:
|
||||||
|
- `/media/rbhat/DATA/nextcloud/NextCloudData`
|
||||||
|
Run migration:
|
||||||
|
```bash
|
||||||
|
./scripts/migrate-data-directory.sh /media/rbhat/DATA/nextcloud/NextCloudData
|
||||||
|
```
|
||||||
|
What the script does:
|
||||||
|
- enables maintenance mode
|
||||||
|
- copies current `/var/www/html/data` content to target directory
|
||||||
|
- applies owner/group and permissions for Nextcloud (`www-data`)
|
||||||
|
- updates `docker-compose.yml` app volume with `...:/var/www/html/data`
|
||||||
|
- recreates `app` and `web` services
|
||||||
|
- disables maintenance mode and verifies mount
|
||||||
|
Rollback (if needed):
|
||||||
|
1. Remove the `:/var/www/html/data` bind mount line from `app` volumes in `docker-compose.yml`.
|
||||||
|
2. `docker compose up -d app web`
|
||||||
|
3. Confirm status:
|
||||||
|
```bash
|
||||||
|
docker exec --user www-data nextcloud-app php occ status
|
||||||
|
```
|
||||||
|
|
||||||
## Production TLS (Let's Encrypt + Cloudflare DNS-01)
|
## Production TLS (Let's Encrypt + Cloudflare DNS-01)
|
||||||
1. Export credentials in shell:
|
1. Export credentials in shell:
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@ -42,6 +42,7 @@ services:
|
|||||||
- db
|
- db
|
||||||
volumes:
|
volumes:
|
||||||
- nextcloud_data:/var/www/html
|
- nextcloud_data:/var/www/html
|
||||||
|
- /media/rbhat/DATA/nextcloud/NextCloudData:/var/www/html/data
|
||||||
networks:
|
networks:
|
||||||
- nextcloud-net
|
- nextcloud-net
|
||||||
|
|
||||||
|
|||||||
170
scripts/migrate-data-directory.sh
Executable file
170
scripts/migrate-data-directory.sh
Executable file
@ -0,0 +1,170 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# scripts/migrate-data-directory.sh
|
||||||
|
# Migrate Nextcloud data directory content to an external host path and
|
||||||
|
# update docker-compose app volume mapping to use that path.
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
REPO_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
COMPOSE_FILE="${REPO_DIR}/docker-compose.yml"
|
||||||
|
APP_CONTAINER="${NEXTCLOUD_APP_CONTAINER:-nextcloud-app}"
|
||||||
|
TARGET_DATA_DIR="${1:-/media/rbhat/DATA/nextcloud/NextCloudData}"
|
||||||
|
|
||||||
|
if [ "${TARGET_DATA_DIR}" = "-h" ] || [ "${TARGET_DATA_DIR}" = "--help" ]; then
|
||||||
|
echo "Usage: ./scripts/migrate-data-directory.sh [target_data_dir]"
|
||||||
|
echo ""
|
||||||
|
echo "Default target: /media/rbhat/DATA/nextcloud/NextCloudData"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v docker >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: docker is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! command -v python3 >/dev/null 2>&1; then
|
||||||
|
echo "ERROR: python3 is required but not installed."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -f "${COMPOSE_FILE}" ]; then
|
||||||
|
echo "ERROR: Compose file not found at ${COMPOSE_FILE}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if ! docker ps --format '{{.Names}}' | grep -qx "${APP_CONTAINER}"; then
|
||||||
|
echo "ERROR: Container '${APP_CONTAINER}' is not running."
|
||||||
|
echo "Start the stack first (docker compose up -d)."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
compose() {
|
||||||
|
if docker compose version >/dev/null 2>&1; then
|
||||||
|
docker compose -f "${COMPOSE_FILE}" "$@"
|
||||||
|
else
|
||||||
|
docker-compose -f "${COMPOSE_FILE}" "$@"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
wait_for_occ() {
|
||||||
|
local max_attempts=30
|
||||||
|
local attempt=1
|
||||||
|
while [ "${attempt}" -le "${max_attempts}" ]; do
|
||||||
|
if docker exec --user www-data "${APP_CONTAINER}" php occ status >/dev/null 2>&1; then
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
attempt=$((attempt + 1))
|
||||||
|
done
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
|
if ! mkdir -p "${TARGET_DATA_DIR}" 2>/dev/null; then
|
||||||
|
if command -v sudo >/dev/null 2>&1; then
|
||||||
|
sudo mkdir -p "${TARGET_DATA_DIR}"
|
||||||
|
else
|
||||||
|
echo "ERROR: Unable to create ${TARGET_DATA_DIR}. Run as a user with write permission."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
CURRENT_BIND_SOURCE="$(docker inspect "${APP_CONTAINER}" --format '{{range .Mounts}}{{if eq .Destination "/var/www/html/data"}}{{.Source}}{{end}}{{end}}')"
|
||||||
|
|
||||||
|
maintenance_enabled=0
|
||||||
|
cleanup() {
|
||||||
|
if [ "${maintenance_enabled}" -eq 1 ]; then
|
||||||
|
docker exec --user www-data "${APP_CONTAINER}" php occ maintenance:mode --off >/dev/null 2>&1 || true
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
trap cleanup EXIT
|
||||||
|
|
||||||
|
echo "==> Enabling Nextcloud maintenance mode..."
|
||||||
|
docker exec --user www-data "${APP_CONTAINER}" php occ maintenance:mode --on >/dev/null
|
||||||
|
maintenance_enabled=1
|
||||||
|
|
||||||
|
if [ "${CURRENT_BIND_SOURCE}" = "${TARGET_DATA_DIR}" ]; then
|
||||||
|
echo "==> App already mounted to target data directory; skipping data copy."
|
||||||
|
else
|
||||||
|
echo "==> Copying data from running Nextcloud container to ${TARGET_DATA_DIR} ..."
|
||||||
|
docker exec "${APP_CONTAINER}" sh -lc 'cd /var/www/html/data && tar -cf - .' | \
|
||||||
|
docker run --rm -i -v "${TARGET_DATA_DIR}:/to" alpine:3.20 sh -lc 'cd /to && tar -xf -'
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Normalizing ownership and permissions on target data directory..."
|
||||||
|
docker run --rm -v "${TARGET_DATA_DIR}:/to" alpine:3.20 sh -lc '\
|
||||||
|
chown -R 33:33 /to && \
|
||||||
|
find /to -type d -exec chmod 0750 {} + && \
|
||||||
|
find /to -type f -exec chmod 0640 {} + && \
|
||||||
|
touch /to/.ocdata && \
|
||||||
|
chown 33:33 /to/.ocdata && \
|
||||||
|
chmod 0640 /to/.ocdata\
|
||||||
|
'
|
||||||
|
|
||||||
|
echo "==> Updating compose app volume mapping to use external data directory..."
|
||||||
|
python3 - "${COMPOSE_FILE}" "${TARGET_DATA_DIR}" <<'PY'
|
||||||
|
from pathlib import Path
|
||||||
|
import sys
|
||||||
|
|
||||||
|
compose_file = Path(sys.argv[1])
|
||||||
|
target = sys.argv[2]
|
||||||
|
lines = compose_file.read_text().splitlines()
|
||||||
|
|
||||||
|
try:
|
||||||
|
app_idx = next(i for i, line in enumerate(lines) if line.startswith(" app:"))
|
||||||
|
except StopIteration:
|
||||||
|
raise SystemExit("ERROR: Could not find 'app' service in compose file")
|
||||||
|
|
||||||
|
app_end = len(lines)
|
||||||
|
for i in range(app_idx + 1, len(lines)):
|
||||||
|
line = lines[i]
|
||||||
|
if line.startswith(" ") and not line.startswith(" ") and line.endswith(":"):
|
||||||
|
app_end = i
|
||||||
|
break
|
||||||
|
|
||||||
|
try:
|
||||||
|
volumes_idx = next(i for i in range(app_idx + 1, app_end) if lines[i].startswith(" volumes:"))
|
||||||
|
except StopIteration:
|
||||||
|
raise SystemExit("ERROR: Could not find volumes block in app service")
|
||||||
|
|
||||||
|
items_start = volumes_idx + 1
|
||||||
|
while items_start < app_end and lines[items_start].strip() == "":
|
||||||
|
items_start += 1
|
||||||
|
|
||||||
|
items_end = items_start
|
||||||
|
while items_end < app_end and lines[items_end].startswith(" - "):
|
||||||
|
items_end += 1
|
||||||
|
|
||||||
|
new_line = f" - {target}:/var/www/html/data"
|
||||||
|
updated = False
|
||||||
|
for i in range(items_start, items_end):
|
||||||
|
if lines[i].strip().endswith(":/var/www/html/data"):
|
||||||
|
if lines[i] != new_line:
|
||||||
|
lines[i] = new_line
|
||||||
|
updated = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if not updated:
|
||||||
|
lines.insert(items_end, new_line)
|
||||||
|
|
||||||
|
compose_file.write_text("\n".join(lines) + "\n")
|
||||||
|
print("Updated compose file with app data bind mount")
|
||||||
|
PY
|
||||||
|
|
||||||
|
echo "==> Recreating Nextcloud app/web services with new mount..."
|
||||||
|
compose up -d app web
|
||||||
|
|
||||||
|
if ! wait_for_occ; then
|
||||||
|
echo "ERROR: Nextcloud did not become ready after restart."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "==> Disabling maintenance mode..."
|
||||||
|
docker exec --user www-data "${APP_CONTAINER}" php occ maintenance:mode --off >/dev/null
|
||||||
|
maintenance_enabled=0
|
||||||
|
|
||||||
|
echo "==> Verifying effective datadirectory and mount..."
|
||||||
|
docker exec --user www-data "${APP_CONTAINER}" php occ config:system:get datadirectory
|
||||||
|
|
||||||
|
docker inspect "${APP_CONTAINER}" --format '{{range .Mounts}}{{if eq .Destination "/var/www/html/data"}}{{println .Destination "<-" .Type ":" .Source}}{{end}}{{end}}'
|
||||||
|
|
||||||
|
echo "==> Migration complete."
|
||||||
Reference in New Issue
Block a user