commit 3c0a730ef48375420e14d5112634c1000fb4bda7 Author: Codex Date: Wed Nov 12 08:43:03 2025 -0600 Initial packaging diff --git a/BUILD.md b/BUILD.md new file mode 100644 index 0000000..e40d939 --- /dev/null +++ b/BUILD.md @@ -0,0 +1,44 @@ +# AFFiNE Cloudron Package Build Guide + +## Prerequisites +- Cloudron CLI logged into your Cloudron server (`cloudron login`) +- Access to the Cloudron build service `builder.docker.due.ren` +- Docker installed locally (required by the Cloudron build tooling) +- Git repository checked out with this packaging folder +- Cloudron admin credentials to install the resulting app image + +## Build Commands +```bash +cloudron build \ + --set-build-service builder.docker.due.ren \ + --build-service-token e3265de06b1d0e7bb38400539012a8433a74c2c96a17955e \ + --set-repository andreasdueren/ente-cloudron \ + --tag 0.1.0 +``` + +## Deployment Steps +1. Remove any previous dev install of AFFiNE on the Cloudron (always reinstall from scratch). +2. Install the freshly built image: + ```bash + cloudron install --location ente.due.ren --image andreasdueren/ente-cloudron:0.1.0 + ``` +3. When prompted, confirm the app info and wait for Cloudron to report success (abort after ~30 seconds if installation stalls or errors to avoid hanging sessions). +4. Visit `https://ente.due.ren` (or the chosen location) and sign in using Cloudron SSO. + +## Testing Checklist +- Open the app dashboard and ensure the landing page loads without 502/504 errors. +- Create a workspace, add a note, upload an asset, and reload to confirm `/app/data` persistence. +- Trigger OIDC login/logout flows to verify Cloudron SSO works (callback `/api/v1/session/callback`). +- Send an invitation email to validate SMTP credentials wired from the Cloudron mail addon. +- Inspect logs with `cloudron logs --app ente.due.ren -f` for migration output from `scripts/self-host-predeploy.js`. + +## Troubleshooting +- **Migrations hang**: restart the app; migrations rerun automatically before the server starts. Check PostgreSQL reachability via `cloudron exec --app -- env | grep DATABASE_URL`. +- **Login issues**: confirm the Cloudron OIDC client is enabled for the app and that the callback URL `/api/v1/session/callback` is allowed. +- **Email failures**: verify the Cloudron sendmail addon is provisioned and that `MAILER_*` env vars show up inside the container (`cloudron exec --app -- env | grep MAILER`). +- **Large uploads rejected**: adjust `client_max_body_size` in `nginx.conf` if you routinely exceed 200 MB assets, then rebuild. + +## Configuration Notes +- Persistent config lives in `/app/data/config/config.json`. Modify values (e.g., Stripe, throttling) and restart the app; the file is backed up by Cloudron. +- Uploaded files live in `/app/data/storage` and map to `~/.affine/storage` inside the runtime. +- Default health check hits `/api/healthz`; customize via `CloudronManifest.json` if upstream changes. diff --git a/CloudronManifest.json b/CloudronManifest.json new file mode 100644 index 0000000..28f377c --- /dev/null +++ b/CloudronManifest.json @@ -0,0 +1,34 @@ +{ + "id": "pro.affine.cloudron", + "title": "AFFiNE", + "author": "AFFiNE Project", + "description": "Next-gen knowledge base that blends docs, whiteboards, and databases for self-hosted teams.", + "website": "https://affine.pro", + "contactEmail": "support@affine.pro", + "version": "0.1.0", + "changelog": "Initial Cloudron packaging", + "manifestVersion": 2, + "minBoxVersion": "7.0.0", + "httpPort": 3000, + "addons": { + "localstorage": {}, + "postgresql": {}, + "redis": {}, + "sendmail": {}, + "oidc": { + "redirectUris": [ + "/api/v1/session/callback" + ], + "loginRedirectUri": "/api/v1/session/callback" + } + }, + "memoryLimit": 2147483648, + "healthCheckPath": "/api/healthz", + "tags": [ + "notes", + "knowledge-base", + "whiteboard", + "collaboration" + ], + "postInstallMessage": "AFFiNE is connected to Cloudron single sign-on by default. Sign in with any Cloudron user to create your first workspace." +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..6288e6e --- /dev/null +++ b/Dockerfile @@ -0,0 +1,35 @@ +FROM ghcr.io/toeverything/affine:stable AS upstream + +FROM cloudron/base:5.0.0 + +ENV APP_CODE_DIR=/app/code \ + APP_DATA_DIR=/app/data \ + APP_RUNTIME_DIR=/run/affine \ + APP_TMP_DIR=/tmp/data \ + APP_BUILD_DIR=/app/code/affine \ + NODE_ENV=production \ + PORT=3010 \ + LD_PRELOAD=libjemalloc.so.2 + +RUN mkdir -p "$APP_CODE_DIR" "$APP_DATA_DIR" "$APP_RUNTIME_DIR" "$APP_TMP_DIR" && \ + apt-get update && \ + apt-get install -y --no-install-recommends jq python3 ca-certificates curl openssl libjemalloc2 && \ + rm -rf /var/lib/apt/lists/* + +# bring in the upstream runtime and packaged server artifacts +COPY --from=upstream /usr/local /usr/local +COPY --from=upstream /app "$APP_BUILD_DIR" + +# configuration, launch scripts, and defaults +COPY start.sh "$APP_CODE_DIR/start.sh" +COPY run-affine.sh "$APP_CODE_DIR/run-affine.sh" +COPY nginx.conf "$APP_CODE_DIR/nginx.conf" +COPY supervisord.conf "$APP_CODE_DIR/supervisord.conf" +COPY config.example.json "$APP_CODE_DIR/config.example.json" +COPY tmp_data/ "$APP_TMP_DIR/" + +RUN chmod +x "$APP_CODE_DIR/start.sh" "$APP_CODE_DIR/run-affine.sh" && \ + chown -R cloudron:cloudron "$APP_CODE_DIR" "$APP_DATA_DIR" "$APP_RUNTIME_DIR" "$APP_TMP_DIR" + +EXPOSE 3000 +CMD ["/app/code/start.sh"] diff --git a/config.example.json b/config.example.json new file mode 100644 index 0000000..7ce286e --- /dev/null +++ b/config.example.json @@ -0,0 +1,6 @@ +{ + "$schema": "https://github.com/toeverything/AFFiNE/releases/latest/download/config.schema.json", + "server": { + "name": "AFFiNE Self Hosted Server" + } +} diff --git a/nginx.conf b/nginx.conf new file mode 100644 index 0000000..8d2790c --- /dev/null +++ b/nginx.conf @@ -0,0 +1,69 @@ +worker_processes auto; +error_log /dev/stderr info; +pid /run/nginx.pid; + +events { + worker_connections 1024; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + keepalive_timeout 65; + types_hash_max_size 4096; + + log_format cloudron '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" ' + '"$http_user_agent"'; + access_log /dev/stdout cloudron; + + map $http_upgrade $connection_upgrade { + default upgrade; + '' close; + } + + upstream affine_upstream { + server 127.0.0.1:3010; + keepalive 32; + } + + server { + listen 3000; + server_name _; + client_max_body_size 200m; + + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header Host $host; + proxy_set_header X-Forwarded-Host $host; + + location /static/ { + alias /app/code/affine/static/; + expires 1h; + add_header Cache-Control "public"; + try_files $uri @affine_app; + } + + location = /api/v1/session/callback { + proxy_pass http://affine_upstream; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + } + + location / { + try_files $uri @affine_app; + } + + location @affine_app { + proxy_pass http://affine_upstream; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header X-Real-IP $remote_addr; + proxy_buffering off; + } + } +} diff --git a/run-affine.sh b/run-affine.sh new file mode 100644 index 0000000..718aaa8 --- /dev/null +++ b/run-affine.sh @@ -0,0 +1,15 @@ +#!/bin/bash +set -euo pipefail + +APP_DIR=${APP_BUILD_DIR:-/app/code/affine} +cd "$APP_DIR" + +log() { + printf '[%s] %s\n' "$(date --iso-8601=seconds)" "$*" +} + +log "Running AFFiNE pre-deployment migrations" +node ./scripts/self-host-predeploy.js + +log "Starting AFFiNE server" +exec node ./dist/main.js diff --git a/start.sh b/start.sh new file mode 100644 index 0000000..8c2ce70 --- /dev/null +++ b/start.sh @@ -0,0 +1,180 @@ +#!/bin/bash +set -euo pipefail + +APP_CODE_DIR=${APP_CODE_DIR:-/app/code} +APP_DATA_DIR=${APP_DATA_DIR:-/app/data} +APP_RUNTIME_DIR=${APP_RUNTIME_DIR:-/run/affine} +APP_TMP_DIR=${APP_TMP_DIR:-/tmp/data} +APP_BUILD_DIR=${APP_BUILD_DIR:-/app/code/affine} +AFFINE_HOME=/home/cloudron/.affine +export APP_CODE_DIR APP_DATA_DIR APP_RUNTIME_DIR APP_TMP_DIR APP_BUILD_DIR + +log() { + printf '[%s] %s\n' "$(date --iso-8601=seconds)" "$*" +} + +require_env() { + local var_name="$1" + if [ -z "${!var_name:-}" ]; then + echo "Environment variable ${var_name} is not set" >&2 + exit 1 + fi +} + +prepare_data_dirs() { + log "Preparing persistent directories" + mkdir -p "$APP_DATA_DIR/config" "$APP_DATA_DIR/storage" "$APP_DATA_DIR/logs" "$APP_RUNTIME_DIR" "$AFFINE_HOME" + + if [ ! -f "$APP_DATA_DIR/config/config.json" ]; then + log "Seeding default configuration" + cp "$APP_TMP_DIR/config/config.json" "$APP_DATA_DIR/config/config.json" + fi + + local storage_contents="" + if [ -d "$APP_DATA_DIR/storage" ]; then + storage_contents=$(ls -A "$APP_DATA_DIR/storage" 2>/dev/null || true) + fi + if [ ! -d "$APP_DATA_DIR/storage" ] || [ -z "$storage_contents" ]; then + cp -a "$APP_TMP_DIR/storage/." "$APP_DATA_DIR/storage/" 2>/dev/null || true + fi + + rm -rf "$AFFINE_HOME/config" "$AFFINE_HOME/storage" + ln -sf "$APP_DATA_DIR/config" "$AFFINE_HOME/config" + ln -sf "$APP_DATA_DIR/storage" "$AFFINE_HOME/storage" + + chown -R cloudron:cloudron "$APP_DATA_DIR" "$APP_RUNTIME_DIR" /home/cloudron +} + +configure_database() { + require_env CLOUDRON_POSTGRESQL_URL + local db_url="$CLOUDRON_POSTGRESQL_URL" + if [[ "$db_url" == postgres://* ]]; then + db_url="postgresql://${db_url#postgres://}" + fi + export DATABASE_URL="$db_url" + log "Configured PostgreSQL endpoint" +} + +configure_redis() { + require_env CLOUDRON_REDIS_URL + local redis_info + redis_info=$(python3 - <<'PY' +import os +from urllib.parse import urlparse +url = os.environ.get('CLOUDRON_REDIS_URL') +if not url: + raise SystemExit('redis url missing') +parsed = urlparse(url) +host = parsed.hostname or 'localhost' +port = parsed.port or 6379 +password = parsed.password or '' +db = (parsed.path or '/0').lstrip('/') or '0' +print(f"{host}\n{port}\n{password}\n{db}") +PY +) + IFS=$'\n' read -r host port password db <<<"$redis_info" + export REDIS_SERVER_HOST="$host" + export REDIS_SERVER_PORT="$port" + export REDIS_SERVER_PASSWORD="$password" + export REDIS_URL="$CLOUDRON_REDIS_URL" + export REDIS_SERVER_URL="$CLOUDRON_REDIS_URL" + log "Configured Redis endpoint" +} + +configure_mail() { + if [ -z "${CLOUDRON_MAIL_SMTP_SERVER:-}" ]; then + log "Cloudron mail addon not configured, skipping SMTP setup" + return + fi + export MAILER_HOST="$CLOUDRON_MAIL_SMTP_SERVER" + export MAILER_PORT="${CLOUDRON_MAIL_SMTP_PORT:-587}" + export MAILER_USER="${CLOUDRON_MAIL_SMTP_USERNAME:-}" + export MAILER_PASSWORD="${CLOUDRON_MAIL_SMTP_PASSWORD:-}" + export MAILER_SENDER="${CLOUDRON_MAIL_FROM:-AFFiNE }" + export MAILER_SERVERNAME="${MAILER_SERVERNAME:-AFFiNE Server}" + log "Configured SMTP relay" +} + +configure_server_metadata() { + if [ -n "${CLOUDRON_APP_ORIGIN:-}" ]; then + export AFFINE_SERVER_EXTERNAL_URL="$CLOUDRON_APP_ORIGIN" + local host + host=$(python3 - <