diff --git a/2026/2026-08-17--decidim.md b/2026/2026-08-17--decidim.md
new file mode 100644
index 0000000..688c4e3
--- /dev/null
+++ b/2026/2026-08-17--decidim.md
@@ -0,0 +1,33 @@
+# Decidim — decidim.hacktivism.ch (2026-08-17)
+
+Site: **https://decidim.hacktivism.ch/**
+Port **9027** → Caddy. Stack: `koopa-decidim` + `koopa-decidim-sidekiq` + `koopa-decidim-db` + `koopa-decidim-redis`. Image `decidim/decidim:0.30.8`.
+
+Live: `~/koopa-decidim/` (compose mirror `configs/decidim/`). Secrets only on host (`.env`, `users.env`).
+
+Instance name **hacktivism**. Theme: exchange-dark (gold `#e8a838`, bg `#1a1410`) via `theme/hacktivism.css`. Analog: `configs/lemmy/`.
+
+Admin users are on the host (`~/koopa-decidim/users.env`), not in this repo.
+
+Caddy: `sudo /home/hernani/koopa-caddy/apply.sh`
+Boot: `scripts/decidim/install-systemd.sh` (linger already yes on koopa).
+
+## Cookie / privacy
+
+Stock Decidim still emits `#dc-dialog-wrapper` (Accept all / essential / Settings). This instance has no third-party trackers; privacy is default.
+
+- CSS: `#dc-dialog-wrapper, .cookies__container { display: none !important; }`
+- JS: `/hacktivism-consent.js` sets `decidim-consent={"essential":true}` if missing
+- Live 2026-08-17: HTML 200, CSS/JS 200, hide rules in public `/hacktivism.css`
+
+## Reboot (checked 2026-08-17)
+
+| Item | Value |
+|------|--------|
+| linger `hernani` | yes |
+| `container-koopa-decidim-db` | enabled + active |
+| `container-koopa-decidim` | enabled + active |
+| `container-koopa-lemmy-db` / `container-koopa-lemmy` | enabled + active |
+| listen | `:9027` `:9026` |
+
+login-health: probes + containers + linger + these units enabled. Loopback HTTP on :9027 is **301** (app wants HTTPS; TLS is Caddy). Probe allows 301. After reboot: login-health only unless asked.
diff --git a/SECRETS.md b/SECRETS.md
index 7500471..5b48906 100644
--- a/SECRETS.md
+++ b/SECRETS.md
@@ -19,6 +19,7 @@ Live passwords/keys live in sibling **`koopa-admin-secrets`**.
|------|--------|------|
| Castopod | `koopa/home-hernani/koopa-castopod/{.env,users.env}` (+ compose mirror in admin-log `configs/castopod/`) | `~/koopa-castopod/` |
| Lemmy | host-only `~/koopa-lemmy/{.env,lemmy.hjson}` (compose mirror in admin-log `configs/lemmy/`) | `~/koopa-lemmy/` |
+| Decidim | host-only `~/koopa-decidim/{.env,users.env}` (compose mirror in admin-log `configs/decidim/`) | `~/koopa-decidim/` |
| Bonfire (`FEDERATE=true`) | `koopa/home-hernani/koopa-bonfire/{.env,users.env,compose.yml}` | `~/koopa-bonfire/` |
| Prime (Jellyfin + qBittorrent) | `koopa/home-hernani/koopa-prime/{.env,users.env}` | `~/koopa-prime/` |
| Forgejo (**rootless**, git.hacktivism.ch) | `koopa/home-hernani/koopa-forgejo/{.env,users.env,compose.yml}` | `~/koopa-forgejo/` |
diff --git a/configs/caddy/Caddyfile b/configs/caddy/Caddyfile
index 58fb8a5..bacff1e 100644
--- a/configs/caddy/Caddyfile
+++ b/configs/caddy/Caddyfile
@@ -1,7 +1,7 @@
# Internal only (not in the browser URL):
# 9010 merchant API | 9011 exchange API | 9012 bank API
# 9013 bank landing | 9014 exchange landing | 9015 merchant landing
-# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9200 forgejo-ssh
+# 9020 castopod | 9021 bonfire | 9022 prime | 9023 bt | 9024 forgejo | 9025 paivana | 9026 lemmy | 9027 decidim | 9200 forgejo-ssh
# 9090 tops ng1 | 9091 tops ng2 | 9092 tops ng3
{
@@ -39,28 +39,28 @@ taler.hacktivism.ch {
# surface + aptdeploy HTML (taler.hacktivism.ch only)
handle /taler-monitoring-surface_err {
- redir * /taler-monitoring-surface_err/ 302
+ redir /taler-monitoring-surface_err/ 302
}
handle /taler-monitoring-surface_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-surface {
- redir * /taler-monitoring-surface/ 302
+ redir /taler-monitoring-surface/ 302
}
handle /taler-monitoring-surface* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-aptdeploy_err {
- redir * /taler-monitoring-aptdeploy_err/ 302
+ redir /taler-monitoring-aptdeploy_err/ 302
}
handle /taler-monitoring-aptdeploy_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-aptdeploy {
- redir * /taler-monitoring-aptdeploy/ 302
+ redir /taler-monitoring-aptdeploy/ 302
}
handle /taler-monitoring-aptdeploy* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -68,14 +68,14 @@ taler.hacktivism.ch {
}
handle /taler-monitoring-mattermost_err {
- redir * /taler-monitoring-mattermost_err/ 302
+ redir /taler-monitoring-mattermost_err/ 302
}
handle /taler-monitoring-mattermost_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-mattermost {
- redir * /taler-monitoring-mattermost/ 302
+ redir /taler-monitoring-mattermost/ 302
}
handle /taler-monitoring-mattermost* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -83,14 +83,14 @@ taler.hacktivism.ch {
}
handle /taler-monitoring-mail_err {
- redir * /taler-monitoring-mail_err/ 302
+ redir /taler-monitoring-mail_err/ 302
}
handle /taler-monitoring-mail_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /taler-monitoring-mail {
- redir * /taler-monitoring-mail/ 302
+ redir /taler-monitoring-mail/ 302
}
handle /taler-monitoring-mail* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -99,14 +99,14 @@ taler.hacktivism.ch {
# taler-monitoring console HTML (host static — not in app containers)
handle /monitoring_err {
- redir * /monitoring_err/ 302
+ redir /monitoring_err/ 302
}
handle /monitoring_err* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
file_server
}
handle /monitoring {
- redir * /monitoring/ 302
+ redir /monitoring/ 302
}
handle /monitoring* {
root * /var/www/monitoring-sites/taler.hacktivism.ch
@@ -143,14 +143,14 @@ exchange.hacktivism.ch {
# taler-monitoring console HTML (host static — not in app containers)
handle /monitoring_err {
- redir * /monitoring_err/ 302
+ redir /monitoring_err/ 302
}
handle /monitoring_err* {
root * /var/www/monitoring-sites/exchange.hacktivism.ch
file_server
}
handle /monitoring {
- redir * /monitoring/ 302
+ redir /monitoring/ 302
}
handle /monitoring* {
root * /var/www/monitoring-sites/exchange.hacktivism.ch
@@ -177,14 +177,14 @@ bank.hacktivism.ch {
# taler-monitoring console HTML (host static — not in app containers)
handle /monitoring_err {
- redir * /monitoring_err/ 302
+ redir /monitoring_err/ 302
}
handle /monitoring_err* {
root * /var/www/monitoring-sites/bank.hacktivism.ch
file_server
}
handle /monitoring {
- redir * /monitoring/ 302
+ redir /monitoring/ 302
}
handle /monitoring* {
root * /var/www/monitoring-sites/bank.hacktivism.ch
@@ -315,7 +315,24 @@ lemmy.hacktivism.ch {
}
}
-http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch {
+# 9027 decidim (rails)
+decidim.hacktivism.ch {
+ header Alt-Svc "clear"
+ reverse_proxy 127.0.0.1:9027 {
+ header_up Host {host}
+ header_up X-Forwarded-For {remote_host}
+ header_up X-Forwarded-Host {host}
+ header_up X-Forwarded-Proto {scheme}
+ header_up X-Forwarded-Port 443
+ flush_interval -1
+ transport http {
+ read_timeout 3600s
+ write_timeout 3600s
+ }
+ }
+}
+
+http://taler.hacktivism.ch, http://exchange.hacktivism.ch, http://bank.hacktivism.ch, http://castopod.hacktivism.ch, http://bonfire.hacktivism.ch, http://prime.hacktivism.ch, http://bt.hacktivism.ch, http://git.hacktivism.ch, http://paivana.hacktivism.ch, http://tops.ng1.hacktivism.ch, http://tops.ng2.hacktivism.ch, http://tops.ng3.hacktivism.ch, http://lemmy.hacktivism.ch, http://decidim.hacktivism.ch {
handle /.well-known/acme-challenge/* {
root * /var/www/acme
file_server
diff --git a/configs/decidim/.env.example b/configs/decidim/.env.example
new file mode 100644
index 0000000..303db55
--- /dev/null
+++ b/configs/decidim/.env.example
@@ -0,0 +1,13 @@
+POSTGRES_PASSWORD=@inline-secret@
+SECRET_KEY_BASE=@inline-secret@
+DECIDIM_APPLICATION_NAME=hacktivism
+DECIDIM_MAILER_SENDER=noreply@hacktivism.ch
+DECIDIM_AVAILABLE_LOCALES=en,de,fr
+DECIDIM_DEFAULT_LOCALE=en
+DECIDIM_HOST=decidim.hacktivism.ch
+SMTP_ADDRESS=127.0.0.1
+SMTP_DOMAIN=hacktivism.ch
+SMTP_USERNAME=unused
+SMTP_PASSWORD=unused
+SMTP_PORT=25
+QUEUE_ADAPTER=sidekiq
diff --git a/configs/decidim/README.md b/configs/decidim/README.md
new file mode 100644
index 0000000..c0433f4
--- /dev/null
+++ b/configs/decidim/README.md
@@ -0,0 +1,25 @@
+# Decidim — decidim.hacktivism.ch
+
+| Item | Value |
+|------|--------|
+| Live | `/home/hernani/koopa-decidim/` |
+| Compose | `compose.yml` (mirror, no secrets) |
+| Port | **9027** → Caddy |
+| Site | https://decidim.hacktivism.ch/ |
+| Image | `decidim/decidim:0.30.8` + postgres 16 + redis 7 + sidekiq |
+
+Secrets: host `~/koopa-decidim/{.env,users.env}` (mode 600). Not in this repo.
+
+Theme: `theme/hacktivism.css` + `theme/hacktivism.rb` (same exchange-dark palette as Lemmy/Bonfire). Analog stack: `configs/lemmy/`.
+
+Privacy: no third-party trackers. Stock GDPR banner is hidden (`#dc-dialog-wrapper`); `theme/hacktivism-consent.js` sets essential-only `decidim-consent`.
+
+```bash
+cd ~/koopa-decidim && set -a && source .env && set +a && podman-compose up -d
+scripts/decidim/install-systemd.sh
+scripts/decidim/apply-branding.sh
+```
+
+Caddy: `sudo /home/hernani/koopa-caddy/apply.sh`
+
+Daylog: `2026/2026-08-17--decidim.md`.
diff --git a/configs/decidim/assets/img/favicon.svg b/configs/decidim/assets/img/favicon.svg
new file mode 100644
index 0000000..e1d3dc3
--- /dev/null
+++ b/configs/decidim/assets/img/favicon.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/configs/decidim/assets/img/logo.svg b/configs/decidim/assets/img/logo.svg
new file mode 100644
index 0000000..e1d3dc3
--- /dev/null
+++ b/configs/decidim/assets/img/logo.svg
@@ -0,0 +1,90 @@
+
+
diff --git a/configs/decidim/bootstrap.rb b/configs/decidim/bootstrap.rb
new file mode 100644
index 0000000..8ec96a1
--- /dev/null
+++ b/configs/decidim/bootstrap.rb
@@ -0,0 +1,78 @@
+# frozen_string_literal: true
+# Create system admin + organization + org admin. Idempotent.
+# Usage: bundle exec rails runner /code/lib/koopa_bootstrap.rb
+
+host = ENV.fetch("DECIDIM_HOST", "decidim.hacktivism.ch")
+sys_email = ENV.fetch("DECIDIM_SYSTEM_EMAIL")
+sys_password = ENV.fetch("DECIDIM_SYSTEM_PASSWORD")
+admin_email = ENV.fetch("DECIDIM_ADMIN_EMAIL")
+admin_password = ENV.fetch("DECIDIM_ADMIN_PASSWORD")
+admin_name = ENV.fetch("DECIDIM_ADMIN_NAME", "hacktivism")
+org_name = ENV.fetch("DECIDIM_APPLICATION_NAME", "hacktivism")
+
+sys = Decidim::System::Admin.find_or_initialize_by(email: sys_email)
+sys.password = sys_password
+sys.password_confirmation = sys_password
+sys.save!
+puts "system admin: #{sys.email}"
+
+org = Decidim::Organization.find_by(host: host)
+if org.nil?
+ attrs = {
+ host: host,
+ default_locale: ENV.fetch("DECIDIM_DEFAULT_LOCALE", "en"),
+ available_locales: ENV.fetch("DECIDIM_AVAILABLE_LOCALES", "en,de,fr").split(","),
+ reference_prefix: "HCK",
+ time_zone: "Europe/Zurich",
+ users_registration_mode: "enabled",
+ force_users_to_authenticate_before_access_organization: false,
+ available_authorizations: [],
+ tos_version: Time.current,
+ colors: {
+ "primary" => "#e8a838",
+ "secondary" => "#3ecfbf",
+ "tertiary" => "#3d3128",
+ "success" => "#16a34a",
+ "warning" => "#f0d090",
+ "alert" => "#e7131a"
+ },
+ header_snippets: %()
+ }
+ name_val = { "en" => org_name, "de" => org_name, "fr" => org_name }
+ desc_val = { "en" => "hacktivism participatory platform", "de" => "hacktivism Partizipationsplattform", "fr" => "plateforme participative hacktivism" }
+ begin
+ org = Decidim::Organization.new(attrs.merge(name: name_val, description: desc_val))
+ org.save!
+ rescue StandardError
+ org = Decidim::Organization.new(attrs.merge(name: org_name, description: "hacktivism participatory platform"))
+ org.save!
+ end
+ Decidim::System::CreateDefaultPages.call(org) if defined?(Decidim::System::CreateDefaultPages)
+ Decidim::System::CreateDefaultContentBlocks.call(org) if defined?(Decidim::System::CreateDefaultContentBlocks)
+ puts "organization created host=#{org.host}"
+else
+ org.colors = {
+ "primary" => "#e8a838",
+ "secondary" => "#3ecfbf",
+ "tertiary" => "#3d3128",
+ "success" => "#16a34a",
+ "warning" => "#f0d090",
+ "alert" => "#e7131a"
+ }
+ org.header_snippets = %()
+ org.save!
+ puts "organization updated host=#{org.host}"
+end
+
+user = Decidim::User.find_or_initialize_by(email: admin_email, organization: org)
+user.name = admin_name
+user.nickname = admin_name.downcase.gsub(/[^a-z0-9_]/, "")[0, 20]
+user.password = admin_password
+user.password_confirmation = admin_password
+user.admin = true
+user.confirmed_at ||= Time.current
+user.tos_agreement = true
+user.accepted_tos_version = org.tos_version
+user.locale = org.default_locale
+user.save!
+puts "org admin: #{user.email}"
diff --git a/configs/decidim/compose.yml b/configs/decidim/compose.yml
new file mode 100644
index 0000000..282e1b3
--- /dev/null
+++ b/configs/decidim/compose.yml
@@ -0,0 +1,132 @@
+# koopa-decidim — Decidim behind host Caddy on :9027
+# Docs: https://docs.decidim.org/en/develop/install/
+# Image: official pre-generated app (core modules only). No secrets in this file.
+
+x-logging: &default-logging
+ driver: "json-file"
+ options:
+ max-size: "50m"
+ max-file: "4"
+
+x-app-env: &app-env
+ RAILS_ENV: production
+ RAILS_SERVE_STATIC_FILES: "true"
+ RAILS_LOG_TO_STDOUT: "true"
+ DATABASE_URL: postgres://decidim:${POSTGRES_PASSWORD}@postgres:5432/decidim
+ REDIS_URL: redis://redis:6379/0
+ SECRET_KEY_BASE: ${SECRET_KEY_BASE}
+ DECIDIM_APPLICATION_NAME: ${DECIDIM_APPLICATION_NAME:-hacktivism}
+ DECIDIM_MAILER_SENDER: ${DECIDIM_MAILER_SENDER:-noreply@hacktivism.ch}
+ DECIDIM_AVAILABLE_LOCALES: ${DECIDIM_AVAILABLE_LOCALES:-en,de,fr}
+ DECIDIM_DEFAULT_LOCALE: ${DECIDIM_DEFAULT_LOCALE:-en}
+ SMTP_ADDRESS: ${SMTP_ADDRESS:-127.0.0.1}
+ SMTP_DOMAIN: ${SMTP_DOMAIN:-hacktivism.ch}
+ SMTP_USERNAME: ${SMTP_USERNAME:-unused}
+ SMTP_PASSWORD: ${SMTP_PASSWORD:-unused}
+ SMTP_PORT: ${SMTP_PORT:-25}
+ SMTP_AUTHENTICATION: ${SMTP_AUTHENTICATION:-plain}
+ QUEUE_ADAPTER: ${QUEUE_ADAPTER:-sidekiq}
+
+services:
+ decidim:
+ image: docker.io/decidim/decidim:0.30.8
+ container_name: koopa-decidim
+ hostname: decidim
+ ports:
+ - "9027:3000"
+ env_file:
+ - .env
+ environment:
+ <<: *app-env
+ volumes:
+ - ./entrypoint.sh:/usr/local/bin/koopa-decidim-entrypoint.sh:ro
+ - ./theme/hacktivism.rb:/code/config/initializers/hacktivism.rb:ro
+ - ./theme/hacktivism.css:/code/public/hacktivism.css:ro
+ - ./theme/hacktivism-consent.js:/code/public/hacktivism-consent.js:ro
+ - ./bootstrap.rb:/code/lib/koopa_bootstrap.rb:ro
+ - ./assets/img/logo.svg:/code/public/hacktivism-logo.svg:ro
+ - ./assets/img/favicon.svg:/code/public/hacktivism-favicon.svg:ro
+ - decidim-storage:/code/storage
+ - decidim-uploads:/code/public/uploads
+ - decidim-tmp:/code/tmp
+ entrypoint: ["/bin/bash", "/usr/local/bin/koopa-decidim-entrypoint.sh"]
+ command: ["bundle", "exec", "rails", "s", "-b", "0.0.0.0", "-p", "3000"]
+ restart: unless-stopped
+ logging: *default-logging
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_started
+ labels:
+ org.hacktivism.service: decidim
+ org.hacktivism.host_port: "9027"
+ org.hacktivism.site: decidim.hacktivism.ch
+ org.hacktivism.managed_by: koopa-admin
+
+ sidekiq:
+ image: docker.io/decidim/decidim:0.30.8
+ container_name: koopa-decidim-sidekiq
+ hostname: sidekiq
+ env_file:
+ - .env
+ environment:
+ <<: *app-env
+ volumes:
+ - ./entrypoint.sh:/usr/local/bin/koopa-decidim-entrypoint.sh:ro
+ - ./theme/hacktivism.rb:/code/config/initializers/hacktivism.rb:ro
+ - decidim-storage:/code/storage
+ - decidim-uploads:/code/public/uploads
+ - decidim-tmp:/code/tmp
+ entrypoint: ["/bin/bash", "/usr/local/bin/koopa-decidim-entrypoint.sh"]
+ command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"]
+ restart: unless-stopped
+ logging: *default-logging
+ depends_on:
+ postgres:
+ condition: service_healthy
+ redis:
+ condition: service_started
+ labels:
+ org.hacktivism.service: decidim
+ org.hacktivism.managed_by: koopa-admin
+
+ postgres:
+ image: docker.io/library/postgres:16-alpine
+ container_name: koopa-decidim-db
+ hostname: postgres
+ environment:
+ - POSTGRES_USER=decidim
+ - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
+ - POSTGRES_DB=decidim
+ volumes:
+ - decidim-db:/var/lib/postgresql/data
+ restart: unless-stopped
+ logging: *default-logging
+ healthcheck:
+ test: ["CMD-SHELL", "pg_isready -U decidim -d decidim"]
+ interval: 5s
+ timeout: 5s
+ retries: 12
+ labels:
+ org.hacktivism.service: decidim
+ org.hacktivism.managed_by: koopa-admin
+
+ redis:
+ image: docker.io/library/redis:7-alpine
+ container_name: koopa-decidim-redis
+ hostname: redis
+ volumes:
+ - decidim-redis:/data
+ restart: unless-stopped
+ logging: *default-logging
+ labels:
+ org.hacktivism.service: decidim
+ org.hacktivism.managed_by: koopa-admin
+
+volumes:
+ decidim-db:
+ decidim-redis:
+ decidim-storage:
+ decidim-uploads:
+ decidim-tmp:
diff --git a/configs/decidim/container-koopa-decidim-db.service b/configs/decidim/container-koopa-decidim-db.service
new file mode 100644
index 0000000..c15c8c6
--- /dev/null
+++ b/configs/decidim/container-koopa-decidim-db.service
@@ -0,0 +1,21 @@
+# user systemd — Decidim postgres + redis
+[Unit]
+Description=Decidim postgres/redis (koopa-decidim-db)
+Wants=network-online.target
+After=network-online.target
+RequiresMountsFor=%t/containers
+
+[Service]
+Environment=PODMAN_SYSTEMD_UNIT=%n
+Restart=on-failure
+RestartSec=20
+TimeoutStartSec=300
+TimeoutStopSec=120
+WorkingDirectory=/home/hernani/koopa-decidim
+ExecStart=/bin/bash -lc 'set -a && source .env && set +a && /usr/bin/podman-compose up -d postgres redis'
+ExecStop=/usr/bin/podman stop -t 30 koopa-decidim-db koopa-decidim-redis
+Type=oneshot
+RemainAfterExit=yes
+
+[Install]
+WantedBy=default.target
diff --git a/configs/decidim/container-koopa-decidim.service b/configs/decidim/container-koopa-decidim.service
new file mode 100644
index 0000000..c84ec49
--- /dev/null
+++ b/configs/decidim/container-koopa-decidim.service
@@ -0,0 +1,21 @@
+# user systemd — Decidim stack (app + sidekiq)
+[Unit]
+Description=Decidim stack (koopa-decidim)
+Wants=network-online.target container-koopa-decidim-db.service
+After=network-online.target container-koopa-decidim-db.service
+RequiresMountsFor=%t/containers
+
+[Service]
+Environment=PODMAN_SYSTEMD_UNIT=%n
+Restart=on-failure
+RestartSec=20
+TimeoutStartSec=600
+TimeoutStopSec=120
+WorkingDirectory=/home/hernani/koopa-decidim
+ExecStart=/bin/bash -lc 'set -a && source .env && set +a && /usr/bin/podman-compose up -d'
+ExecStop=/usr/bin/podman stop -t 30 koopa-decidim koopa-decidim-sidekiq
+Type=oneshot
+RemainAfterExit=yes
+
+[Install]
+WantedBy=default.target
diff --git a/configs/decidim/entrypoint.sh b/configs/decidim/entrypoint.sh
new file mode 100755
index 0000000..b7af36d
--- /dev/null
+++ b/configs/decidim/entrypoint.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+# Wait for Postgres, migrate, then exec CMD (rails or sidekiq).
+set -euo pipefail
+
+export RAILS_ENV="${RAILS_ENV:-production}"
+
+echo "koopa-decidim: waiting for postgres..."
+i=0
+until bundle exec rails runner 'ActiveRecord::Base.connection.execute("SELECT 1")' >/dev/null 2>&1; do
+ i=$((i + 1))
+ if [ "$i" -ge 60 ]; then
+ echo "koopa-decidim: postgres not ready after ${i} tries" >&2
+ exit 1
+ fi
+ sleep 3
+done
+
+echo "koopa-decidim: db:prepare"
+bundle exec rails db:prepare
+
+echo "koopa-decidim: exec $*"
+exec "$@"
diff --git a/configs/decidim/theme/hacktivism-consent.js b/configs/decidim/theme/hacktivism-consent.js
new file mode 100644
index 0000000..796f5d7
--- /dev/null
+++ b/configs/decidim/theme/hacktivism-consent.js
@@ -0,0 +1,10 @@
+/* Essential-only consent; banner hidden via CSS. */
+(function () {
+ var name = "decidim-consent";
+ if (document.cookie.indexOf(name + "=") !== -1) return;
+ document.cookie =
+ name +
+ "=" +
+ encodeURIComponent(JSON.stringify({ essential: true })) +
+ "; path=/; max-age=31536000; SameSite=Lax";
+})();
diff --git a/configs/decidim/theme/hacktivism.css b/configs/decidim/theme/hacktivism.css
new file mode 100644
index 0000000..f38e96d
--- /dev/null
+++ b/configs/decidim/theme/hacktivism.css
@@ -0,0 +1,98 @@
+/* hacktivism — same palette as bonfire/git/lemmy (exchange-dark) */
+:root,
+html {
+ --primary: #e8a838;
+ --primary-rgb: 232, 168, 56;
+ --secondary: #3ecfbf;
+ --secondary-rgb: 62, 207, 191;
+ --tertiary: #3d3128;
+ --success: #16a34a;
+ --warning: #f0d090;
+ --alert: #e7131a;
+ --highlight: #e8a838;
+}
+
+html,
+body {
+ background-color: #1a1410 !important;
+ background-image:
+ radial-gradient(ellipse 90% 55% at 50% 108%, rgba(26, 107, 110, 0.35) 0%, transparent 55%),
+ radial-gradient(circle 420px at 12% 18%, rgba(232, 168, 56, 0.22) 0%, transparent 62%),
+ linear-gradient(165deg, #2c1e14 0%, #1a1410 38%, #12181a 72%, #0e1c1e 100%) !important;
+ background-attachment: fixed !important;
+ color: #fff6e8 !important;
+}
+
+a {
+ color: #e8a838 !important;
+}
+a:hover {
+ color: #f0d090 !important;
+}
+
+.title-bar,
+.main-bar,
+.navbar,
+.topbar,
+header.header,
+.main-nav,
+.footer,
+.mini-footer,
+.off-canvas,
+.sidebar {
+ background-color: #14110e !important;
+ color: #fff6e8 !important;
+ border-color: #2a2018 !important;
+}
+
+.card,
+.card__content,
+.callout,
+.flash,
+.dialog,
+.reveal,
+.accordion-item,
+.comment-thread,
+.process-header,
+.hero,
+.home-section {
+ background-color: #221c16 !important;
+ color: #fff6e8 !important;
+ border-color: #2a2018 !important;
+}
+
+.button,
+.button.primary,
+.button--sc,
+input[type="submit"].button {
+ background-color: #e8a838 !important;
+ border-color: #e8a838 !important;
+ color: #1a1410 !important;
+}
+
+.button.secondary {
+ background-color: #3d3128 !important;
+ border-color: #3d3128 !important;
+ color: #fff6e8 !important;
+}
+
+h1, h2, h3, h4, h5, h6,
+.title-bar__title,
+.card__title {
+ color: #f0c86a !important;
+}
+
+input,
+textarea,
+select,
+.input-group-field {
+ background-color: #221c16 !important;
+ color: #fff6e8 !important;
+ border-color: #3d3128 !important;
+}
+
+/* No third-party trackers; hide stock GDPR banner (privacy by default). */
+#dc-dialog-wrapper,
+.cookies__container {
+ display: none !important;
+}
diff --git a/configs/decidim/theme/hacktivism.rb b/configs/decidim/theme/hacktivism.rb
new file mode 100644
index 0000000..762e5d8
--- /dev/null
+++ b/configs/decidim/theme/hacktivism.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+# hacktivism theme + host allowlist for decidim.hacktivism.ch
+
+Rails.application.config.hosts << "decidim.hacktivism.ch"
+Rails.application.config.hosts << "127.0.0.1"
+Rails.application.config.hosts << "localhost"
+
+if Rails.application.config.respond_to?(:assume_ssl=)
+ Rails.application.config.assume_ssl = true
+end
+
+Decidim.configure do |config|
+ config.enable_html_header_snippets = true if config.respond_to?(:enable_html_header_snippets=)
+end
diff --git a/configs/ports.md b/configs/ports.md
index f8bdc8d..872d09c 100644
--- a/configs/ports.md
+++ b/configs/ports.md
@@ -18,12 +18,13 @@
| **9024** | podman **`koopa-forgejo`** (HTTP) → Caddy **`git.hacktivism.ch`** |
| **9025** | podman **`koopa-paivana`** (paivana-httpd) → Caddy **`paivana.hacktivism.ch`** |
| **9026** | podman **`koopa-lemmy-proxy`** → Caddy **`lemmy.hacktivism.ch`** |
+| **9027** | podman **`koopa-decidim`** → Caddy **`decidim.hacktivism.ch`** |
| **9200** | podman **Forgejo git-SSH** (host-direct; not Caddy) |
| 9090–9092 | podman `koopa-tops-ng1` … `ng3` → Caddy `tops.ng{1,2,3}.hacktivism.ch` |
| 8080 | Tor ORPort |
VeciGate: WAN **80→9000**, WAN **443→9001**.
-Public apps: Caddy vhosts on **9001** → 127.0.0.1:{9010–9015, 9020–9026, 9090–9092}.
+Public apps: Caddy vhosts on **9001** → 127.0.0.1:{9010–9015, 9020–9027, 9090–9092}.
Git SSH needs separate NAT/firewall **9200/tcp** if exposed to WAN.
| **1789** | podman **`koopa-nym`** mixnet (nym.com nym-node) |
diff --git a/host/overview/LIVE.md b/host/overview/LIVE.md
index 46ee303..11120be 100644
--- a/host/overview/LIVE.md
+++ b/host/overview/LIVE.md
@@ -36,6 +36,7 @@ ss -lntp | grep -E '90[0-9]{2}|9200|8080|9080|1789'
| `koopa-forgejo` (+ postgres) | `forgejo:11-rootless` | **9024**, **9200** | `git.hacktivism.ch` |
| **`koopa-paivana`** (+ upstream) | `localhost/koopa-paivana:latest` | **9025** | `paivana.hacktivism.ch` |
| `koopa-lemmy-proxy` (+ lemmy, ui, pictrs, db) | `lemmy:0.19.20` | **9026** | `lemmy.hacktivism.ch` |
+| `koopa-decidim` (+ postgres, redis, sidekiq) | `decidim/decidim:0.30.8` | **9027** | `decidim.hacktivism.ch` |
| **`koopa-tor-relay`** | `localhost/koopa-tor-relay:latest` | **8080**, **9051** (host net) | Tor OR (non-exit) |
| **`koopa-nym`** | `localhost/koopa-nym:latest` | **9080**, **1789**, **1790** (+ opt. WG) | Nym mixnet node (nym.com) |
| `koopa-tops-ng1` | `nginx` | **9090** | `tops.ng1.hacktivism.ch` |
@@ -61,6 +62,7 @@ Managed from **`/home/hernani/ansible-taler-exchange/`** (see `configs/taler-exc
| `git.hacktivism.ch` | 9024 (HTTP); git-SSH **9200** host-direct |
| `paivana.hacktivism.ch` | 9025 (paivana-httpd GOA paywall) |
| `lemmy.hacktivism.ch` | 9026 |
+| `decidim.hacktivism.ch` | 9027 |
| `tops.ng1.hacktivism.ch` | 9090 |
| `tops.ng2.hacktivism.ch` | 9091 |
| `tops.ng3.hacktivism.ch` | 9092 |
@@ -74,6 +76,7 @@ Config: `/etc/caddy/Caddyfile` (mirror `configs/caddy/Caddyfile`).
| `~/ansible-taler-exchange/` | Ansible + scripts for exchange-ansible container |
| `~/koopa-castopod/` | Castopod compose |
| `~/koopa-lemmy/` | Lemmy compose |
+| `~/koopa-decidim/` | Decidim compose |
| `~/koopa-bonfire/` | Bonfire compose + gitbot |
| `~/koopa-prime/` | Jellyfin + qBittorrent |
| `~/koopa-forgejo/` | Forgejo rootless |
@@ -90,6 +93,8 @@ Config: `/etc/caddy/Caddyfile` (mirror `configs/caddy/Caddyfile`).
| Taler merchant/bank | root `start_base_services_*` → service user → `/usr/local/bin/start_*.sh` |
| Exchange (GOA) | `~/ansible-taler-exchange/run-container-koopa.sh` then `deploy-hacktivism-goa.sh` |
| User apps | `cd ~/koopa-* && podman-compose up -d` |
+| Lemmy | user units `container-koopa-lemmy-db` + `container-koopa-lemmy` (linger) |
+| Decidim | user units `container-koopa-decidim-db` + `container-koopa-decidim` (linger) |
| tops (ng1–ng3) | user unit `container-koopa-tops.service` (linger) |
## Config mirrors in this repo (`configs/`)
@@ -111,3 +116,4 @@ Config: `/etc/caddy/Caddyfile` (mirror `configs/caddy/Caddyfile`).
| Forgejo | `2026/2026-07-10--forgejo-rootless.md` |
| Bonfire public feeds | `configs/bonfire/public-feeds.md` |
| Castopod content | `2026/2026-07-09--castopod-content.md` |
+| Decidim | `2026/2026-08-17--decidim.md` · `configs/decidim/` |
diff --git a/host/overview/services.md b/host/overview/services.md
index 0c9daf2..6c72548 100644
--- a/host/overview/services.md
+++ b/host/overview/services.md
@@ -37,6 +37,7 @@ flowchart TB
Forgejo["koopa-forgejo ROOTLESS\n:9024 HTTP / :9200 SSH"]
Paivana["koopa-paivana\n:9025"]
Lemmy["koopa-lemmy-proxy\n:9026"]
+ Decidim["koopa-decidim\n:9027"]
end
Tor["tor ORPort :8080"]
@@ -63,6 +64,7 @@ flowchart TB
CaddyHTTPS -->|git.hacktivism.ch| Forgejo
CaddyHTTPS -->|paivana.hacktivism.ch| Paivana
CaddyHTTPS -->|lemmy.hacktivism.ch| Lemmy
+ CaddyHTTPS -->|decidim.hacktivism.ch| Decidim
```
## Port table
@@ -84,6 +86,7 @@ flowchart TB
| **9024** | podman | **forgejo** HTTP → `git.hacktivism.ch` |
| **9025** | podman | **paivana** → `paivana.hacktivism.ch` |
| **9026** | podman | **lemmy** → `lemmy.hacktivism.ch` |
+| **9027** | podman | **decidim** → `decidim.hacktivism.ch` |
| **9200** | podman | **forgejo** git-SSH (host-direct) |
| 9090–9092 | podman | **tops** `koopa-tops-ng1`…`ng3` |
| 8080 | tor | ORPort |
diff --git a/scripts/decidim/apply-branding.sh b/scripts/decidim/apply-branding.sh
new file mode 100755
index 0000000..6327fe3
--- /dev/null
+++ b/scripts/decidim/apply-branding.sh
@@ -0,0 +1,22 @@
+#!/usr/bin/env bash
+# Apply hacktivism colors + header CSS on the live organization.
+set -euo pipefail
+cd "${HOME}/koopa-decidim"
+set -a
+# shellcheck disable=SC1091
+source .env
+# shellcheck disable=SC1091
+source users.env
+set +a
+podman exec \
+ -e DECIDIM_HOST="${DECIDIM_HOST:-decidim.hacktivism.ch}" \
+ -e DECIDIM_SYSTEM_EMAIL \
+ -e DECIDIM_SYSTEM_PASSWORD \
+ -e DECIDIM_ADMIN_EMAIL \
+ -e DECIDIM_ADMIN_PASSWORD \
+ -e DECIDIM_ADMIN_NAME \
+ -e DECIDIM_APPLICATION_NAME \
+ -e DECIDIM_DEFAULT_LOCALE \
+ -e DECIDIM_AVAILABLE_LOCALES \
+ koopa-decidim bundle exec rails runner /code/lib/koopa_bootstrap.rb
+echo "branding applied. public: https://decidim.hacktivism.ch/"
diff --git a/scripts/decidim/install-systemd.sh b/scripts/decidim/install-systemd.sh
new file mode 100755
index 0000000..01934d3
--- /dev/null
+++ b/scripts/decidim/install-systemd.sh
@@ -0,0 +1,47 @@
+#!/usr/bin/env bash
+# Install Decidim user systemd units on koopa (boot via linger).
+set -euo pipefail
+HERE="$(cd "$(dirname "$0")" && pwd)"
+LIVE="${HOME}/koopa-decidim"
+# admin-log tree (scripts/decidim → ../..) or live ~/koopa-decidim/bin
+if [ -f "${HERE}/../../configs/decidim/container-koopa-decidim.service" ]; then
+ UNIT_SRC="${HERE}/../../configs/decidim"
+elif [ -f "${LIVE}/container-koopa-decidim.service" ]; then
+ UNIT_SRC="${LIVE}"
+else
+ echo "ERROR: decidim unit files not found" >&2
+ exit 1
+fi
+UNIT_DIR="${HOME}/.config/systemd/user"
+
+mkdir -p "${UNIT_DIR}"
+cp "${UNIT_SRC}/container-koopa-decidim-db.service" "${UNIT_DIR}/"
+cp "${UNIT_SRC}/container-koopa-decidim.service" "${UNIT_DIR}/"
+
+systemctl --user daemon-reload
+systemctl --user enable \
+ container-koopa-decidim-db.service \
+ container-koopa-decidim.service
+
+if ! podman ps --format '{{.Names}}' | grep -qx koopa-decidim; then
+ (cd "${HOME}/koopa-decidim" && set -a && source .env && set +a && podman-compose up -d)
+fi
+
+systemctl --user start \
+ container-koopa-decidim-db.service \
+ container-koopa-decidim.service
+
+systemctl --user --no-pager --lines=0 status \
+ container-koopa-decidim-db.service \
+ container-koopa-decidim.service || true
+
+if command -v loginctl >/dev/null 2>&1; then
+ if [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)" != "yes" ]; then
+ echo "NOTE: enable linger so units start after reboot without login:"
+ echo " sudo loginctl enable-linger $(id -un)"
+ else
+ echo "linger already enabled for $(id -un)"
+ fi
+fi
+
+echo "OK. Public: https://decidim.hacktivism.ch/"
diff --git a/scripts/decidim/up.sh b/scripts/decidim/up.sh
new file mode 100755
index 0000000..21a6a9d
--- /dev/null
+++ b/scripts/decidim/up.sh
@@ -0,0 +1,21 @@
+#!/usr/bin/env bash
+set -euo pipefail
+DIR="${HOME}/koopa-decidim"
+cd "${DIR}"
+set -a
+# shellcheck disable=SC1091
+source .env
+set +a
+podman-compose up -d
+echo "== wait for :9027 =="
+ok=0
+for i in $(seq 1 80); do
+ code=$(curl -sS -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:9027/" || true)
+ echo "try $i: $code"
+ case "$code" in
+ 2*|3*) ok=1; break ;;
+ esac
+ sleep 5
+done
+[[ "$ok" -eq 1 ]] || { echo "ERROR: no answer on 9027"; podman-compose ps; exit 1; }
+echo "local ok. public: https://decidim.hacktivism.ch/"
diff --git a/scripts/lemmy/install-systemd.sh b/scripts/lemmy/install-systemd.sh
index 626dc3b..f064f9f 100755
--- a/scripts/lemmy/install-systemd.sh
+++ b/scripts/lemmy/install-systemd.sh
@@ -1,12 +1,21 @@
#!/usr/bin/env bash
# Install Lemmy user systemd units on koopa (boot via linger).
set -euo pipefail
-ROOT="$(cd "$(dirname "$0")/../.." && pwd)"
+HERE="$(cd "$(dirname "$0")" && pwd)"
+LIVE="${HOME}/koopa-lemmy"
+if [ -f "${HERE}/../../configs/lemmy/container-koopa-lemmy.service" ]; then
+ UNIT_SRC="${HERE}/../../configs/lemmy"
+elif [ -f "${LIVE}/container-koopa-lemmy.service" ]; then
+ UNIT_SRC="${LIVE}"
+else
+ echo "ERROR: lemmy unit files not found" >&2
+ exit 1
+fi
UNIT_DIR="${HOME}/.config/systemd/user"
mkdir -p "${UNIT_DIR}"
-cp "${ROOT}/configs/lemmy/container-koopa-lemmy-db.service" "${UNIT_DIR}/"
-cp "${ROOT}/configs/lemmy/container-koopa-lemmy.service" "${UNIT_DIR}/"
+cp "${UNIT_SRC}/container-koopa-lemmy-db.service" "${UNIT_DIR}/"
+cp "${UNIT_SRC}/container-koopa-lemmy.service" "${UNIT_DIR}/"
systemctl --user daemon-reload
systemctl --user enable \
@@ -25,4 +34,13 @@ systemctl --user --no-pager --lines=0 status \
container-koopa-lemmy-db.service \
container-koopa-lemmy.service || true
+if command -v loginctl >/dev/null 2>&1; then
+ if [ "$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)" != "yes" ]; then
+ echo "NOTE: enable linger so units start after reboot without login:"
+ echo " sudo loginctl enable-linger $(id -un)"
+ else
+ echo "linger already enabled for $(id -un)"
+ fi
+fi
+
echo "OK. Public: https://lemmy.hacktivism.ch/"
diff --git a/scripts/login-health/README.md b/scripts/login-health/README.md
index 04b1a6b..0003217 100644
--- a/scripts/login-health/README.md
+++ b/scripts/login-health/README.md
@@ -65,7 +65,9 @@ overall: OK ok=42 warn=0 fail=0
| Git SSH | TCP `:9200` |
**Critical** (→ overall FAIL): merchant, exchange, bank, forgejo, edge/critical ports.
-**Optional** (→ WARN): castopod, bonfire, lemmy, prime, bt, paivana, tops, tor, nym.
+**Optional** (→ WARN): castopod, bonfire, lemmy, decidim, prime, bt, paivana, tops, tor, nym.
+
+Collect also checks **linger** and that Lemmy/Decidim user units are **enabled** (reboot survival).
Read-only. No restarts, no secrets.
diff --git a/scripts/login-health/koopa-login-health.sh b/scripts/login-health/koopa-login-health.sh
index aa9c2db..309a782 100755
--- a/scripts/login-health/koopa-login-health.sh
+++ b/scripts/login-health/koopa-login-health.sh
@@ -151,6 +151,7 @@ EXPECT_CONTAINERS=(
koopa-castopod
koopa-bonfire
koopa-lemmy-proxy
+ koopa-decidim
koopa-prime-jellyfin
koopa-prime-qbittorrent
koopa-forgejo
@@ -173,6 +174,7 @@ OPTIONAL_CONTAINERS=(
koopa-castopod
koopa-bonfire
koopa-lemmy-proxy
+ koopa-decidim
koopa-paivana
)
@@ -185,6 +187,7 @@ PROBES=(
"castopod|http://127.0.0.1:9020/|200,302,303,307,308"
"bonfire|http://127.0.0.1:9021/|200,302,303,307,308"
"lemmy|http://127.0.0.1:9026/|200,302,303,307,308"
+ "decidim|http://127.0.0.1:9027/|200,301,302,303,307,308"
"prime|http://127.0.0.1:9022/|200,302,303,307,308"
"bt|http://127.0.0.1:9023/|200,302,303,307,308,401"
"paivana|http://127.0.0.1:9025/|200,302,303,307,308"
@@ -193,7 +196,7 @@ PROBES=(
"tops-ng3|http://127.0.0.1:9092/|200,302,303"
)
-OPTIONAL_PROBES=(castopod bonfire lemmy prime bt paivana tops-ng1 tops-ng2 tops-ng3)
+OPTIONAL_PROBES=(castopod bonfire lemmy decidim prime bt paivana tops-ng1 tops-ng2 tops-ng3)
C_OK=; C_WARN=; C_FAIL=; C_DIM=; C_RST=; C_BOLD=
# colors only when writing for a tty later; snapshots are plain + optional ANSI if COLLECT_COLOR=1
@@ -277,6 +280,34 @@ do_collect() {
record WARN "systemctl not available"
fi
+ # Reboot survival: linger + enabled user units (public stacks).
+ if command -v loginctl >/dev/null 2>&1; then
+ linger_st=$(loginctl show-user "$(id -un)" -p Linger --value 2>/dev/null || echo no)
+ if [ "${linger_st}" = "yes" ]; then
+ record OK "linger=yes ($(id -un))"
+ else
+ record FAIL "linger=${linger_st} (user units will not start after reboot without login)"
+ fi
+ else
+ record WARN "loginctl not available (cannot check linger)"
+ fi
+ if command -v systemctl >/dev/null 2>&1 && [ "$(id -u 2>/dev/null)" != "0" ]; then
+ local u en
+ for u in \
+ container-koopa-lemmy-db.service \
+ container-koopa-lemmy.service \
+ container-koopa-decidim-db.service \
+ container-koopa-decidim.service
+ do
+ en=$(systemctl --user is-enabled "${u}" 2>/dev/null || echo unknown)
+ if [ "${en}" = "enabled" ]; then
+ record OK "unit ${u} enabled"
+ else
+ record WARN "unit ${u} is ${en} (will not start on reboot)"
+ fi
+ done
+ fi
+
if command -v ss >/dev/null 2>&1; then
listen_blob=$(ss -lnt 2>/dev/null | awk '{print $4}' || true)
for port in 9000 9001 9010 9011 9012 9024 9200; do
@@ -286,7 +317,7 @@ do_collect() {
record FAIL "nothing listening on :${port}"
fi
done
- for port in 9020 9021 9022 9023 9025 9026 9090 9091 9092 8080 9080; do
+ for port in 9020 9021 9022 9023 9025 9026 9027 9090 9091 9092 8080 9080; do
if printf '%s\n' "${listen_blob}" | grep -qE "[:.]${port}\$"; then
record OK "listen :${port} (optional)"
else
@@ -323,7 +354,7 @@ do_collect() {
fi
fi
done
- for dep in koopa-forgejo-db koopa-castopod-mariadb koopa-castopod-redis koopa-bonfire-db koopa-lemmy-db; do
+ for dep in koopa-forgejo-db koopa-castopod-mariadb koopa-castopod-redis koopa-bonfire-db koopa-lemmy-db koopa-decidim-db koopa-decidim-redis; do
if printf '%s\n' "${running_names}" | grep -qxF "${dep}"; then
record INFO "dep ${dep} running"
else