From 7c65dc18d3775bb87b994a2a8b22e025833dca20 Mon Sep 17 00:00:00 2001 From: reachableceo Date: Sun, 6 Sep 2026 18:03:58 -0500 Subject: [PATCH] feat(consuldemocracy): add initial Cloudron package Build from source on ruby:3.4.10-trixie (trimmed from the upstream dev Dockerfile: no Chromium, no sudo, fixed non-root user). Uses the postgresql and localstorage addons; start.sh regenerates database.yml/secrets.yml under /app/data every boot, seeds the DB once (marker file) with a local admin, runs memcached in-container, and backgrounds a delayed_job worker gated on migrations. Platform OIDC is wired into secrets.yml; local login stays for the seeded admin. Also drop --rm from the app container in grind-stack.sh so a crashed container keeps its logs for `logs`. --- .../consuldemocracy/.dockerignore | 1 + .../consuldemocracy/CHANGELOG.md | 13 ++ .../consuldemocracy/CloudronManifest.json | 22 ++++ .../Collaboration/consuldemocracy/Dockerfile | 86 ++++++++++++ .../Collaboration/consuldemocracy/README.md | 49 +++++++ .../Collaboration/consuldemocracy/start.sh | 123 ++++++++++++++++++ scripts/grind-stack.sh | 5 +- 7 files changed, 297 insertions(+), 2 deletions(-) create mode 100644 Package-Workspace/Collaboration/consuldemocracy/.dockerignore create mode 100644 Package-Workspace/Collaboration/consuldemocracy/CHANGELOG.md create mode 100644 Package-Workspace/Collaboration/consuldemocracy/CloudronManifest.json create mode 100644 Package-Workspace/Collaboration/consuldemocracy/Dockerfile create mode 100644 Package-Workspace/Collaboration/consuldemocracy/README.md create mode 100755 Package-Workspace/Collaboration/consuldemocracy/start.sh diff --git a/Package-Workspace/Collaboration/consuldemocracy/.dockerignore b/Package-Workspace/Collaboration/consuldemocracy/.dockerignore new file mode 100644 index 0000000..3c3a4cd --- /dev/null +++ b/Package-Workspace/Collaboration/consuldemocracy/.dockerignore @@ -0,0 +1 @@ +repo/.git diff --git a/Package-Workspace/Collaboration/consuldemocracy/CHANGELOG.md b/Package-Workspace/Collaboration/consuldemocracy/CHANGELOG.md new file mode 100644 index 0000000..3d306d6 --- /dev/null +++ b/Package-Workspace/Collaboration/consuldemocracy/CHANGELOG.md @@ -0,0 +1,13 @@ +# Changelog — ConsulDemocracy Cloudron Package + +## 1.0.0 — 2026-09-06 + +- Initial package: build-from-source on ruby:3.4.10-trixie (upstream + commit at package time; trimmed dev Dockerfile — no Chromium) +- postgresql + localstorage addons; Rails server on :3000 +- Generated configs (database.yml / secrets.yml) under /app/data, + regenerated each boot; persisted secret_key_base +- First-boot db:create/migrate/seed with marker; gated delayed_job + worker; in-container memcached +- Platform OIDC wired into secrets.yml; local admin seeded + (admin@consul.dev — password change required on first login) diff --git a/Package-Workspace/Collaboration/consuldemocracy/CloudronManifest.json b/Package-Workspace/Collaboration/consuldemocracy/CloudronManifest.json new file mode 100644 index 0000000..00a8e2a --- /dev/null +++ b/Package-Workspace/Collaboration/consuldemocracy/CloudronManifest.json @@ -0,0 +1,22 @@ +{ + "manifestVersion": 2, + "type": "app", + "id": "io.cloudron.consuldemocracy", + "title": "ConsulDemocracy", + "description": "Open-source citizen participation platform (the engine behind Madrid's Consul): proposals with supports, participatory budgeting, debates, polls, collaborative legislation, and community voting. Rails on PostgreSQL with delayed_job background jobs. Single sign-on via the Cloudron OIDC provider; local login available for the seeded admin.", + "author": "Consul Democracy contributors", + "website": "https://consuldemocracy.org/", + "contactEmail": "cloudron@tsys.dev", + "tagline": "Citizen participation: proposals, budgets, debates and voting", + "version": "1.0.0", + "healthCheckPath": "/", + "httpPort": 3000, + "memoryLimit": 2048, + "addons": { + "localstorage": {}, + "postgresql": {} + }, + "mediaLinks": [], + "changelog": "Initial Cloudron package for ConsulDemocracy (build-from-source on ruby 3.4.10, trimmed from the upstream dev Dockerfile: no Chromium, fixed non-root user). start.sh regenerates config/database.yml + config/secrets.yml under /app/data every boot (addon credentials rotate), runs memcached locally, seeds the DB once (marker file; local admin admin@consul.dev - password must be changed on first login), backgrounds a gated delayed_job worker and serves Rails on :3000. OIDC wired from the platform provider into secrets.yml.", + "icon": "file://logo.png" +} diff --git a/Package-Workspace/Collaboration/consuldemocracy/Dockerfile b/Package-Workspace/Collaboration/consuldemocracy/Dockerfile new file mode 100644 index 0000000..7c278e3 --- /dev/null +++ b/Package-Workspace/Collaboration/consuldemocracy/Dockerfile @@ -0,0 +1,86 @@ +# ConsulDemocracy Cloudron Package +# +# ConsulDemocracy is the citizen participation platform behind Madrid's +# Consul (MIT): proposals, participatory budgeting, debates, polls, +# legislation co-editing. Rails 8 + PostgreSQL, single web process plus +# a delayed_job worker; memcached for the fragment cache. +# +# Upstream: https://github.com/consuldemocracy/consuldemocracy +# - No published image; the in-repo Dockerfile targets local dev +# (bind-mount uid remapping, Chromium for E2E tests). This package +# builds from source on the same ruby base, trimmed: no Chromium, +# no sudo, fixed non-root user, runtime configs via symlinks into +# /app/data (generated by start.sh on every boot). +# +# Authentication: NATIVE OIDC (preferred). devise + omniauth with +# omniauth_openid_connect (name: :oidc, discovery: true) configured +# per-environment in config/secrets.yml (oidc_client_id / +# oidc_client_secret / oidc_issuer - see devise.rb:289-296 and +# app/lib/omniauth_tenant_setup.rb). start.sh maps the Cloudron +# platform provider (CLOUDRON_OIDC_*) into the generated secrets.yml. +# SAML is also available; local login remains for the seeded admin. +# +# Pattern: build-from-source (Rails). Gemfile.lock + package.json are +# committed upstream, so bundle/npm install are deterministic. +FROM ruby:3.4.10-trixie + +ENV RAILS_ROOT=/var/www/consul \ + RAILS_ENV=production \ + RAILS_LOG_TO_STDOUT=true + +# runtime deps: imagemagick (upload processing), libpq + postgres client +# (pg gem + db tasks), memcached (production cache_store is mem_cache_store +# with dalli), plus build headers for the native gems below +RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ + build-essential cmake pkg-config imagemagick libpq-dev \ + postgresql-client memcached \ + && rm -rf /var/lib/apt/lists/* + +RUN adduser --shell /bin/bash --disabled-password --gecos "" --uid 1000 consul + +WORKDIR $RAILS_ROOT + +# gem layer first (cache marker): lockfile-driven, system-wide install +# (.ruby-version MUST keep its name: the Gemfile resolves ruby file: ".ruby-version") +COPY repo/.ruby-version ./ +COPY repo/Gemfile repo/Gemfile.lock ./ +# the Gemfile evals ./Gemfile_custom (developer-local, not in git); empty is valid +RUN touch Gemfile_custom && \ + bundle config set --local without 'development test' && bundle install --jobs 4 + +# node for the asset pipeline (node-build pinned by .node-version) +COPY repo/.node-version ./ +ENV PATH=/usr/local/node/bin:$PATH +RUN curl -sL https://github.com/nodenv/node-build/archive/master.tar.gz | tar xz -C /tmp/ && \ + /tmp/node-build-master/bin/node-build "$(cat .node-version)" /usr/local/node && \ + rm -rf /tmp/node-build-master + +COPY repo/package.json repo/package-lock.json ./ +RUN npm install + +COPY repo/ ./ + +# Rails 8's regexp_timeout default kills assets:precompile on the +# graphiql minified-JS regexes under CPU contention; disable it (the +# platform proxy bounds request time anyway). Also let the runtime user +# write tmp/log/public/assets and db/ (apartment schema dump warning). +RUN echo 'Regexp.timeout = nil' > config/initializers/00_cloudron_regexp_timeout.rb && \ + mkdir -p public/assets tmp/pids tmp/sockets log db && \ + chown -R consul:consul public/assets tmp log db + +# persistence: runtime-generated configs + marker files live in localstorage +# (repo ships only .example files, the real ones are generated at start). +# VOLUME + ownership: a fresh named volume inherits this dir's owner, so +# start.sh (running as consul, uid 1000) can write its generated configs +RUN mkdir -p /app/data && chown consul:consul /app/data && \ + ln -s /app/data/secrets.yml config/secrets.yml && \ + ln -s /app/data/database.yml config/database.yml +VOLUME /app/data + +COPY start.sh /app/start.sh + +EXPOSE 3000 + +USER consul + +ENTRYPOINT ["/bin/bash", "/app/start.sh"] diff --git a/Package-Workspace/Collaboration/consuldemocracy/README.md b/Package-Workspace/Collaboration/consuldemocracy/README.md new file mode 100644 index 0000000..5cdfb87 --- /dev/null +++ b/Package-Workspace/Collaboration/consuldemocracy/README.md @@ -0,0 +1,49 @@ +# ConsulDemocracy — Cloudron Package + +[ConsulDemocracy](https://github.com/consuldemocracy/consuldemocracy) — the +open-source citizen participation platform behind Madrid's Consul: +proposals with supports, participatory budgeting, debates, polls, +collaborative legislation, community voting. Rails 8 + PostgreSQL + +delayed_job; packaged **build-from-source** on `ruby:3.4.10-trixie` +(trimmed from upstream's dev Dockerfile — no Chromium, no sudo, fixed +non-root user, uid 1000). + +## Authentication (auth gate: ✅ OIDC preferred) + +Generic OpenID Connect is implemented in code via +`omniauth_openid_connect` (devise.rb:289-296, `name: :oidc`, +`discovery: true`), configured through `config/secrets.yml` +(`oidc_client_id` / `oidc_client_secret` / `oidc_issuer`). `start.sh` +regenerates secrets.yml on every boot with the Cloudron platform +provider values (`CLOUDRON_OIDC_*`). + +- SSO redirect: `https:///users/auth/oidc/callback` +- Local login stays available alongside SSO. +- SAML is also supported (saml_* keys in secrets.yml) if an external + IdP is preferred. + +**First boot creates a local admin: `admin@consul.dev` / `12345678` — +log in and change that password immediately** (also update the account +email; it is the recovery path if OIDC is ever unavailable). + +## Runtime layout + +| Concern | How | +|---------|-----| +| Database | Cloudron `postgresql` addon; `db:create db:migrate db:seed` on first boot (marker `.db_seeded` in /app/data), `db:migrate` on later boots | +| Config | `config/database.yml` + `config/secrets.yml` are symlinks to generated files in `/app/data` — rewritten every boot (addon passwords rotate on restore/migration) | +| secret_key_base | Generated once (64 hex chars), persisted in `/app/data` | +| Background jobs | delayed_job worker backgrounded by start.sh, gated on the web port (starts only after migrations) | +| Cache | memcached inside the container (production `cache_store` is `mem_cache_store`/dalli, matching upstream's image) | +| Multitenancy | Off (`multitenancy: false`); single-tenant is the sane Cloudron default | +| force_ssl | Off at the app — Cloudron's proxy terminates TLS | + +## Notes + +- First boot takes several minutes (schema + seeds). The marker file + makes subsequent boots fast. +- The seeds include default participation settings and the local admin; + everything else (settings, tags, banners) is configured in the admin + UI after login. +- Package pattern + verification details: repo `JOURNAL.md`, the + ConsulDemocracy section. diff --git a/Package-Workspace/Collaboration/consuldemocracy/start.sh b/Package-Workspace/Collaboration/consuldemocracy/start.sh new file mode 100755 index 0000000..9c38c55 --- /dev/null +++ b/Package-Workspace/Collaboration/consuldemocracy/start.sh @@ -0,0 +1,123 @@ +#!/bin/bash +set -euo pipefail + +# ConsulDemocracy runtime setup for Cloudron: +# 1. generate config/database.yml + config/secrets.yml under /app/data +# (config/* are symlinks; addon creds rotate on restore/migration, so +# both files are rewritten on EVERY start) +# 2. start memcached (production cache_store is mem_cache_store/dalli) +# 3. wait for the postgresql addon, then create/migrate/seed the DB once +# (marker file) or just migrate on later boots +# 4. background the delayed_job worker (gated on the web port, so the +# schema exists first), then exec the Rails server on :3000 +# +# Authentication: OIDC via the Cloudron platform provider, mapped into +# secrets.yml (oidc_client_id/secret/issuer). The seeds create a local +# admin (admin@consul.dev / 12345678) - CHANGE ITS PASSWORD on first +# login (see README). + +DATA_DIR="/app/data" +SECRET_FILE="${DATA_DIR}/.secret_key_base" +MARKER="${DATA_DIR}/.db_seeded" + +cd /var/www/consul + +# --- 1. runtime configs (regenerated every boot) ------------------------------- +wait_tcp() { + local host="$1" port="$2" name="$3" + echo "Waiting for ${name} at ${host}:${port} ..." + until (exec 3<>"/dev/tcp/${host}/${port}") 2>/dev/null; do + echo "${name} is unavailable - sleeping" + sleep 2 + done + echo "${name} is up" +} + +yml_escape() { printf '%s' "$1" | sed -e 's/\\/\\\\/g' -e "s/'/\\\\'/g"; } + +if [[ ! -s "${SECRET_FILE}" ]]; then + ( umask 077; openssl rand -hex 32 > "${SECRET_FILE}" ) + echo "Generated new secret_key_base" +fi +SECRET_KEY_BASE="$(cat "${SECRET_FILE}")" + +DB_HOST="${CLOUDRON_POSTGRESQL_HOST:-127.0.0.1}" +DB_PORT="${CLOUDRON_POSTGRESQL_PORT:-5432}" +DB_NAME="${CLOUDRON_POSTGRESQL_DATABASE:-consul}" +DB_USER="${CLOUDRON_POSTGRESQL_USERNAME:-consul}" +DB_PASS="${CLOUDRON_POSTGRESQL_PASSWORD:-}" + +cat > "${DATA_DIR}/database.yml" < + schema_search_path: "public,shared_extensions" + username: ${DB_USER} + password: <%= ENV["PGPASSWORD"] %> + +production: + <<: *default + database: ${DB_NAME} +EOF +export PGPASSWORD="${DB_PASS}" + +OIDC_CLIENT_ID="$(yml_escape "${CLOUDRON_OIDC_CLIENT_ID:-}")" +OIDC_CLIENT_SECRET="$(yml_escape "${CLOUDRON_OIDC_CLIENT_SECRET:-}")" +OIDC_ISSUER="$(yml_escape "${CLOUDRON_OIDC_ISSUER:-}")" +SERVER_NAME="$(yml_escape "${CLOUDRON_APP_DOMAIN:-localhost}")" + +cat > "${DATA_DIR}/secrets.yml" </dev/tcp/127.0.0.1/3000) 2>/dev/null; do sleep 2; done + echo "web port is up - starting delayed_job worker" + exec bundle exec rake jobs:work +) & + +echo "Starting ConsulDemocracy on :3000 ..." +exec bundle exec rails server -b 0.0.0.0 -p 3000 diff --git a/scripts/grind-stack.sh b/scripts/grind-stack.sh index e6138ba..fb17173 100755 --- a/scripts/grind-stack.sh +++ b/scripts/grind-stack.sh @@ -75,8 +75,9 @@ cmd_up() { $(label_args) redis:7-alpine --requirepass testrd >/dev/null ;; esac - # the same env contract Cloudron injects, pointed at the test services - docker run -d --rm --name "$APP" --cgroup-parent "$SLICE" --network "$NET" \ + # the same env contract Cloudron injects, pointed at the test services. + # No --rm on the app: a crashed container must keep its logs for `logs`. + docker run -d --name "$APP" --cgroup-parent "$SLICE" --network "$NET" \ $(label_args) -v "grind-$APPNAME-data:/app/data" \ -p "127.0.0.1:$TESTPORT:$PORT" \ -e CLOUDRON_POSTGRESQL_HOST="$PG" -e CLOUDRON_POSTGRESQL_PORT=5432 \