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`.
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
repo/.git
|
||||||
@@ -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)
|
||||||
@@ -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"
|
||||||
|
}
|
||||||
@@ -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"]
|
||||||
@@ -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://<app-domain>/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.
|
||||||
+123
@@ -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" <<EOF
|
||||||
|
default: &default
|
||||||
|
adapter: postgresql
|
||||||
|
encoding: unicode
|
||||||
|
host: ${DB_HOST}
|
||||||
|
port: ${DB_PORT}
|
||||||
|
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
|
||||||
|
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" <<EOF
|
||||||
|
# Generated by start.sh on every boot - manual edits will be lost.
|
||||||
|
shared:
|
||||||
|
map_tiles_provider: "//{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
|
map_tiles_provider_attribution: "OpenStreetMap contributors"
|
||||||
|
|
||||||
|
production:
|
||||||
|
secret_key_base: ${SECRET_KEY_BASE}
|
||||||
|
server_name: ${SERVER_NAME}
|
||||||
|
force_ssl: false
|
||||||
|
delay_jobs: true
|
||||||
|
multitenancy: false
|
||||||
|
http_basic_username: ""
|
||||||
|
http_basic_password: ""
|
||||||
|
authentication_logs: false
|
||||||
|
devise_lockable: false
|
||||||
|
oidc_client_id: "${OIDC_CLIENT_ID}"
|
||||||
|
oidc_client_secret: "${OIDC_CLIENT_SECRET}"
|
||||||
|
oidc_issuer: "${OIDC_ISSUER}"
|
||||||
|
EOF
|
||||||
|
chmod 600 "${DATA_DIR}/secrets.yml"
|
||||||
|
|
||||||
|
# --- 2. memcached (localhost, as upstream's image intends) ----------------------
|
||||||
|
memcached -d -p 11211 -u consul -m 64
|
||||||
|
|
||||||
|
# --- 3. database lifecycle -------------------------------------------------------
|
||||||
|
wait_tcp "${DB_HOST}" "${DB_PORT}" "PostgreSQL"
|
||||||
|
|
||||||
|
if [[ ! -f "${MARKER}" ]]; then
|
||||||
|
echo "First boot: creating + migrating + seeding the database ..."
|
||||||
|
bundle exec rails db:create db:migrate db:seed
|
||||||
|
touch "${MARKER}"
|
||||||
|
echo "Database seeded (local admin: admin@consul.dev - change the password!)"
|
||||||
|
else
|
||||||
|
bundle exec rails db:migrate
|
||||||
|
fi
|
||||||
|
|
||||||
|
# assets:precompile needs the booted environment (DB-dependent
|
||||||
|
# initializers), so it runs here - after the PG wait, every boot
|
||||||
|
# (idempotent: fast no-op when the manifests are current)
|
||||||
|
bundle exec rails assets:precompile
|
||||||
|
|
||||||
|
# --- 4. delayed_job worker + web server ------------------------------------------
|
||||||
|
# gate on the web port (migrations done) so the worker never races the schema
|
||||||
|
(
|
||||||
|
until (exec 3<>/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
|
||||||
@@ -75,8 +75,9 @@ cmd_up() {
|
|||||||
$(label_args) redis:7-alpine --requirepass testrd >/dev/null ;;
|
$(label_args) redis:7-alpine --requirepass testrd >/dev/null ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
# the same env contract Cloudron injects, pointed at the test services
|
# the same env contract Cloudron injects, pointed at the test services.
|
||||||
docker run -d --rm --name "$APP" --cgroup-parent "$SLICE" --network "$NET" \
|
# 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" \
|
$(label_args) -v "grind-$APPNAME-data:/app/data" \
|
||||||
-p "127.0.0.1:$TESTPORT:$PORT" \
|
-p "127.0.0.1:$TESTPORT:$PORT" \
|
||||||
-e CLOUDRON_POSTGRESQL_HOST="$PG" -e CLOUDRON_POSTGRESQL_PORT=5432 \
|
-e CLOUDRON_POSTGRESQL_HOST="$PG" -e CLOUDRON_POSTGRESQL_PORT=5432 \
|
||||||
|
|||||||
Reference in New Issue
Block a user