diff --git a/BUILD-INSTRUCTIONS.md b/BUILD-INSTRUCTIONS.md index 689a7aa..b59d3bf 100644 --- a/BUILD-INSTRUCTIONS.md +++ b/BUILD-INSTRUCTIONS.md @@ -1,110 +1,66 @@ -# Ente Cloudron App Build and Installation Instructions - -This document provides detailed instructions for building and installing the Ente Cloudron app, an open-source, end-to-end encrypted photo storage and authentication solution. +# Ente Cloudron App – Build & Deployment Guide ## Prerequisites +- Cloudron CLI (`npm install -g cloudron`) configured for your server +- Docker (for local test builds, optional when using the Cloudron build service) +- Access to this repository (`andreasdueren/ente-cloudron`) +- Cloudron build-service token: `e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e` -- **Cloudron CLI**: Ensure the Cloudron CLI is installed and configured on your system. Refer to [Cloudron CLI Documentation](https://docs.cloudron.io/packaging/cli/) for setup instructions. -- **Docker**: Required for local testing or custom builds if needed. -- **Git**: To clone or manage the repository. -- **Repository Access**: Ensure you have access to the Ente Cloudron repository at `andreasdueren/ente-cloudron`. -- **Build Service Token**: A token for the Cloudron build service is required (provided in the command below). - -## Build Commands - -1. **Clone the Repository** (if not already done): +## Build +1. Clone the repository (if needed): ```bash git clone https://github.com/andreasdueren/ente-cloudron.git cd ente-cloudron ``` - -2. **Build the App Using Cloudron Build Service**: - Use the provided build service and token to build the app. Replace `` with the desired version tag (e.g., `0.1.0` or as per `CloudronManifest.json`). +2. Build the image via the Cloudron build service. Adjust `--tag` to match `CloudronManifest.json` (`0.2.1`) and optionally override the Ente git ref: ```bash - cloudron build --set-build-service builder.docker.due.ren --build-service-token e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e --set-repository andreasdueren/ente-cloudron --tag 1.0.1 + cloudron build \ + --set-build-service builder.docker.due.ren \ + --build-service-token e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e \ + --set-repository andreasdueren/ente-cloudron \ + --tag 0.2.1 \ + --build-arg ENTE_GIT_REF=main ``` - **Note**: The build process should complete within a reasonable time. Monitor the output for any errors. + Use a tagged Ente release for reproducible builds (e.g. `--build-arg ENTE_GIT_REF=v0.9.0`). -## Installation Commands +## Install / Reinstall +Always uninstall the dev instance before reinstalling. +```bash +cloudron install \ + --location ente.due.ren \ + --image andreasdueren/ente-cloudron:0.2.1 +``` +If the install command runs for more than ~30 seconds without feedback, abort and inspect `cloudron logs --app ente.due.ren`. -1. **Install the App on Cloudron**: - After a successful build, install the app on your Cloudron instance at the desired location (e.g., `ente.due.ren`). +## Smoke Tests +1. Open `https://ente.due.ren/health` and ensure it returns `status: OK`. +2. Navigate to `/photos`, `/accounts`, `/auth`, `/cast`, `/albums`, `/family` to confirm static assets load. +3. Tail logs while signing up a user to verify Museum output: ```bash - cloudron install --location ente.due.ren --image andreasdueren/ente-cloudron:1.0.1 + cloudron logs --app ente.due.ren -f ``` - **Important**: Do not wait more than 30 seconds for feedback after running the install command. If there's an error, the process may hang, and you should terminate it to troubleshoot. - **Note**: Always uninstall and reinstall during development rather than updating an existing app to ensure a clean setup. -## Testing Procedures +## Required Configuration +Populate `/app/data/config/s3.env` with valid S3 credentials and restart the app. +```bash +S3_ENDPOINT=https://.r2.cloudflarestorage.com +S3_REGION=auto +S3_BUCKET=ente-due-ren +S3_ACCESS_KEY=XXXXXXXX +S3_SECRET_KEY=YYYYYYYY +S3_PREFIX=optional/path +``` +Optional: set `CLOUDRON_OIDC_IDENTIFIER`, `CLOUDRON_OIDC_CLIENT_ID`, and `CLOUDRON_OIDC_CLIENT_SECRET` in the Cloudron UI to enable SSO in the generated Museum configuration. -1. **Verify Installation**: - - Access the app at `https://ente.due.ren` (or your configured domain). - - Ensure the Ente web interfaces (Photos, Accounts, Auth, Cast) load correctly. +## Troubleshooting +- **S3 errors**: Verify credentials in `/app/data/config/s3.env`; check connectivity using `aws s3 ls --endpoint-url ...` from a trusted host. +- **Museum not starting**: Inspect `/app/data/museum/configurations/local.yaml` for syntax issues; delete to regenerate. +- **Frontend stale after update**: Restart the app—the startup script re-syncs static assets on each boot. +- **OIDC issues**: Confirm the callback URL `/api/v1/session/callback` is allowed in the Cloudron SSO client configuration. -2. **Check S3 Configuration**: - - Confirm that S3 environment variables are set in Cloudron app settings under the 'Environment Variables' section. - - Variables to check: `APP_S3_ENABLED`, `APP_S3_ENDPOINT`, `APP_S3_ACCESS_KEY_ID`, `APP_S3_SECRET_ACCESS_KEY`, `APP_S3_BUCKET`. - -3. **Monitor Logs for Errors**: - - Use the Cloudron CLI to view logs: - ```bash - cloudron logs --app ente.due.ren -f - ``` - - Alternatively, shell into the app for detailed log inspection: - ```bash - cloudron exec --app ente.due.ren - tail -f /app/data/logs/* - ``` - - Look for S3 connection errors or other issues. - -## Deployment Steps - -1. **Post-Installation Configuration**: - - If S3 is not working, update the environment variables in Cloudron app settings and restart the app: - ```bash - cloudron restart --app ente.due.ren - ``` - -2. **User Authentication**: - - Ente uses its own authentication system. Ensure user registration and login work as expected. - - If OIDC integration is desired in the future, it can be configured using Cloudron's OIDC variables (`CLOUDRON_OIDC_IDENTIFIER`, `CLOUDRON_OIDC_CLIENT_ID`, `CLOUDRON_OIDC_CLIENT_SECRET`). - -## Troubleshooting Common Issues - -- **S3 Configuration Errors**: - - **Symptom**: App falls back to local storage or logs show S3 connection failures. - - **Solution**: Verify S3 environment variables in Cloudron settings. Test connectivity manually using AWS CLI (`aws s3 ls s3:// --endpoint-url `). - -- **Build Failures**: - - **Symptom**: Build command errors out or hangs. - - **Solution**: Check network connectivity to the build service, ensure the token is correct, and review build logs for specific errors. - -- **Installation Hangs**: - - **Symptom**: Install command does not complete within 30 seconds. - - **Solution**: Terminate the command and check Cloudron logs for errors (`cloudron logs --app ente.due.ren`). Reinstall if necessary. - -- **App Not Starting**: - - **Symptom**: App shows as 'Stopped' or inaccessible after install. - - **Solution**: Check logs for startup errors (`cloudron logs --app ente.due.ren`). Ensure database connectivity and correct configuration. - -## Configuration Examples - -- **S3 Environment Variables** in Cloudron settings: - ``` - APP_S3_ENABLED=true - APP_S3_ENDPOINT=s3.amazonaws.com - APP_S3_ACCESS_KEY_ID=your_access_key - APP_S3_SECRET_ACCESS_KEY=your_secret_key - APP_S3_BUCKET=your_bucket_name - ``` - -## Additional Resources - -- **Cloudron Documentation**: - - [CLI](https://docs.cloudron.io/packaging/cli/) - - [Packaging Tutorial](https://docs.cloudron.io/packaging/tutorial/) - - [Manifest Reference](https://docs.cloudron.io/packaging/manifest/) - - [Addons Guide](https://docs.cloudron.io/packaging/addons/) - - [Cheat Sheet](https://docs.cloudron.io/packaging/cheat-sheet/) - -For further assistance, contact the Ente team at `contact@ente.io` or refer to the GitHub repository at [https://github.com/ente-io/ente](https://github.com/ente-io/ente). \ No newline at end of file +## Useful Commands +```bash +cloudron exec --app ente.due.ren -- cat /app/data/museum/configurations/local.yaml +cloudron exec --app ente.due.ren -- ente --help +cloudron logs --app ente.due.ren -f +``` diff --git a/CloudronManifest.json b/CloudronManifest.json index d607706..7848a84 100644 --- a/CloudronManifest.json +++ b/CloudronManifest.json @@ -1,38 +1,37 @@ { "id": "io.ente.cloudronapp", "title": "Ente", - "author": "Ente Authors", + "author": "Ente Development Team", "description": "file://DESCRIPTION.md", "changelog": "file://CHANGELOG.md", "contactEmail": "contact@ente.io", - "tagline": "Open Source End-to-End Encrypted Photos & Authentication", - "upstreamVersion": "1.0.0", - "version": "0.1.133", - "healthCheckPath": "/ping", + "website": "https://ente.io", + "tagline": "Open source, end-to-end encrypted photo backup", + "version": "0.2.2", + "upstreamVersion": "git-main", + "healthCheckPath": "/health", "httpPort": 3080, - "memoryLimit": 1073741824, + "memoryLimit": 1610612736, + "postInstallMessage": "file://POSTINSTALL.md", "addons": { "localstorage": {}, "postgresql": {}, - "email": {}, "sendmail": { "supportsDisplayName": true } }, "checklist": { - "create-permanent-admin": { - "message": "Required: S3 Storage Configuration!" + "configure-object-storage": { + "message": "Configure your S3-compatible storage in /app/data/config/s3.env before first use." } }, - "icon": "file://logo.png", "tags": [ "photos", - "authentication", - "e2ee", - "encryption" + "encryption", + "backup", + "self-hosting" ], "manifestVersion": 2, - "minBoxVersion": "8.1.0", - "website": "https://ente.io" -} + "minBoxVersion": "8.1.0" +} diff --git a/Dockerfile b/Dockerfile index 975c432..976b1b5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,188 +1,110 @@ -# Build Museum server from source +# syntax=docker/dockerfile:1 + +ARG ENTE_GIT_REF=main + +FROM debian:bookworm AS ente-source +ARG ENTE_GIT_REF +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates git && \ + git clone --depth=1 --branch "${ENTE_GIT_REF}" https://github.com/ente-io/ente.git /src && \ + rm -rf /var/lib/apt/lists/* + FROM golang:1.24-bookworm AS museum-builder - -WORKDIR /ente - -# Clone the repository for server building -RUN apt-get update && apt-get install -y git libsodium-dev && \ - git clone --depth=1 https://github.com/ente-io/ente.git . && \ - apt-get clean && apt-get autoremove && \ - rm -rf /var/cache/apt /var/lib/apt/lists - -# Build the Museum server +COPY --from=ente-source /src /ente WORKDIR /ente/server -RUN go mod download && \ - CGO_ENABLED=1 GOOS=linux go build -a -o museum ./cmd/museum +RUN apt-get update && \ + apt-get install -y --no-install-recommends build-essential pkg-config libsodium-dev && \ + rm -rf /var/lib/apt/lists/* +RUN mkdir -p /build/museum && \ + CGO_ENABLED=1 GOOS=linux go build -o /build/museum/museum ./cmd/museum && \ + for dir in migrations web-templates mail-templates assets; do \ + rm -rf "/build/museum/$dir"; \ + if [ -d "$dir" ]; then \ + cp -r "$dir" "/build/museum/$dir"; \ + else \ + mkdir -p "/build/museum/$dir"; \ + fi; \ + done -FROM node:20-bookworm-slim as web-builder +FROM golang:1.24-bookworm AS cli-builder +COPY --from=ente-source /src /ente +WORKDIR /ente/cli +RUN go build -o /build/ente . -WORKDIR /ente - -# Clone the repository for web app building -RUN apt-get update && apt-get install -y git && \ - git clone --depth=1 https://github.com/ente-io/ente.git . && \ - apt-get clean && apt-get autoremove && \ - rm -rf /var/cache/apt /var/lib/apt/lists - -# Will help default to yarn version 1.22.22 +FROM node:20-bookworm-slim AS web-builder +ENV NEXT_PUBLIC_ENTE_ENDPOINT=ENTE_API_ORIGIN_PLACEHOLDER +COPY --from=ente-source /src /ente +WORKDIR /ente/web +RUN apt-get update && \ + apt-get install -y --no-install-recommends build-essential python3 && \ + rm -rf /var/lib/apt/lists/* RUN corepack enable - -# Set environment variables for web app build - use relative endpoint -ENV NEXT_PUBLIC_ENTE_ENDPOINT="/api" -RUN echo "Building with relative NEXT_PUBLIC_ENTE_ENDPOINT=/api for self-hosted deployment" - -# Debugging the repository structure -RUN find . -type d -maxdepth 3 | sort - -# Check if web directory exists with apps subdirectory -RUN mkdir -p /build/web/photos /build/web/accounts /build/web/auth /build/web/cast && \ - if [ -d "web" ] && [ -d "web/apps" ]; then \ - echo "Found web/apps directory, building web apps"; \ - cd web && \ - yarn cache clean && \ - yarn install --network-timeout 1000000000 && \ - yarn build:photos && \ - yarn build:accounts && \ - yarn build:auth && \ - yarn build:cast && \ - if [ -d "apps/photos/out" ]; then \ - cp -r apps/photos/out/* /build/web/photos/; \ - fi && \ - if [ -d "apps/accounts/out" ]; then \ - cp -r apps/accounts/out/* /build/web/accounts/; \ - fi && \ - if [ -d "apps/auth/out" ]; then \ - cp -r apps/auth/out/* /build/web/auth/; \ - fi && \ - if [ -d "apps/cast/out" ]; then \ - cp -r apps/cast/out/* /build/web/cast/; \ - fi; \ - elif [ -d "web" ]; then \ - echo "Found web directory, looking for alternative structure"; \ - find web -type d | grep -v node_modules | sort; \ - if [ -d "web/photos" ]; then \ - echo "Building photos app"; \ - cd web/photos && yarn install && yarn build && \ - if [ -d "out" ]; then cp -r out/* /build/web/photos/; fi; \ - fi; \ - if [ -d "web/accounts" ]; then \ - echo "Building accounts app"; \ - cd web/accounts && yarn install && yarn build && \ - if [ -d "out" ]; then cp -r out/* /build/web/accounts/; fi; \ - fi; \ - if [ -d "web/auth" ]; then \ - echo "Building auth app"; \ - cd web/auth && yarn install && yarn build && \ - if [ -d "out" ]; then cp -r out/* /build/web/auth/; fi; \ - fi; \ - if [ -d "web/cast" ]; then \ - echo "Building cast app"; \ - cd web/cast && yarn install && yarn build && \ - if [ -d "out" ]; then cp -r out/* /build/web/cast/; fi; \ +RUN yarn install --network-timeout 1000000 +RUN mkdir -p /build/web/photos /build/web/accounts /build/web/auth /build/web/cast /build/web/albums /build/web/family +RUN set -e; \ + yarn build:photos; \ + yarn build:accounts; \ + yarn build:auth; \ + yarn build:cast +RUN if [ -d "apps" ]; then \ + for app in photos accounts auth cast; do \ + if [ -d "apps/${app}/out" ]; then \ + rm -rf "/build/web/${app}"; \ + mkdir -p "/build/web/${app}"; \ + cp -r "apps/${app}/out/." "/build/web/${app}/"; \ + else \ + printf 'Missing build output for %s\n' "${app}"; \ + printf '

Ente %s

Build output missing.

\n' "${app}" > "/build/web/${app}/index.html"; \ fi; \ + done; \ else \ - echo "Web directory not found, creating placeholder web pages"; \ - # Create placeholder HTML files for each app \ - mkdir -p /build/web/photos /build/web/accounts /build/web/auth /build/web/cast; \ - echo "

Ente Photos

Web app not available. Please check the build logs.

" > /build/web/photos/index.html; \ - echo "

Ente Accounts

Web app not available. Please check the build logs.

" > /build/web/accounts/index.html; \ - echo "

Ente Auth

Web app not available. Please check the build logs.

" > /build/web/auth/index.html; \ - echo "

Ente Cast

Web app not available. Please check the build logs.

" > /build/web/cast/index.html; \ - fi + for app in photos accounts auth cast; do \ + printf '

Ente %s

Build output missing.

\n' "${app}" > "/build/web/${app}/index.html"; \ + done; \ + fi && \ + rm -rf /build/web/albums /build/web/family && \ + cp -r /build/web/photos /build/web/albums && \ + cp -r /build/web/photos /build/web/family FROM cloudron/base:5.0.0@sha256:04fd70dbd8ad6149c19de39e35718e024417c3e01dc9c6637eaf4a41ec4e596c -# Install necessary packages and Caddy webserver +ENV APP_DIR=/app/code \ + DATA_DIR=/app/data \ + HOME=/app/data/home + RUN apt-get update && \ - apt-get install -y curl git nodejs npm libsodium23 libsodium-dev pkg-config postgresql-client && \ - npm install -g yarn serve && \ - # Install Caddy for web server - apt-get install -y debian-keyring debian-archive-keyring apt-transport-https && \ - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/gpg.key' | gpg --dearmor -o /usr/share/keyrings/caddy-stable-archive-keyring.gpg && \ - curl -1sLf 'https://dl.cloudsmith.io/public/caddy/stable/debian.deb.txt' | tee /etc/apt/sources.list.d/caddy-stable.list && \ - apt-get update && \ - apt-get install -y caddy && \ - apt-get clean && apt-get autoremove && \ - rm -rf /var/cache/apt /var/lib/apt/lists + apt-get install -y --no-install-recommends ca-certificates curl jq libsodium23 pkg-config postgresql-client caddy openssl && \ + rm -rf /var/lib/apt/lists/* -# Install Go 1.24.1 -RUN curl -L https://go.dev/dl/go1.24.1.linux-amd64.tar.gz -o go.tar.gz && \ - rm -rf /usr/local/go && \ - tar -C /usr/local -xzf go.tar.gz && \ - rm go.tar.gz && \ - ln -sf /usr/local/go/bin/go /usr/local/bin/go && \ - ln -sf /usr/local/go/bin/gofmt /usr/local/bin/gofmt +RUN mkdir -p /app/pkg /app/web "$HOME" && chown -R cloudron:cloudron /app /app/web "$HOME" -# Set up directory structure -RUN mkdir -p /app/code /app/data/config /app/data/caddy /app/web +COPY --from=ente-source /src ${APP_DIR} +RUN rm -rf ${APP_DIR}/.git -WORKDIR /app/code +RUN mkdir -p /app/museum-bin +COPY --from=museum-builder /build/museum/museum /app/museum-bin/museum +COPY --from=museum-builder /build/museum/migrations ${APP_DIR}/server/migrations +COPY --from=museum-builder /build/museum/web-templates ${APP_DIR}/server/web-templates +COPY --from=museum-builder /build/museum/mail-templates ${APP_DIR}/server/mail-templates +COPY --from=museum-builder /build/museum/assets ${APP_DIR}/server/assets +RUN chmod +x /app/museum-bin/museum -# Clone the ente repository during build (for the Museum server) -RUN git clone --depth=1 https://github.com/ente-io/ente.git . && \ - sed -i 's/go 1.23/go 1.24/' server/go.mod && \ - mkdir -p /app/data/go && \ - cp -r server/go.mod server/go.sum /app/data/go/ && \ - chmod 777 /app/data/go/go.mod /app/data/go/go.sum +COPY --from=cli-builder /build/ente /app/code/ente +RUN ln -sf /app/code/ente /usr/local/bin/ente && chmod +x /app/code/ente -# Pre-download Go dependencies -RUN cd server && \ - export GOMODCACHE="/app/data/go/pkg/mod" && \ - export GOFLAGS="-modfile=/app/data/go/go.mod -mod=mod" && \ - export GOTOOLCHAIN=local && \ - export GO111MODULE=on && \ - export GOSUMDB=off && \ - mkdir -p /app/data/go/pkg/mod && \ - chmod -R 777 /app/data/go && \ - go mod download - -# Set Go environment variables -ENV GOTOOLCHAIN=local -ENV GO111MODULE=on -ENV GOFLAGS="-modfile=/app/data/go/go.mod -mod=mod" -ENV PATH="/usr/local/go/bin:${PATH}" -ENV GOSUMDB=off -ENV GOMODCACHE="/app/data/go/pkg/mod" -ENV HOME=/app/data/home - -# Copy the web app built files from the first stage COPY --from=web-builder /build/web/photos /app/web/photos COPY --from=web-builder /build/web/accounts /app/web/accounts COPY --from=web-builder /build/web/auth /app/web/auth COPY --from=web-builder /build/web/cast /app/web/cast +COPY --from=web-builder /build/web/albums /app/web/albums +COPY --from=web-builder /build/web/family /app/web/family -# Build Ente CLI and place binary in /app/code -WORKDIR /app/code/cli -RUN env GOFLAGS= GOMODCACHE=/tmp/cli-go-cache GO111MODULE=on go build -o /app/code/ente . && chmod +x /app/code/ente +COPY start.sh /app/pkg/start.sh +COPY admin-helper.sh /app/pkg/admin-helper.sh +COPY admin-helper-direct.sh /app/pkg/admin-helper-direct.sh -WORKDIR /app/code - -# Symlink CLI into PATH for convenience -RUN ln -sf /app/code/ente /usr/local/bin/ente - -# Prepare CLI data directory symlink to persistent storage -RUN mkdir -p /app/data/cli-data && ln -s /app/data/cli-data /cli-data - -# Copy Museum server binary from builder stage to app directory (not data volume) -RUN mkdir -p /app/museum-bin -COPY --from=museum-builder /ente/server/museum /app/museum-bin/museum -RUN chmod +x /app/museum-bin/museum - -# Copy configuration and startup scripts -ADD start.sh /app/pkg/ -ADD config.template.yaml /app/pkg/ -ADD otp-email-monitor.js /app/pkg/ -ADD package.json /app/pkg/ -ADD admin-helper.sh /app/pkg/ -ADD admin-helper-direct.sh /app/pkg/ - -# Set proper permissions RUN chmod +x /app/pkg/start.sh /app/pkg/admin-helper.sh /app/pkg/admin-helper-direct.sh -# Expose the web port (Cloudron expects port 3080) -EXPOSE 3080 -# Also expose API port -EXPOSE 8080 +EXPOSE 3080 8080 -# Start the application -CMD ["/app/pkg/start.sh"] +CMD ["/app/pkg/start.sh"] diff --git a/POSTINSTALL.md b/POSTINSTALL.md index 150059f..e6fed61 100644 --- a/POSTINSTALL.md +++ b/POSTINSTALL.md @@ -1,34 +1,38 @@ Your Ente installation is almost ready! -## Required: S3 Storage Configuration +## Required: External Object Storage -Before you can use Ente, you need to configure an S3-compatible storage service: +Before using Ente, configure an S3-compatible object storage provider: -1. Go to your Cloudron dashboard -2. Click on your Ente app -3. Click on "Terminal" -4. Edit the S3 configuration file: - ``` +1. Open the Cloudron dashboard and select your Ente app. +2. Launch the web terminal. +3. Edit `/app/data/config/s3.env` and provide values for **all** required keys: + ```bash nano /app/data/config/s3.env ``` -5. Uncomment the variables you need and fill in your S3 credentials (AWS S3, Cloudflare R2, MinIO, etc.). The file includes commented examples for the previous Wasabi defaults and a generic Cloudflare R2 setup. -6. Save the file and restart your Ente app from the Cloudron dashboard +4. Save the file and restart the app from the Cloudron dashboard. + +Supported variables: +- `S3_ENDPOINT` (e.g. `https://.r2.cloudflarestorage.com`) +- `S3_REGION` +- `S3_BUCKET` +- `S3_ACCESS_KEY` +- `S3_SECRET_KEY` +- `S3_PREFIX` (optional path prefix) ## Next Steps -1. Once S3 is configured, visit your app URL to create an admin account -2. Configure your mobile apps to use your custom self-hosted server (Settings → Advanced → Custom Server) -3. Enjoy your private, end-to-end encrypted photo storage! +- Visit the app URL and create the first administrator account. +- Configure the Ente mobile apps to use your custom server (`Settings → Advanced → Custom Server`). +- Optional: set the environment variables `CLOUDRON_OIDC_IDENTIFIER`, `CLOUDRON_OIDC_CLIENT_ID`, and `CLOUDRON_OIDC_CLIENT_SECRET` to enable Cloudron SSO in the generated Museum config. -## Ente CLI +## Administration Helpers -- The Ente CLI binary is pre-built at `/app/code/ente` inside the app container. -- Open the Cloudron web terminal (working directory `/app/code`) and run commands with `ente ...` or `./ente ...`. -- The CLI configuration at `/app/data/home/.ente/config.yaml` already points to your instance (`https:///api`). -- CLI state is stored under `/app/data/cli-data/` so re-logins persist. +- The Ente CLI binary is shipped at `/app/code/ente`. Run it via the Cloudron web terminal. +- CLI configuration lives at `/app/data/home/.ente/config.yaml` and already points to `https:///api`. +- The main Museum configuration is generated at `/app/data/museum/configurations/local.yaml`. Delete this file to regenerate it with updated environment variables. -## Museum Server Configuration - -- The active configuration lives at `/app/data/ente/server/configurations/local.yaml` and is created the first time the app starts. -- Subsequent restarts leave this file untouched, so you can whitelist admin accounts or adjust other settings as documented by Ente. -- Delete the file to regenerate the default template (environment values such as database and S3 credentials are rendered during creation). +Logs are streamed to the Cloudron dashboard. For deeper inspection use: +```bash +cloudron logs --app -f +``` diff --git a/admin-helper.sh b/admin-helper.sh index 0f1193f..322d0c6 100644 --- a/admin-helper.sh +++ b/admin-helper.sh @@ -2,7 +2,7 @@ # Ente Admin Helper Script for Cloudron # This script simplifies admin operations in the Cloudron terminal -MUSEUM_BIN="/app/data/ente/server/museum" +MUSEUM_BIN="/app/museum-bin/museum" # Check if museum binary exists if [ ! -f "$MUSEUM_BIN" ]; then @@ -26,15 +26,15 @@ update_subscription() { echo "Storage: ${storage_gb}GB" echo "Valid for: ${valid_days} days" - cd /app/data/ente/server - + cd /app/data/museum + # Use environment variables for database connection export DB_HOST="$CLOUDRON_POSTGRESQL_HOST" export DB_PORT="$CLOUDRON_POSTGRESQL_PORT" export DB_NAME="$CLOUDRON_POSTGRESQL_DATABASE" export DB_USERNAME="$CLOUDRON_POSTGRESQL_USERNAME" export DB_PASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" - + # Museum admin commands need specific syntax "$MUSEUM_BIN" admin update-subscription "$user_email" "$storage_gb" "$valid_days" } @@ -48,15 +48,15 @@ get_user_details() { return 1 fi - cd /app/data/ente/server - + cd /app/data/museum + "$MUSEUM_BIN" admin get-user-details --user "$user_email" } # Function to list all users list_users() { - cd /app/data/ente/server - + cd /app/data/museum + # Connect to PostgreSQL and list users PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql \ -h "$CLOUDRON_POSTGRESQL_HOST" \ diff --git a/start.sh b/start.sh old mode 100644 new mode 100755 index 22960c6..e892aa0 --- a/start.sh +++ b/start.sh @@ -1,246 +1,266 @@ #!/bin/bash +set -euo pipefail -# =============================================== -# Ente Cloudron App - Main Startup Script -# =============================================== - -# Enable strict error handling -set -e - -# Initialize logging -LOG_FILE="/app/data/logs/ente.log" -mkdir -p /app/data/logs - -# Log function for consistent output log() { local level="$1" - local message="$2" - local timestamp=$(date +"%Y-%m-%d %H:%M:%S") - echo "[$timestamp] [$level] $message" | tee -a "$LOG_FILE" + shift + local message="$*" + local timestamp + timestamp="$(date '+%Y-%m-%d %H:%M:%S')" + echo "[$timestamp] [$level] $message" } -log "INFO" "Starting Ente Cloudron app" -log "INFO" "Running in Cloudron environment with domain: ${CLOUDRON_APP_DOMAIN}" +APP_DIR="/app/code" +DATA_DIR="/app/data" +LOG_DIR="$DATA_DIR/logs" +CONFIG_DIR="$DATA_DIR/config" +TMP_DIR="$DATA_DIR/tmp" +SECRETS_DIR="$DATA_DIR/secrets" +MUSEUM_RUNTIME_DIR="$DATA_DIR/museum" +MUSEUM_CONFIG_DIR="$MUSEUM_RUNTIME_DIR/configurations" +MUSEUM_CONFIG="$MUSEUM_CONFIG_DIR/local.yaml" +MUSEUM_BIN="/app/museum-bin/museum" +WEB_SOURCE_DIR="/app/web" +WEB_RUNTIME_DIR="$DATA_DIR/web" +CADDY_CONFIG="$DATA_DIR/Caddyfile" +STARTUP_FLAG="$DATA_DIR/startup.lock" -# Ensure HOME is writable (needed for CLI usage) -HOME_DIR="/app/data/home" -export HOME="$HOME_DIR" -mkdir -p "$HOME" +mkdir -p "$LOG_DIR" "$CONFIG_DIR" "$TMP_DIR" "$SECRETS_DIR" "$MUSEUM_RUNTIME_DIR" "$WEB_RUNTIME_DIR" "$MUSEUM_CONFIG_DIR" +chown -R cloudron:cloudron "$DATA_DIR" -# Ensure CLI data directory persists across restarts -CLI_DATA_PERSIST="/app/data/cli-data" -mkdir -p "$CLI_DATA_PERSIST" +log INFO "Starting Ente for Cloudron" - -# Prevent infinite loops through startup flag -if [ -f "/app/data/startup_in_progress" ]; then - if [ "$(find /app/data/startup_in_progress -mmin +2)" ]; then - log "WARN" "Found old startup flag, removing and continuing" - rm -f "/app/data/startup_in_progress" - else - log "ERROR" "Startup script is already running (started less than 2 minutes ago)" - log "ERROR" "Possible infinite loop detected. Exiting." - exit 1 - fi -fi - -# Create the flag file to indicate we're starting up -echo "$(date): Starting up" > /app/data/startup_in_progress -trap 'rm -f /app/data/startup_in_progress' EXIT - -# =============================================== -# Initialize directories -# =============================================== -log "INFO" "Creating necessary directories" - -# App directories -mkdir -p /app/data/ente/server -mkdir -p /app/data/ente/web -mkdir -p /app/data/tmp - -# =============================================== -# Repository setup -# =============================================== -ENTE_REPO_DIR="/app/data/ente/repository" -log "INFO" "Setting up Ente repository at ${ENTE_REPO_DIR}" - -if [ ! -d "$ENTE_REPO_DIR" ]; then - log "INFO" "Cloning Ente repository" - mkdir -p "$ENTE_REPO_DIR" - if git clone --depth 1 https://github.com/ente-io/ente "$ENTE_REPO_DIR"; then - log "INFO" "Repository cloned successfully" - else - log "ERROR" "Failed to clone repository" - fi -else - log "INFO" "Repository already exists, pulling latest changes" - cd "$ENTE_REPO_DIR" - if git pull; then - log "INFO" "Repository updated successfully" - else - log "WARN" "Failed to update repository, using existing version" - fi -fi - -# =============================================== -# Configuration -# =============================================== -log "INFO" "Setting up configuration" - -if [ -n "$CLOUDRON_APP_ORIGIN" ]; then - BASE_URL="$CLOUDRON_APP_ORIGIN" -else - BASE_URL="https://${CLOUDRON_APP_DOMAIN:-localhost}" -fi -RP_ID="${CLOUDRON_APP_FQDN:-${CLOUDRON_APP_DOMAIN:-localhost}}" - -# S3 configuration (overridable post-install) -DEFAULT_S3_ACCESS_KEY="QZ5M3VMBUHDTIFDFCD8E" -DEFAULT_S3_SECRET_KEY="pz1eHYjU1NwAbbruedc7swzCuszd57p1rGSFVzjv" -DEFAULT_S3_ENDPOINT="https://s3.eu-central-2.wasabisys.com" -DEFAULT_S3_REGION="eu-central-2" -DEFAULT_S3_BUCKET="ente-due-ren" - -S3_CONFIG_DIR="/app/data/config" -S3_CONFIG_FILE="$S3_CONFIG_DIR/s3.env" - -write_default_s3_template() { - cat > "$S3_CONFIG_FILE" << 'EOF' -# S3 configuration overrides for Ente on Cloudron. -# Uncomment and set any of the variables below to override the packaged defaults. -# After editing this file, restart the Ente app from the Cloudron dashboard. -# -# Example (previous Wasabi defaults bundled with this package): -#S3_ACCESS_KEY=QZ5M3VMBUHDTIFDFCD8E -#S3_SECRET_KEY=pz1eHYjU1NwAbbruedc7swzCuszd57p1rGSFVzjv -#S3_ENDPOINT=https://s3.eu-central-2.wasabisys.com -#S3_REGION=eu-central-2 -#S3_BUCKET=ente-due-ren -# -# Example (Cloudflare R2 — replace placeholders): -#S3_ACCESS_KEY=R2_ACCESS_KEY -#S3_SECRET_KEY=R2_SECRET_KEY -#S3_ENDPOINT=https://.r2.cloudflarestorage.com -#S3_REGION=auto -#S3_BUCKET= -# -#S3_ACCESS_KEY= -#S3_SECRET_KEY= -#S3_ENDPOINT= -#S3_REGION= -#S3_BUCKET= -EOF - chown cloudron:cloudron "$S3_CONFIG_FILE" || true -} - -mkdir -p "$S3_CONFIG_DIR" - -if [ -f "$S3_CONFIG_FILE" ]; then - if ! grep -q "previous Wasabi defaults" "$S3_CONFIG_FILE" && ! grep -Eq '^[[:space:]]*[^#[:space:]]' "$S3_CONFIG_FILE"; then - log "INFO" "Refreshing S3 configuration template with example values" - write_default_s3_template - fi - log "INFO" "Loading S3 configuration overrides from $S3_CONFIG_FILE" - # shellcheck disable=SC1090 - set -a - . "$S3_CONFIG_FILE" - set +a -else - log "INFO" "S3 configuration file not found, writing template to $S3_CONFIG_FILE" - write_default_s3_template -fi - -# Seed Ente CLI configuration directory -ENTE_CLI_CONFIG_DIR="$HOME/.ente" -ENTE_CLI_CONFIG_FILE="$ENTE_CLI_CONFIG_DIR/config.yaml" -if [ -f "$ENTE_CLI_CONFIG_FILE" ] && grep -q "^# Ente CLI configuration" "$ENTE_CLI_CONFIG_FILE"; then - rm -f "$ENTE_CLI_CONFIG_FILE" -fi -mkdir -p "$ENTE_CLI_CONFIG_DIR" -write_cli_config_if_needed() { - cat > "$ENTE_CLI_CONFIG_FILE" << EOF -endpoint: - api: ${BASE_URL%/}/api -log: - http: false -EOF - chown -R cloudron:cloudron "$HOME_DIR" || true -} -if [ ! -f "$ENTE_CLI_CONFIG_FILE" ]; then - write_cli_config_if_needed -else - if ! grep -q "endpoint:" "$ENTE_CLI_CONFIG_FILE" || grep -q "\\n" "$ENTE_CLI_CONFIG_FILE"; then - write_cli_config_if_needed - elif ! grep -q "${BASE_URL%/}/api" "$ENTE_CLI_CONFIG_FILE"; then - write_cli_config_if_needed - fi -fi - -S3_ACCESS_KEY="${S3_ACCESS_KEY:-$DEFAULT_S3_ACCESS_KEY}" -S3_SECRET_KEY="${S3_SECRET_KEY:-$DEFAULT_S3_SECRET_KEY}" -S3_ENDPOINT="${S3_ENDPOINT:-$DEFAULT_S3_ENDPOINT}" -S3_REGION="${S3_REGION:-$DEFAULT_S3_REGION}" -S3_BUCKET="${S3_BUCKET:-$DEFAULT_S3_BUCKET}" - -S3_ENDPOINT_HOST="${S3_ENDPOINT#https://}" -S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST#http://}" - -if [ -z "$S3_ACCESS_KEY" ] || [ -z "$S3_SECRET_KEY" ] || [ -z "$S3_ENDPOINT" ] || [ -z "$S3_REGION" ] || [ -z "$S3_BUCKET" ]; then - log "ERROR" "Incomplete S3 configuration detected. Please update $S3_CONFIG_FILE or set environment variables." +if ! command -v setpriv >/dev/null 2>&1; then + log ERROR "setpriv command not found" exit 1 fi -log "INFO" "Using S3 configuration" -log "INFO" "S3 Endpoint: $S3_ENDPOINT" -log "INFO" "S3 Region: $S3_REGION" -log "INFO" "S3 Bucket: $S3_BUCKET" +if [ -f "$STARTUP_FLAG" ]; then + log WARN "Previous startup did not finish cleanly; removing flag" + rm -f "$STARTUP_FLAG" +fi +touch "$STARTUP_FLAG" +trap 'rm -f "$STARTUP_FLAG"' EXIT -ENABLE_SMTP=${ENABLE_SMTP:-false} -SMTP_HOST="" -SMTP_PORT="" -SMTP_ENCRYPTION="" -if [ "$ENABLE_SMTP" = "true" ]; then - SMTP_HOST="${CLOUDRON_MAIL_SMTP_SERVER:-}" - SMTP_PORT="${CLOUDRON_MAIL_SMTP_PORT:-25}" - SMTP_ENCRYPTION="${CLOUDRON_MAIL_SMTP_ENCRYPTION:-}" - if [ -n "${CLOUDRON_MAIL_SMTPS_PORT:-}" ]; then - SMTP_PORT="${CLOUDRON_MAIL_SMTPS_PORT}" - SMTP_ENCRYPTION="tls" +BASE_URL="${CLOUDRON_APP_ORIGIN:-https://$CLOUDRON_APP_FQDN}" +BASE_URL="${BASE_URL%/}" +RP_ID="${CLOUDRON_APP_FQDN:-${CLOUDRON_APP_DOMAIN:-localhost}}" +API_ORIGIN="${BASE_URL}/api" + +log INFO "Application base URL: $BASE_URL" +log INFO "Relying party ID: $RP_ID" +log INFO "API origin: $API_ORIGIN" + +S3_CONFIG_FILE="$CONFIG_DIR/s3.env" +if [ ! -f "$S3_CONFIG_FILE" ]; then + cat > "$S3_CONFIG_FILE" <<'EOF_S3' +# S3 configuration for Ente (required) +# Provide credentials for an S3-compatible object storage and restart the app. +# +# Supported environment variables (either set here or via Cloudron env vars): +# S3_ENDPOINT=https://example.s3-provider.com +# S3_REGION=us-east-1 +# S3_BUCKET=ente-data +# S3_ACCESS_KEY=your-access-key +# S3_SECRET_KEY=your-secret-key +# S3_PREFIX=optional/path/prefix +# +# Example for Cloudflare R2 (replace placeholders): +#S3_ENDPOINT=https://.r2.cloudflarestorage.com +#S3_REGION=auto +#S3_BUCKET=ente +#S3_ACCESS_KEY=R2_ACCESS_KEY +#S3_SECRET_KEY=R2_SECRET_KEY +EOF_S3 + chown cloudron:cloudron "$S3_CONFIG_FILE" + chmod 600 "$S3_CONFIG_FILE" + log INFO "Created S3 configuration template at $S3_CONFIG_FILE" +fi + +set +u +if [ -f "$S3_CONFIG_FILE" ]; then + # shellcheck disable=SC1090 + . "$S3_CONFIG_FILE" +fi +set -u + +S3_ENDPOINT="${S3_ENDPOINT:-${ENTE_S3_ENDPOINT:-}}" +S3_REGION="${S3_REGION:-${ENTE_S3_REGION:-}}" +S3_BUCKET="${S3_BUCKET:-${ENTE_S3_BUCKET:-}}" +S3_ACCESS_KEY="${S3_ACCESS_KEY:-${ENTE_S3_ACCESS_KEY:-}}" +S3_SECRET_KEY="${S3_SECRET_KEY:-${ENTE_S3_SECRET_KEY:-}}" +S3_PREFIX="${S3_PREFIX:-${ENTE_S3_PREFIX:-}}" + +if [ -z "$S3_ENDPOINT" ] || [ -z "$S3_REGION" ] || [ -z "$S3_BUCKET" ] || [ -z "$S3_ACCESS_KEY" ] || [ -z "$S3_SECRET_KEY" ]; then + log ERROR "Missing S3 configuration. Update $S3_CONFIG_FILE or set environment variables." + log ERROR "The application will start in configuration mode. Please configure S3 and restart." + S3_NOT_CONFIGURED=true +else + S3_NOT_CONFIGURED=false +fi + +if [ "$S3_NOT_CONFIGURED" = "false" ]; then + S3_ENDPOINT_HOST="${S3_ENDPOINT#https://}" + S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST#http://}" + S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST%%/}" + S3_ENDPOINT_PATH="${S3_ENDPOINT_HOST#*/}" + if [ "$S3_ENDPOINT_PATH" != "$S3_ENDPOINT_HOST" ]; then + if [ -z "$S3_PREFIX" ]; then + S3_PREFIX="$S3_ENDPOINT_PATH" + fi + S3_ENDPOINT_HOST="${S3_ENDPOINT_HOST%%/*}" fi - if [ "${SMTP_ENCRYPTION}" = "tls" ] && [ -n "${CLOUDRON_MAIL_DOMAIN:-}" ]; then + + log INFO "Using S3 endpoint $S3_ENDPOINT_HOST (region $S3_REGION, bucket $S3_BUCKET)" +else + S3_ENDPOINT_HOST="s3.example.com" + log WARN "S3 not configured - using placeholder values" +fi + +MASTER_KEY_FILE="$SECRETS_DIR/master_key" +HASH_KEY_FILE="$SECRETS_DIR/hash_key" +JWT_SECRET_FILE="$SECRETS_DIR/jwt_secret" +SESSION_SECRET_FILE="$SECRETS_DIR/session_secret" + +SMTP_HOST="${CLOUDRON_MAIL_SMTP_SERVER:-}" +SMTP_PORT="${CLOUDRON_MAIL_SMTP_PORT:-25}" +SMTP_ENCRYPTION="${CLOUDRON_MAIL_SMTP_ENCRYPTION:-}" +if [ -n "${CLOUDRON_MAIL_SMTPS_PORT:-}" ]; then + SMTP_PORT="${CLOUDRON_MAIL_SMTPS_PORT}" + SMTP_ENCRYPTION="tls" + if [ -n "${CLOUDRON_MAIL_DOMAIN:-}" ]; then SMTP_HOST="mail.${CLOUDRON_MAIL_DOMAIN}" fi -else - log "INFO" "EMAIL_DISABLED: Skipping SMTP configuration (ENABLE_SMTP=false)" fi +SMTP_USERNAME="${CLOUDRON_MAIL_SMTP_USERNAME:-}" +SMTP_PASSWORD="${CLOUDRON_MAIL_SMTP_PASSWORD:-}" +SMTP_EMAIL="${CLOUDRON_MAIL_FROM:-no-reply@$RP_ID}" SMTP_SENDER_NAME="${CLOUDRON_MAIL_FROM_DISPLAY_NAME:-Ente}" -# Museum server configuration - create configurations directory structure -MUSEUM_CONFIG_DIR="/app/data/ente/server/configurations" -MUSEUM_CONFIG="$MUSEUM_CONFIG_DIR/local.yaml" -mkdir -p "$MUSEUM_CONFIG_DIR" +if [ -n "$SMTP_HOST" ]; then + log INFO "SMTP configured for $SMTP_HOST:$SMTP_PORT (encryption: ${SMTP_ENCRYPTION:-none})" +else + log INFO "SMTP not configured; Museum will skip outbound email" +fi + +normalize_b64() { + local value="$1" + value="$(printf '%s' "$value" | tr -d '\r\n')" + value="$(printf '%s' "$value" | tr '-_' '+/')" + local mod=$(( ${#value} % 4 )) + if [ $mod -eq 2 ]; then + value="${value}==" + elif [ $mod -eq 3 ]; then + value="${value}=" + elif [ $mod -eq 1 ]; then + value="" + fi + printf '%s' "$value" +} + +normalize_b64url() { + local value="$1" + value="$(printf '%s' "$value" | tr -d '\r\n')" + value="$(printf '%s' "$value" | tr '+/' '-_')" + local mod=$(( ${#value} % 4 )) + if [ $mod -eq 2 ]; then + value="${value}==" + elif [ $mod -eq 3 ]; then + value="${value}=" + elif [ $mod -eq 1 ]; then + value="" + fi + printf '%s' "$value" +} + +generate_b64() { + local bytes="$1" + openssl rand -base64 "$bytes" | tr -d '\n' +} + +generate_b64url() { + local bytes="$1" + openssl rand -base64 "$bytes" | tr '+/' '-_' | tr -d '\n' +} + +ensure_secret() { + local file="$1" + local bytes="$2" + local mode="$3" + local current="" + if [ -f "$file" ]; then + current="$(tr -d '\n' < "$file")" + fi + if [ "$mode" = "b64" ]; then + current="$(normalize_b64 "$current")" + if [ -z "$current" ]; then + current="$(generate_b64 "$bytes")" + fi + else + current="$(normalize_b64url "$current")" + if [ -z "$current" ]; then + current="$(generate_b64url "$bytes")" + fi + fi + printf '%s +' "$current" > "$file" +} + +ensure_secret "$MASTER_KEY_FILE" 32 b64 +ensure_secret "$HASH_KEY_FILE" 64 b64 +ensure_secret "$JWT_SECRET_FILE" 32 b64url +ensure_secret "$SESSION_SECRET_FILE" 32 b64url + +MASTER_KEY="$(tr -d '\n' < "$MASTER_KEY_FILE")" +HASH_KEY="$(tr -d '\n' < "$HASH_KEY_FILE")" +JWT_SECRET="$(tr -d '\n' < "$JWT_SECRET_FILE")" +SESSION_SECRET="$(tr -d '\n' < "$SESSION_SECRET_FILE")" + +chown cloudron:cloudron "$MASTER_KEY_FILE" "$HASH_KEY_FILE" "$JWT_SECRET_FILE" "$SESSION_SECRET_FILE" +chmod 600 "$MASTER_KEY_FILE" "$HASH_KEY_FILE" "$JWT_SECRET_FILE" "$SESSION_SECRET_FILE" + +log INFO "Ensuring Museum runtime assets" + +sync_dir() { + local source="$1" + local target="$2" + if [ -d "$source" ]; then + log INFO "Syncing $(basename "$source") into data directory" + rm -rf "$target" + cp -a "$source" "$target" + chown -R cloudron:cloudron "$target" + else + log WARN "Missing expected directory: $source" + fi +} + +sync_dir "$APP_DIR/server/migrations" "$MUSEUM_RUNTIME_DIR/migrations" +sync_dir "$APP_DIR/server/web-templates" "$MUSEUM_RUNTIME_DIR/web-templates" +sync_dir "$APP_DIR/server/mail-templates" "$MUSEUM_RUNTIME_DIR/mail-templates" +sync_dir "$APP_DIR/server/assets" "$MUSEUM_RUNTIME_DIR/assets" + +if [ ! -x "$MUSEUM_BIN" ]; then + log ERROR "Museum binary not found at $MUSEUM_BIN" + exit 1 +fi if [ ! -f "$MUSEUM_CONFIG" ]; then - log "INFO" "Rendering Museum server configuration" - cat > "$MUSEUM_CONFIG" << EOF -# Museum server configuration - -# Server settings + log INFO "Rendering Museum configuration" + cat > "$MUSEUM_CONFIG" <> "$MUSEUM_CONFIG" < /dev/null 2>&1; then - log "INFO" "PostgreSQL connection successful" -else - log "ERROR" "Failed to connect to PostgreSQL" - log "ERROR" "Connection details:" - log "ERROR" " Host: $CLOUDRON_POSTGRESQL_HOST" - log "ERROR" " Port: $CLOUDRON_POSTGRESQL_PORT" - log "ERROR" " User: $CLOUDRON_POSTGRESQL_USERNAME" - log "ERROR" " Database: $CLOUDRON_POSTGRESQL_DATABASE" - exit 1 -fi - -# =============================================== -# Museum Server Binary Setup -# =============================================== -MUSEUM_BIN="/app/data/ente/server/museum" -MUSEUM_LOG="/app/data/logs/museum.log" -USE_PLACEHOLDER=${FORCE_PLACEHOLDER:-false} - -log "INFO" "Setting up Museum server binary" - -# Copy Museum binary from build location to data directory -MUSEUM_BUILD_BIN="/app/museum-bin/museum" -log "INFO" "Checking for pre-built Museum binary at: $MUSEUM_BUILD_BIN" -if [ -f "$MUSEUM_BUILD_BIN" ]; then - log "INFO" "Found pre-built Museum binary, copying to data directory" - cp "$MUSEUM_BUILD_BIN" "$MUSEUM_BIN" - chmod +x "$MUSEUM_BIN" - log "INFO" "Copied Museum binary to $MUSEUM_BIN" -else - log "WARN" "Pre-built Museum binary not found at $MUSEUM_BUILD_BIN" -fi - -# Copy migration files to Museum working directory -MUSEUM_MIGRATIONS_DIR="/app/data/ente/server/migrations" -REPO_MIGRATIONS_DIR="/app/data/ente/repository/server/migrations" -if [ ! -d "$MUSEUM_MIGRATIONS_DIR" ] && [ -d "$REPO_MIGRATIONS_DIR" ]; then - log "INFO" "Copying database migration files" - cp -r "$REPO_MIGRATIONS_DIR" "$MUSEUM_MIGRATIONS_DIR" - log "INFO" "Copied migration files to $MUSEUM_MIGRATIONS_DIR" -else - log "INFO" "Migration files already exist or source not available" -fi - -# Copy web templates to Museum working directory -MUSEUM_WEB_TEMPLATES_DIR="/app/data/ente/server/web-templates" -REPO_WEB_TEMPLATES_DIR="/app/data/ente/repository/server/web-templates" -if [ ! -d "$MUSEUM_WEB_TEMPLATES_DIR" ] && [ -d "$REPO_WEB_TEMPLATES_DIR" ]; then - log "INFO" "Copying web templates" - cp -r "$REPO_WEB_TEMPLATES_DIR" "$MUSEUM_WEB_TEMPLATES_DIR" - log "INFO" "Copied web templates to $MUSEUM_WEB_TEMPLATES_DIR" -else - log "INFO" "Web templates already exist or source not available" -fi - -# Copy mail templates for transactional emails -MUSEUM_MAIL_TEMPLATES_DIR="/app/data/ente/server/mail-templates" -REPO_MAIL_TEMPLATES_DIR="/app/data/ente/repository/server/mail-templates" -if [ ! -d "$MUSEUM_MAIL_TEMPLATES_DIR" ] && [ -d "$REPO_MAIL_TEMPLATES_DIR" ]; then - log "INFO" "Copying mail templates" - cp -r "$REPO_MAIL_TEMPLATES_DIR" "$MUSEUM_MAIL_TEMPLATES_DIR" - log "INFO" "Copied mail templates to $MUSEUM_MAIL_TEMPLATES_DIR" -else - log "INFO" "Mail templates already exist or source not available" -fi - -# Check if Museum binary exists and is valid -log "INFO" "Checking for Museum binary at: $MUSEUM_BIN" -if [ -f "$MUSEUM_BIN" ]; then - log "INFO" "Museum binary file exists" - if [ -x "$MUSEUM_BIN" ]; then - log "INFO" "Museum binary is executable" - # Since Museum's --help and --version commands trigger full startup (including DB migration), - # we'll trust that an existing executable binary should work - log "INFO" "Museum binary is ready to use" - else - log "INFO" "Museum binary exists but is not executable, fixing permissions" - chmod +x "$MUSEUM_BIN" - if [ -x "$MUSEUM_BIN" ]; then - log "INFO" "Fixed permissions, Museum binary is ready to use" +log INFO "Preparing frontend assets" +if [ -d "$WEB_SOURCE_DIR" ]; then + for app in photos accounts auth cast albums family; do + if [ -d "$WEB_SOURCE_DIR/$app" ]; then + log INFO "Updating $app frontend assets" + rm -rf "$WEB_RUNTIME_DIR/$app" + cp -a "$WEB_SOURCE_DIR/$app" "$WEB_RUNTIME_DIR/$app" + chown -R cloudron:cloudron "$WEB_RUNTIME_DIR/$app" else - log "WARN" "Failed to fix permissions, using placeholder" - USE_PLACEHOLDER=true + log WARN "Missing built frontend for $app" fi - fi -else - log "WARN" "Museum binary file not found at $MUSEUM_BIN" - log "INFO" "Checking directory contents: $(ls -la $(dirname $MUSEUM_BIN) 2>/dev/null || echo 'Directory not found')" - USE_PLACEHOLDER=true -fi - - -# =============================================== -# Web Application Setup -# =============================================== -log "INFO" "Setting up web applications with writable directory" - -# Copy web apps to writable data directory first -WRITABLE_WEB_DIR="/app/data/web" -if [ ! -d "$WRITABLE_WEB_DIR" ]; then - log "INFO" "Copying web applications to writable directory" - mkdir -p "$WRITABLE_WEB_DIR" - cp -r /app/web/* "$WRITABLE_WEB_DIR/" - chown -R cloudron:cloudron "$WRITABLE_WEB_DIR" - log "INFO" "Web applications copied to $WRITABLE_WEB_DIR" -else - log "INFO" "Web applications already exist in writable directory" -fi - -# Fix API endpoint configuration in built JavaScript files -log "INFO" "Updating API endpoint configuration in web apps" -ACTUAL_ENDPOINT="${BASE_URL}/api" -log "INFO" "Setting API endpoint to: $ACTUAL_ENDPOINT" - -declare -a PLACEHOLDER_ENDPOINTS=( - "https://example.com/api" - "https://placeholder.invalid/api" - "https://api.ente.io" - "https://api.ente.io/api" -) - -declare -A HOST_REWRITES=( - ["https://accounts.ente.io"]="${BASE_URL}/accounts" - ["https://auth.ente.io"]="${BASE_URL}/auth" - ["https://cast.ente.io"]="${BASE_URL}/cast" - ["https://photos.ente.io"]="${BASE_URL}/photos" - ["https://web.ente.io"]="${BASE_URL}/photos" -) - -for webapp in photos accounts auth cast; do - WEB_DIR="$WRITABLE_WEB_DIR/${webapp}" - if [ -d "$WEB_DIR" ]; then - log "INFO" "Processing ${webapp} app for endpoint rewrites" - for placeholder in "${PLACEHOLDER_ENDPOINTS[@]}"; do - find "$WEB_DIR" -name "*.js" -type f -exec sed -i "s|${placeholder}|${ACTUAL_ENDPOINT}|g" {} \; - done - for source in "${!HOST_REWRITES[@]}"; do - target="${HOST_REWRITES[$source]}" - find "$WEB_DIR" -name "*.js" -type f -exec sed -i "s|${source}|${target}|g" {} \; - done - log "INFO" "Endpoint rewrites complete for ${webapp}" - else - log "WARN" "Web directory not found for ${webapp}" - fi -done - -# =============================================== -# Node.js Placeholder Server -# =============================================== -create_nodejs_placeholder() { - log "INFO" "Creating Node.js placeholder server" - - # Create server script - cat > "/app/data/ente/server/placeholder.js" << 'EOF' -const http = require('http'); -const fs = require('fs'); -const { execSync } = require('child_process'); -const path = require('path'); - -const PORT = 8080; -const LOG_FILE = '/app/data/logs/museum.log'; -const DB_SCHEMA_FILE = '/app/data/ente/server/schema.sql'; - -// Log function -function log(message) { - const timestamp = new Date().toISOString(); - const logMessage = `${timestamp} - ${message}\n`; - console.log(logMessage); - try { - fs.appendFileSync(LOG_FILE, logMessage); - } catch (err) { - console.error(`Error writing to log: ${err.message}`); - } -} - -log('Starting Ente placeholder server...'); - -// Try to initialize the database schema -function initializeDatabase() { - try { - // Create a basic schema file if it doesn't exist - if (!fs.existsSync(DB_SCHEMA_FILE)) { - log('Creating basic database schema file'); - const basicSchema = ` --- Basic schema for Ente Museum server -CREATE TABLE IF NOT EXISTS users ( - id SERIAL PRIMARY KEY, - email VARCHAR(255) UNIQUE NOT NULL, - password_hash VARCHAR(255) NOT NULL, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); - -CREATE TABLE IF NOT EXISTS files ( - id SERIAL PRIMARY KEY, - user_id INTEGER REFERENCES users(id), - filename VARCHAR(255) NOT NULL, - path VARCHAR(255) NOT NULL, - mime_type VARCHAR(100), - size BIGINT, - created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP, - updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP -); -`; - fs.writeFileSync(DB_SCHEMA_FILE, basicSchema); - } - - // Initialize database - const dbUser = process.env.CLOUDRON_POSTGRESQL_USERNAME; - const dbPassword = process.env.CLOUDRON_POSTGRESQL_PASSWORD; - const dbHost = process.env.CLOUDRON_POSTGRESQL_HOST; - const dbPort = process.env.CLOUDRON_POSTGRESQL_PORT; - const dbName = process.env.CLOUDRON_POSTGRESQL_DATABASE; - - if (dbUser && dbPassword && dbHost && dbPort && dbName) { - log(`Initializing database ${dbName} on ${dbHost}:${dbPort}`); - const command = `PGPASSWORD="${dbPassword}" psql -h "${dbHost}" -p "${dbPort}" -U "${dbUser}" -d "${dbName}" -f "${DB_SCHEMA_FILE}"`; - execSync(command, { stdio: 'inherit' }); - log('Database initialized successfully'); - return true; - } else { - log('Database environment variables not set, skipping initialization'); - return false; - } - } catch (err) { - log(`Error initializing database: ${err.message}`); - return false; - } -} - -// Try to initialize database -initializeDatabase(); - -// API response handlers -const apiHandlers = { - // Health check endpoint - '/health': (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - status: 'OK', - server: 'Ente Placeholder', - version: '1.0.0' - })); - log('Health check request - responded with status OK'); - }, - - // User verification endpoint (returns minimal structure expected by UI) - '/api/users/verify-email': (req, res) => { - const buildResponse = (emailAddress) => { - const email = emailAddress || 'unknown@example.com'; - const stableId = Math.abs(Buffer.from(email).reduce((acc, byte) => (acc * 31 + byte) % 100000, 17)) || 1; - return { - id: stableId, - token: `placeholder-token-${stableId}`, - encryptedToken: `placeholder-encrypted-token-${stableId}`, - accountsUrl: `${process.env.CLOUDRON_APP_ORIGIN || 'https://example.com'}/accounts`, - twoFactorSessionID: undefined, - twoFactorSessionIDV2: undefined, - passkeySessionID: undefined, - keyAttributes: undefined - }; - }; - - if (req.method !== 'POST') { - res.writeHead(405, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, message: 'Method not allowed' })); - return; - } - - let rawBody = ''; - req.on('data', chunk => { rawBody += chunk.toString(); }); - req.on('end', () => { - let email = 'unknown@example.com'; - let ott = 'unknown'; - try { - const payload = JSON.parse(rawBody || '{}'); - if (payload.email) { - email = payload.email; - } - if (payload.ott) { - ott = payload.ott; - } - } catch (err) { - log(`Failed to parse verify-email request body: ${err.message}`); - } - - const responsePayload = buildResponse(email); - log(`Verifying OTT ${ott} for ${email}`); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify(responsePayload)); - }); - }, - - // User login endpoint - '/api/users/login': (req, res) => { - if (req.method === 'POST') { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - req.on('end', () => { - log('Login request received'); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - token: 'placeholder-jwt-token-' + Date.now(), - user: { - id: 1, - email: 'placeholder@example.com', - name: 'Placeholder User' - } - })); - }); - } else { - res.writeHead(405, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - message: 'Method not allowed' - })); - } - }, - - // User signup endpoint - '/api/users/signup': (req, res) => { - if (req.method === 'POST') { - let body = ''; - req.on('data', chunk => { - body += chunk.toString(); - }); - req.on('end', () => { - log('Signup request received'); - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: true, - token: 'placeholder-jwt-token-' + Date.now(), - user: { - id: 1, - email: 'placeholder@example.com', - name: 'New User' - } - })); - }); - } else { - res.writeHead(405, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ - success: false, - message: 'Method not allowed' - })); - } - }, - - // OTT endpoint - '/users/ott': (req, res) => { - if (req.method !== 'POST') { - res.writeHead(405, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: false, message: 'Method not allowed' })); - return; - } - - let body = ''; - req.on('data', chunk => { body += chunk.toString(); }); - req.on('end', () => { - let email = 'unknown@example.com'; - try { - const payload = JSON.parse(body || '{}'); - if (payload.email) { - email = payload.email; - } - } catch (err) { - log(`Failed to parse OTT request body: ${err.message}`); - } - - const ott = ('' + Math.floor(100000 + Math.random() * 900000)).slice(-6); - log(`Generated OTT ${ott} for ${email}`); - - res.writeHead(200, { 'Content-Type': 'application/json' }); - res.end(JSON.stringify({ success: true, ott, email })); - }); - }, - '/api/users/ott': (req, res) => { - apiHandlers['/users/ott'](req, res); - }, - '/users/verify-email': (req, res) => { - apiHandlers['/api/users/verify-email'](req, res); - }, - '/api/users/verify': (req, res) => { - apiHandlers['/api/users/verify-email'](req, res); - }, - '/users/verify': (req, res) => { - apiHandlers['/api/users/verify-email'](req, res); - }, - '/ping': (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - log('Ping request - responding with status OK'); - res.end(JSON.stringify({ status: 'OK', server: 'Ente Placeholder', time: new Date().toISOString() })); - }, - - // Files endpoint - '/api/files': (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - log('Files request - responding with empty list'); - res.end(JSON.stringify({ - success: true, - files: [] - })); - }, - - // Collections endpoint - '/api/collections': (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - log('Collections request - responding with empty list'); - res.end(JSON.stringify({ - success: true, - collections: [] - })); - }, - - // Default API handler - 'default': (req, res) => { - res.writeHead(200, { 'Content-Type': 'application/json' }); - log(`API request to ${req.url} - responding with generic success`); - res.end(JSON.stringify({ - success: true, - message: 'Placeholder API response', - path: req.url - })); - } -}; - -// Create server -const server = http.createServer((req, res) => { - log(`Request received: ${req.method} ${req.url}`); - - // Set CORS headers for all responses - res.setHeader('Access-Control-Allow-Origin', '*'); - res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); - res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type,Authorization'); - - // Handle OPTIONS request (for CORS preflight) - if (req.method === 'OPTIONS') { - res.writeHead(200); - res.end(); - return; - } - - // Handle health check endpoint - if (req.url === '/health' || req.url === '/api/health') { - apiHandlers['/health'](req, res); - return; - } - - // Handle paths that exactly match defined endpoints - if (apiHandlers[req.url]) { - apiHandlers[req.url](req, res); - return; - } - - // Route based on URL pattern - if (req.url.startsWith('/api/')) { - const handler = apiHandlers['default']; - handler(req, res); - return; - } - - // Default response for any other endpoint - res.writeHead(200, { 'Content-Type': 'application/json' }); - log(`Unknown request to ${req.url} - responding with default message`); - res.end(JSON.stringify({ - message: 'Ente Placeholder Server', - path: req.url, - server: 'Node.js Placeholder' - })); -}); - -// Start server -try { - server.listen(PORT, '0.0.0.0', () => { - log(`Ente placeholder server running on port ${PORT}`); - log(`Server is listening at http://0.0.0.0:${PORT}`); - }); -} catch (err) { - log(`Failed to start server: ${err.message}`); - process.exit(1); -} - -// Handle errors -server.on('error', (error) => { - log(`Server error: ${error.message}`); - if (error.code === 'EADDRINUSE') { - log('Address already in use, retrying in 5 seconds...'); - setTimeout(() => { - server.close(); - server.listen(PORT, '0.0.0.0'); - }, 5000); - } -}); - -log('Ente placeholder server initialization complete'); -EOF - - # Start the Node.js placeholder server - log "INFO" "Starting Node.js placeholder server" - cd /app/data/ente/server - node placeholder.js > "$MUSEUM_LOG" 2>&1 & - PLACEHOLDER_PID=$! - log "INFO" "Started Node.js server with PID: $PLACEHOLDER_PID" - - # Wait for the server to start - MAX_ATTEMPTS=30 - ATTEMPT=0 - SUCCESS=false - - while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - if curl -s http://localhost:8080/ping > /dev/null 2>&1; then - log "INFO" "Node.js placeholder server started successfully" - SUCCESS=true - break - fi - ATTEMPT=$((ATTEMPT+1)) - log "INFO" "Waiting for server to start (attempt $ATTEMPT/$MAX_ATTEMPTS)" - sleep 1 done - - if [ "$SUCCESS" = false ]; then - log "ERROR" "Node.js placeholder server failed to start" - log "ERROR" "Last 20 lines of log:" - tail -n 20 "$MUSEUM_LOG" | while read -r line; do - log "ERROR" " $line" - done - return 1 - fi - - return 0 -} - -# =============================================== -# Start the appropriate server -# =============================================== -log "INFO" "Starting server" - -if [ "$USE_PLACEHOLDER" = true ]; then - log "INFO" "Using Node.js placeholder server" - create_nodejs_placeholder else - log "INFO" "Starting actual Museum server" - cd /app/data/ente/server - export ENVIRONMENT="${MUSEUM_ENVIRONMENT:-local}" - stdbuf -oL "$MUSEUM_BIN" 2>&1 | tee -a "$MUSEUM_LOG" & - MUSEUM_PID=$! - log "INFO" "Started Museum server (pipeline PID: $MUSEUM_PID)" - - # Wait for the server to start - MAX_ATTEMPTS=30 - ATTEMPT=0 - SUCCESS=false - - while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - if curl -s http://localhost:8080/ping > /dev/null 2>&1; then - log "INFO" "Museum server started successfully" - SUCCESS=true - break - fi - ATTEMPT=$((ATTEMPT+1)) - log "INFO" "Waiting for Museum server to start (attempt $ATTEMPT/$MAX_ATTEMPTS)" - sleep 1 - done - - if [ "$SUCCESS" = false ]; then - log "ERROR" "Museum server failed to start within $MAX_ATTEMPTS seconds" - if ps -p "$MUSEUM_PID" > /dev/null 2>&1; then - log "INFO" "Stopping Museum server pipeline" - kill "$MUSEUM_PID" || true - fi - log "ERROR" "Last 20 lines of museum.log:" - tail -n 20 "$MUSEUM_LOG" | while read -r line; do - log "ERROR" " $line" - done - exit 1 - fi + log ERROR "Frontend assets directory missing at $WEB_SOURCE_DIR" fi -# =============================================== -# Setup Caddy web server -# =============================================== -log "INFO" "Setting up Caddy web server" +rewrite_frontend_reference() { + local search="$1" + local replace="$2" + local count=0 + local file + + if [ -z "$search" ] || [ -z "$replace" ] || [ "$search" = "$replace" ]; then + return + fi + + while IFS= read -r -d '' file; do + if LC_ALL=C grep -F -q "$search" "$file"; then + sed -i "s|$search|$replace|g" "$file" + chown cloudron:cloudron "$file" + count=$((count + 1)) + fi + done < <(find "$WEB_RUNTIME_DIR" -type f \( -name "*.js" -o -name "*.json" -o -name "*.html" -o -name "*.css" -o -name "*.txt" \) -print0) + + if [ "$count" -gt 0 ]; then + log INFO "Replaced '$search' with '$replace' in $count frontend files" + fi +} + +if [ -d "$WEB_RUNTIME_DIR" ]; then + log INFO "Rewriting frontend endpoints for local deployment" + FRONTEND_REPLACEMENTS=( + "ENTE_API_ORIGIN_PLACEHOLDER|$API_ORIGIN" + "https://api.ente.io|$API_ORIGIN" + "https://accounts.ente.io|$BASE_URL/accounts" + "https://auth.ente.io|$BASE_URL/auth" + "https://cast.ente.io|$BASE_URL/cast" + "https://photos.ente.io|$BASE_URL/photos" + "https://web.ente.io|$BASE_URL/photos" + "https://albums.ente.io|$BASE_URL/albums" + "https://family.ente.io|$BASE_URL/family" + ) + OLD_IFS="$IFS" + for entry in "${FRONTEND_REPLACEMENTS[@]}"; do + IFS="|" read -r search replace <<<"$entry" + rewrite_frontend_reference "$search" "$replace" + done + IFS="$OLD_IFS" +fi + +log INFO "Ensuring CLI configuration" +CLI_HOME="$DATA_DIR/home/.ente" +mkdir -p "$CLI_HOME" +cat > "$CLI_HOME/config.yaml" < "$CADDY_CONFIG" < "$CADDY_CONFIG" << EOF :3080 { log { - output file /app/data/logs/caddy.log level INFO + output stdout } - # Enable compression encode gzip - # CORS preflight handling @options { method OPTIONS } handle @options { - header { - Access-Control-Allow-Origin "*" - Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" - Access-Control-Allow-Headers "*" - Access-Control-Max-Age "3600" - } + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" + header Access-Control-Allow-Headers "*" + header Access-Control-Max-Age "3600" respond 204 } - # API endpoints with CORS handle_path /api/* { reverse_proxy localhost:8080 { header_up Host {http.request.host} @@ -979,132 +435,183 @@ cat > "$CADDY_CONFIG" << EOF header_up X-Forwarded-For {http.request.remote} header_up X-Forwarded-Proto {http.request.scheme} } - header { - Access-Control-Allow-Origin "*" - Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" - Access-Control-Allow-Headers "*" - Access-Control-Allow-Credentials "true" + header Access-Control-Allow-Origin "*" + header Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" + header Access-Control-Allow-Headers "*" + header Access-Control-Allow-Credentials "true" + } + + handle /health { + rewrite * /ping + reverse_proxy localhost:8080 { + header_up Host {http.request.host} + header_up X-Real-IP {http.request.remote} + header_up X-Forwarded-For {http.request.remote} + header_up X-Forwarded-Proto {http.request.scheme} } } - - # Public albums endpoint + + handle /ping { + reverse_proxy localhost:8080 { + header_up Host {http.request.host} + header_up X-Real-IP {http.request.remote} + header_up X-Forwarded-For {http.request.remote} + header_up X-Forwarded-Proto {http.request.scheme} + } + } + handle /public/* { reverse_proxy localhost:8080 - header { - Access-Control-Allow-Origin "*" - } - } - - # Health check endpoint - handle /health { - reverse_proxy localhost:8080 } - handle /images/* { - rewrite * /photos{path} - root * /app/data/web - file_server - } - - # Static files for Next.js assets shared across apps handle /_next/* { - root * /app/data/web - try_files photos{path} accounts{path} auth{path} cast{path} {path} + root * $WEB_RUNTIME_DIR + try_files {path} auth{path} accounts{path} photos{path} cast{path} albums{path} family{path} file_server - header { - Cache-Control "public, max-age=31536000" - Access-Control-Allow-Origin "*" - } } - - # Default to serve SPA assets + + handle /auth/* { + root * $WEB_RUNTIME_DIR + try_files {path} {path}/index.html /auth/index.html + file_server + } + + handle /accounts/* { + root * $WEB_RUNTIME_DIR + try_files {path} {path}/index.html /accounts/index.html + file_server + } + + handle /cast/* { + root * $WEB_RUNTIME_DIR + try_files {path} {path}/index.html /cast/index.html + file_server + } + + handle /family/* { + root * $WEB_RUNTIME_DIR + try_files {path} {path}/index.html /family/index.html + file_server + } + + handle /albums/* { + root * $WEB_RUNTIME_DIR + try_files {path} {path}/index.html /albums/index.html + file_server + } + + handle /photos/* { + root * $WEB_RUNTIME_DIR + try_files {path} {path}/index.html /photos/index.html + file_server + } + handle { - root * /app/data/web - try_files {path}/index.html {path} /photos/index.html + root * $WEB_RUNTIME_DIR + try_files {path} {path}/index.html /photos/index.html file_server } } -EOF +EOF_CADDY -log "INFO" "Validating Caddy configuration" -if caddy validate --config "$CADDY_CONFIG" 2>&1 | tee -a "$LOG_FILE"; then - log "INFO" "Caddy configuration is valid" -else - log "ERROR" "Caddy configuration validation failed!" - log "ERROR" "Caddyfile contents:" - cat "$CADDY_CONFIG" | while read -r line; do - log "ERROR" " $line" - done +chown cloudron:cloudron "$CADDY_CONFIG" + +log INFO "Validating Caddy configuration" +if ! caddy validate --config "$CADDY_CONFIG" > "$TMP_DIR/caddy-validate.log" 2>&1; then + cat "$TMP_DIR/caddy-validate.log" + log ERROR "Caddy configuration validation failed" + exit 1 fi -log "INFO" "Starting Caddy web server" -caddy run --config "$CADDY_CONFIG" > /app/data/logs/caddy.log 2>&1 & -CADDY_PID=$! -log "INFO" "Caddy web server started with PID: $CADDY_PID" - -# Wait a moment to see if Caddy stays running -sleep 2 -if ps -p $CADDY_PID > /dev/null; then - log "INFO" "Caddy is still running after 2 seconds" -else - log "ERROR" "Caddy has crashed! Last 20 lines of Caddy log:" - tail -n 20 /app/data/logs/caddy.log | while read -r line; do - log "ERROR" " $line" - done +log INFO "Testing PostgreSQL connectivity" +if ! PGPASSWORD="$CLOUDRON_POSTGRESQL_PASSWORD" psql -h "$CLOUDRON_POSTGRESQL_HOST" -p "$CLOUDRON_POSTGRESQL_PORT" \ + -U "$CLOUDRON_POSTGRESQL_USERNAME" -d "$CLOUDRON_POSTGRESQL_DATABASE" -c "SELECT 1" >/dev/null 2>&1; then + log ERROR "Unable to connect to PostgreSQL" + exit 1 fi -# =============================================== -# Finalization and monitoring -# =============================================== -log "INFO" "Setup complete" +if [ "$S3_NOT_CONFIGURED" = "true" ]; then + log WARN "S3 not configured - creating configuration page" + mkdir -p "$WEB_RUNTIME_DIR/config" + cat > "$WEB_RUNTIME_DIR/config/index.html" <<'EOF_CONFIG' + + + + Ente Configuration Required + + + +

Ente Configuration Required

+
+ S3 Storage Not Configured +

Ente requires S3-compatible object storage to function. Please configure your S3 credentials.

+
+

Configuration Steps

+
    +
  1. Open the Cloudron dashboard
  2. +
  3. Go to your Ente app and open the Terminal
  4. +
  5. Edit /app/data/config/s3.env: +
    nano /app/data/config/s3.env
    +
  6. +
  7. Add your S3 credentials: +
    S3_ENDPOINT=https://your-s3-endpoint.com
    +S3_REGION=your-region
    +S3_BUCKET=your-bucket-name
    +S3_ACCESS_KEY=your-access-key
    +S3_SECRET_KEY=your-secret-key
    +
  8. +
  9. Save the file and restart the app from the Cloudron dashboard
  10. +
+

For more information, see the Ente S3 Configuration Guide.

+ + +EOF_CONFIG + chown -R cloudron:cloudron "$WEB_RUNTIME_DIR/config" -# Create startup instructions -cat > /app/data/SETUP-INSTRUCTIONS.md << EOF -# Ente Cloudron App - Setup Instructions - -## Configuration - -1. **S3 Storage**: Edit the configuration file at \`/app/data/config/s3.env\` (uncomment lines and add your values) with your S3-compatible storage credentials. - -2. **Museum Server**: The server configuration is at \`/app/data/ente/server/museum.yaml\` if you need to customize settings. - -## Troubleshooting - -- **Logs**: Check the logs at \`/app/data/logs/\` for any issues. -- **Restart**: If you change configuration, restart the app to apply changes. - -## Web Applications - -The following web applications are available: - -- Photos: https://${CLOUDRON_APP_FQDN}/photos/ -- Accounts: https://${CLOUDRON_APP_FQDN}/accounts/ -- Auth: https://${CLOUDRON_APP_FQDN}/auth/ -- Cast: https://${CLOUDRON_APP_FQDN}/cast/ - -## Support - -For more information, visit the [Ente GitHub repository](https://github.com/ente-io/ente). -EOF - -# Remove startup flag -rm -f /app/data/startup_in_progress - -# Verify running services -log "INFO" "Verifying running services" -if ps aux | grep -E "museum|placeholder" | grep -v grep > /dev/null; then - log "INFO" "Server is running" + log INFO "Starting Caddy in configuration mode" + setpriv --reuid=cloudron --regid=cloudron --init-groups caddy file-server --listen :3080 --root "$WEB_RUNTIME_DIR/config" & + CADDY_PID=$! + MUSEUM_PID="" else - log "ERROR" "No server is running!" + log INFO "Starting Museum server and Caddy" + + setpriv --reuid=cloudron --regid=cloudron --init-groups /bin/bash -lc "cd '$MUSEUM_RUNTIME_DIR' && exec stdbuf -oL '$MUSEUM_BIN'" & + MUSEUM_PID=$! + + setpriv --reuid=cloudron --regid=cloudron --init-groups caddy run --config "$CADDY_CONFIG" --watch & + CADDY_PID=$! fi -if ps aux | grep caddy | grep -v grep > /dev/null; then - log "INFO" "Caddy server is running" +terminate() { + log INFO "Shutting down services" + if [ -n "$MUSEUM_PID" ]; then + kill "$MUSEUM_PID" 2>/dev/null || true + fi + if [ -n "$CADDY_PID" ]; then + kill "$CADDY_PID" 2>/dev/null || true + fi + if [ -n "$MUSEUM_PID" ]; then + wait "$MUSEUM_PID" 2>/dev/null || true + fi + if [ -n "$CADDY_PID" ]; then + wait "$CADDY_PID" 2>/dev/null || true + fi +} + +trap terminate TERM INT + +if [ -n "$MUSEUM_PID" ]; then + wait -n "$MUSEUM_PID" "$CADDY_PID" else - log "ERROR" "Caddy server is not running!" + wait "$CADDY_PID" fi - -log "INFO" "Ente Cloudron app startup complete" - -# Keep the script running to prevent container exit -exec tail -f "$MUSEUM_LOG" +EXIT_CODE=$? +terminate +log ERROR "Service exited unexpectedly" +exit "$EXIT_CODE"