diff --git a/.github/workflows/build_install_folder.yml b/.github/workflows/build_install_folder.yml new file mode 100644 index 00000000..d9c56e7e --- /dev/null +++ b/.github/workflows/build_install_folder.yml @@ -0,0 +1,24 @@ +name: Build Deployment Zip + +on: + push: + branches: + - main + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Create deployment ZIP + run: | + zip -r deploy_bundle.zip \ + scripts/ \ + + - name: Release + uses: softprops/action-gh-release@v2 + with: + files: deploy_bundle.zip diff --git a/README.md b/README.md index 4b0e703a..8159ba3a 100644 --- a/README.md +++ b/README.md @@ -101,3 +101,69 @@ sudo chown -R $(whoami): ${APP_NAME} ``` From here on you can follow the steps on the [Getting Started](https://docs.decidim.org/en/install/) guide. + +## Using a production deploy script + +We've been working on a script that you can use to have a fully functional, production ready decidim instance. + +``` +curl -fsSL https://decidim.org/install | bash +``` + +It will install the necessary tools to make decidim work on your server. + +- Docker +- unzip +- UFW + +The application will be hosted in the `/opt/decidim` directory by default, even though you can change it with `REPOSITOIRY_PATH` + +## App - Main Decidim Web Application +The app itself will be the container with the base image you decide (By default is the latest Decidim version: `decidim/decidim:latest`). You can change it with the `DECIDIM_IMAGE` environment variable. + +This is the front-end web process users access in the browser. + +## Worker +The worker will be the one responsible for all the background jobs that the application needs to run. + +## Cache +The app needs a cache server. This will be a `redis:8-alpine` instance. This cache will be used both by the app and the worker. + +## Database + +The application needs a database to run. Through the installation process you will be asked if you have an already working database, if not, you will have a postgres container with all the schema and migrations run (It will be a `postgres:17-alpine`) + +## Configuration + +To configure the application you will have to answer some questions that will, at the end, generate a `.env` file. + +### Environment Variables Reference + +To see the full list of Decidim Environment Variables, and that you can add to your generated `.env` file, you can take a look at the oficial [documentation](https://docs.decidim.org/en/develop/configure/environment_variables) + +| Variable | Default | Used In | Description | +|----------|---------|---------|-------------| +| **BUNDLE_GEMFILE** | `Gemfile.wrapper` | app, worker | Selects which Gemfile the container should use. | +| **DECIDIM_IMAGE** | `decidim/decidim:latest` | app, worker | Overrides the Decidim Docker image version. | +| **DECIDIM_DOMAIN** | — | app, traefik | Domain for HTTPS routing and URL generation. | +| **SECRET_KEY_BASE** | — | app, worker | Rails secret key used for sessions and cookies. | +| **DATABASE_NAME** | `decidim` | db | PostgreSQL database name. | +| **DATABASE_USER** | `decidim` | db | PostgreSQL username. | +| **DATABASE_HOST** | `db` | app, worker | Hostname of your PostgreSQL instance. | +| **DATABASE_PASSWORD** | `decidim` | db | PostgreSQL user password. | +| **DATABASE_URL** | — | app, worker | Full PostgreSQL connection URL (overrides other DB vars). | +| **SMTP_USERNAME** | — | app, worker | Username for SMTP authentication. | +| **SMTP_PASSWORD** | — | app, worker | Password for SMTP authentication. | +| **SMTP_ADDRESS** | — | app, worker | SMTP server hostname. | +| **SMTP_DOMAIN** | — | app, worker | SMTP domain. | +| **SMTP_PORT** | — | app, worker | SMTP port. | +| **SMTP_STARTTLS_AUTO** | `true` | app | Enables STARTTLS automatically. | +| **REDIS_URL** | `redis://decidim_cache:6379/0` | app | Redis URL for cache + sessions. | +| **VAPID_PUBLIC_KEY** | — | app | Web Push public key for browser notifications. | +| **VAPID_PRIVATE_KEY** | — | app | Web Push private key (keep secret). | +| **CERTIFICATE_EMAIL** | — | traefik | Email used by Let's Encrypt for certificate issues/renewals. | +| **WEB_CONCURRENCY** | `2` | app | Puma concurrency setting. | +| **LOG_LEVEL** | `info` | app | Log level for Rails. | +| **DECIDIM_FORCE_SSL** | `false` | app | Enforce HTTPS-only traffic. | +| **MAPS_API_KEY** | — | app | API key for maps provider. | +| **MAPS_PROVIDER** | `here` | app | Selects map provider (here, mapbox, google, etc). | diff --git a/install/config/sidekiq.yml b/install/config/sidekiq.yml new file mode 100644 index 00000000..c9e659c4 --- /dev/null +++ b/install/config/sidekiq.yml @@ -0,0 +1,16 @@ +:concurrency: <%= ENV.fetch("SIDEKIQ_CONCURRENCY", 5) %> +:queues: + - [mailers, 4] + - [vote_reminder, 2] + - [reminders, 2] + - [default, 2] + - [newsletter, 2] + - [newsletters_opt_in, 2] + - [conference_diplomas, 2] + - [events, 2] + - [translations, 2] + - [user_report, 2] + - [block_user, 2] + - [metrics, 1] + - [exports, 1] + - [close_meeting_reminder, 1] diff --git a/install/dependencies/build_env.sh b/install/dependencies/build_env.sh new file mode 100644 index 00000000..c10260a1 --- /dev/null +++ b/install/dependencies/build_env.sh @@ -0,0 +1,273 @@ +#!/bin/bash +set -e +set -u +set -o pipefail + +BUILD_ENV_PATH="$REPOSITORY_PATH/.env" + +if [ -z "${REPOSITORY_PATH:-}" ]; then + echo "❌ Error: REPOSITORY_PATH is not set" + exit 1 +fi + +echo "───────────────────────────────────────────────" +echo "🔧 Environment Configuration Phase" +echo " We'll now collect all the information needed to configure your Decidim instance." +echo " All responses will be saved in a .env file that you can edit later." +echo +echo "📝 Information we'll collect:" +echo " • Instance details (name, domain)" +echo " • Database settings (local PostgreSQL or external)" +echo " • Email configuration (SMTP server)" +echo " • File storage (local filesystem or S3 bucket)" +echo " • Security keys (auto-generated)" +echo +echo "💡 Don't worry if you don't have all the details ready!" +echo " You can always modify the .env file after installation." +echo +echo "Press Enter to continue..." +read -r "$BUILD_ENV_PATH" <>"$BUILD_ENV_PATH" </dev/null 2>&1; then + echo "🐳 Docker not found. Installing Docker..." + echo "⚠️ After installation, you'll need to re-run this script for changes to take effect." + echo "💡 You can run 'newgrp docker' to activate Docker group membership." + + if ! curl -fsSL https://get.docker.com | bash; then + echo "❌ Failed to install Docker" + exit 1 + fi + + if ! sudo usermod -aG docker "${USER}"; then + echo "❌ Failed to add user to Docker group" + exit 1 + fi + + echo "" + echo "🔄 Docker installation completed!" + echo "📋 Next steps:" + echo " 1. Log out and log back in, OR run: 'newgrp docker'" + echo " 2. Re-run this installation script" + echo "" + echo "⏹️ Exiting for user session refresh..." + exit 1 +else + echo "✅ Docker is installed: $(docker --version)" + + # Check if user can run docker commands + if ! docker info >/dev/null 2>&1; then + echo "⚠️ Docker is installed but current user cannot run Docker commands." + echo "💡 Try running: 'newgrp docker' or log out and log back in." + echo " If that doesn't work, you may need to: 'sudo usermod -aG docker \$USER'" + exit 1 + fi + + echo "✅ Docker is accessible for current user" +fi diff --git a/install/dependencies/create_system_admin.sh b/install/dependencies/create_system_admin.sh new file mode 100644 index 00000000..4fedd9cd --- /dev/null +++ b/install/dependencies/create_system_admin.sh @@ -0,0 +1,48 @@ +#!/bin/bash +set -e +set -u +set -o pipefail + +generate_system_admin() { + # docker exec -ti \ + # decidim \ + # bin/rails decidim_system:create_admin /dev/null; do + echo "Waiting for Rails server to start..." + sleep 10 +done + +echo "Container is running correctly... Now we are going to create the system admin." + +generate_system_admin + +if [ $? -eq 1 ]; then + echo "❌ Seems like there was a problem creating the system admin." + echo + echo "🔧 Troubleshooting:" + echo " • Try running the command manually:" + echo " docker exec -ti decidim bin/rails decidim_system:create_admin" + echo " • Review the logs for any errors:" + echo " docker compose logs decidim" +else + echo "✅ System administrator created successfully!" + echo "Your password to access is: ${SYSTEM_PASSSWORD}" + echo "📍 You can now access the admin panel at: https://${DECIDIM_DOMAIN}/system" +fi diff --git a/install/dependencies/decidim_version.sh b/install/dependencies/decidim_version.sh new file mode 100644 index 00000000..2908b405 --- /dev/null +++ b/install/dependencies/decidim_version.sh @@ -0,0 +1,30 @@ +#!/bin/bash + +echo "───────────────────────────────────────────────" +echo "📦 Decidim version" +echo +echo "💡 About Decidim versions:" +echo " • latest - Most recent stable release (recommended)" +echo " • Custom images - Your own modified Decidim builds" +echo +echo "If you want, later on, you can modify the 'docker-compose.yml' to change the Decidim version" +echo +echo "Default image: decidim/decidim:latest" +echo + +while true; do + read -r "👉 Click Enter to continue with the download of the Decidim image" Gemfile.wrapper <Gemfile.local <>Gemfile.local +fi diff --git a/install/dependencies/generate_vapid_keys.sh b/install/dependencies/generate_vapid_keys.sh new file mode 100644 index 00000000..4dc68a46 --- /dev/null +++ b/install/dependencies/generate_vapid_keys.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +set -e +set -u +set -o pipefail + +echo "🔐 Generating VAPID keys..." + +# Check if DECIDIM_IMAGE is set +if [ -z "${DECIDIM_IMAGE:-}" ]; then + echo "❌ Error: DECIDIM_IMAGE is not set" + exit 1 +fi + +output=$(docker run --rm \ + "$DECIDIM_IMAGE" \ + bin/rails decidim:pwa:generate_vapid_keys) + +echo "✅ The VAPID keys have been generated correctly" + +VAPID_PUBLIC_KEY=$(echo "$output" | grep 'VAPID_PUBLIC_KEY' | cut -d'=' -f2) +VAPID_PRIVATE_KEY=$(echo "$output" | grep 'VAPID_PRIVATE_KEY' | cut -d'=' -f2) + +# Export the keys for use by calling script +export VAPID_PUBLIC_KEY +export VAPID_PRIVATE_KEY + +echo "🔑 Keys successfully extracted" diff --git a/install/dependencies/open_ports.sh b/install/dependencies/open_ports.sh new file mode 100644 index 00000000..fce59011 --- /dev/null +++ b/install/dependencies/open_ports.sh @@ -0,0 +1,35 @@ +#!/bin/bash + +open_ports() { + echo + echo "To handle the SSL certificate we will have to open the port 80 and the port 443" + echo + + if ! command -v ufw; then + echo "UFW nos intalled. We are going to install it to allow openning ports 80 and 443 on this machine." + sudo apt install ufw + fi + + sudo ufw allow 22 + sudo ufw allow 80 + sudo ufw allow 443 + sudo ufw --force enable +} + +echo "───────────────────────────────────────────────" +echo "Now we are going to open the necessary ports for Decidim to work ussing UFW." +echo "This is a standard practice to protect your server." +echo +read -r -p "Can we proceed openning ports 22, 80 and 443? [Y/n] " yn /dev/null | grep -Eq "Ubuntu|Debian"; then + echo "This installation process must be run on a Debian/Ubuntu distribution." + exit 1 +fi + +echo "Correct distribution." diff --git a/install/docker-compose.yml b/install/docker-compose.yml new file mode 100644 index 00000000..905ded49 --- /dev/null +++ b/install/docker-compose.yml @@ -0,0 +1,129 @@ +services: + app: + container_name: decidim + image: ${DECIDIM_IMAGE:-"decidim/decidim:latest"} + command: ["bin/rails", "server", "-b", "0.0.0.0"] + entrypoint: ["/code/entrypoint.sh"] + restart: always + depends_on: + - cache + expose: + - 3000 + volumes: + - ${PWD}/Gemfile.wrapper:/code/Gemfile.wrapper + - ${PWD}/Gemfile.local:/code/Gemfile.local + - ${PWD}/scripts/entrypoint.sh:/code/entrypoint.sh + - app_gems:/usr/local/bundle + - storage_data:/code/storage + - migrations_data:/code/db/migrate + labels: + - "traefik.enable=true" + - "traefik.http.routers.app.rule=Host(`$DECIDIM_DOMAIN`)" + - "traefik.http.routers.app.entrypoints=websecure" + - "traefik.http.routers.app.tls=true" + - "traefik.http.routers.app.tls.certresolver=myresolver" + - "traefik.http.services.app.loadbalancer.server.port=3000" + env_file: + - .env + environment: + - BUNDLE_GEMFILE=Gemfile.wrapper + - DATABASE_URL + - SECRET_KEY_BASE + - DECIDIM_FORCE_SSL=${DECIDIM_FORCE_SSL:-false} + - REDIS_URL=${REDIS_URL:-redis://decidim_cache:6379/0} + - WEB_CONCURRENCY=${WEB_CONCURRENCY:-2} + - LOG_LEVEL=${LOG_LEVEL:-info} + - DECIDIM_ENABLE_HTML_HEADER_SNIPPETS + - SMTP_STARTTLS_AUTO=${SMTP_STARTTLS_AUTO:-true} + - SMTP_USERNAME + - SMTP_PASSWORD + - SMTP_ADDRESS + - SMTP_DOMAIN + - SMTP_PORT + - DECIDIM_MAILER_SENDER + - MAPS_API_KEY + - MAPS_PROVIDER=${MAPS_PROVIDER:-here} + worker: + image: ${DECIDIM_IMAGE:-decidim/decidim:latest} + container_name: decidim_worker + command: ["bundle", "exec", "sidekiq", "-C", "config/sidekiq.yml"] + entrypoint: ["/code/sidekiq_entrypoint.sh"] + pull_policy: always + restart: always + env_file: + - .env + volumes: + - ${PWD}/Gemfile.wrapper:/code/Gemfile.wrapper + - ${PWD}/Gemfile.local:/code/Gemfile.local + - ${PWD}/scripts/sidekiq_entrypoint.sh:/code/sidekiq_entrypoint.sh + - ${PWD}/scripts/config/sidekiq.yml:/code/config/sidekiq.yml + - worker_gems:/usr/local/bundle + - storage_data:/code/storage + - migrations_data:/code/db/migrate + environment: + - BUNDLE_GEMFILE + - DATABASE_URL + - SECRET_KEY_BASE + - DECIDIM_FORCE_SSL=false + - QUEUE_ADAPTER=sidekiq + - REDIS_URL=${REDIS_URL:-redis://decidim_cache:6379/0} + - SMTP_USERNAME + - SMTP_PASSWORD + - SMTP_ADDRESS + - SMTP_DOMAIN + - SMTP_PORT + - DECIDIM_MAILER_SENDER + links: + - cache + traefik: + image: traefik:v3.6 + container_name: traefik + command: + - --api + - --providers.docker=true + - --log.level=DEBUG + - --entrypoints.web.address=:80 + - --entrypoints.websecure.address=:443 + - --entrypoints.traefik.address=:8080 + - --entrypoints.web.http.redirections.entryPoint.to=websecure + - --entrypoints.web.http.redirections.entryPoint.scheme=https + - --certificatesresolvers.myresolver.acme.email=${CERTIFICATE_EMAIL} + - --certificatesresolvers.myresolver.acme.storage=/letsencrypt/acme.json + - --certificatesresolvers.myresolver.acme.httpchallenge=true + - --certificatesresolvers.myresolver.acme.httpchallenge.entrypoint=web + ports: + - "80:80" + - "443:443" + restart: unless-stopped + volumes: + - traefik_data:/letsencrypt + - "/var/run/docker.sock:/var/run/docker.sock:ro" + labels: + - "traefik.enable=true" + - "traefik.http.routers.traefik.service=api@internal" + db: + image: postgres:17-alpine + container_name: decidim_db + profiles: ["db"] + env_file: + - .env + environment: + POSTGRES_USER: ${DATABASE_USER:-decidim} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-decidim} + POSTGRES_DB: ${DATABASE_NAME:-decidim} + volumes: + - pg_data:/var/lib/postgresql/data + cache: + image: redis:8-alpine + container_name: decidim_cache + volumes: + - cache_data:/data + restart: always +volumes: + app_gems: {} + worker_gems: {} + pg_data: {} + cache_data: {} + storage_data: {} + migrations_data: {} + traefik_data: {} diff --git a/install/install.sh b/install/install.sh new file mode 100644 index 00000000..34e85ba1 --- /dev/null +++ b/install/install.sh @@ -0,0 +1,152 @@ +#!/bin/bash +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +# + +set -e +set -u +set -o pipefail + +echo -e "***********************************************************************" +echo -e "* 🚀 Welcome to Decidim Installation Script! *" +echo -e "* *" +echo -e "* This script will install Decidim on this machine and guide you *" +echo -e "* through the complete configuration process. *" +echo -e "* *" +echo -e "* You'll need to provide: *" +echo -e "* • Instance name and domain *" +echo -e "* • Database configuration (local or external) *" +echo -e "* • SMTP server settings for emails *" +echo -e "* • File storage settings (local or S3) *" +echo -e "* *" +echo -e "* 💡 All dependencies (Docker, etc.) will be installed for you *" +echo -e "* *" +echo -e "* ⚠️ For production use, review security settings and documentation. *" +echo -e "* *" +echo -e "***********************************************************************" + +REPOSITORY_PATH=${DECIDIM_PATH:-/opt/decidim} +REPOSITORY_URL="https://github.com/decidim/docker" +REPOSITORY_BRANCH="feat/decidim_install" + +export REPOSITORY_URL +export REPOSITORY_BRANCH + +echo "📁 Installation directory: $REPOSITORY_PATH" + +trap 'echo "❌ Error occurred at line $LINENO. You can re-run this script to restart the installation."' ERR + +if [ ! -d "$REPOSITORY_PATH" ]; then + echo "📁 Creating installation directory: $REPOSITORY_PATH" + if ! sudo mkdir -p "$REPOSITORY_PATH"; then + echo "❌ Failed to create directory $REPOSITORY_PATH" + exit 1 + fi + if ! sudo chown "$USER":"$USER" "$REPOSITORY_PATH"; then + echo "❌ Failed to set ownership of $REPOSITORY_PATH" + exit 1 + fi +fi + +TMP="/tmp/decidim-docker-files" +if [ ! -d "$TMP" ]; then + mkdir "$TMP" +fi + +echo "📥 Downloading the installation necessary files." +curl -fsSL \ + --retry 3 \ + --retry-delay 2 \ + --connect-timeout 30 \ + --max-time 300 \ + --progress-bar \ + -o "$TMP/deploy.zip" \ + "$REPOSITORY_URL/releases/download/latest/deploy.zip" + +echo "📦 Installing unzip package..." +if ! (sudo apt update && sudo apt install unzip -y); then + echo "❌ Failed to install unzip package" + exit 1 +fi + +echo "📂 Extracting files to $REPOSITORY_PATH..." +if ! unzip -u -o "$TMP/deploy.zip" -d "$REPOSITORY_PATH" /dev/null +groupmod -g "$USER_GID" decidim 2>/dev/null +usermod -g "$USER_GID" decidim 2>/dev/null + +chown -R -h "$USER_UID" "$BUNDLE_PATH" +chgrp -R -h "$USER_GID" "$BUNDLE_PATH" + +# Check all the gems are installed or fails. +if ! bundle check; then + echo "❌ Gems in Gemfile are not installed. Installing them with \"bundle install\"..." + bundle install +else + echo "✅ Gems in Gemfile are installed" +fi + +# Check to see if there are migrations to install +bundle exec rake railties:install:migrations + +# Check no migrations are pending migrations +if [ -z "$SKIP_MIGRATIONS" ]; then + bundle exec rails db:migrate +else + echo "⚠️ Skipping migrations" +fi + +echo "✅ Migrations are all up" + +echo "🚀" "$@" +exec "$@" diff --git a/scripts/hello-world.sh b/install/scripts/hello-world.sh similarity index 100% rename from scripts/hello-world.sh rename to install/scripts/hello-world.sh diff --git a/install/scripts/sidekiq_entrypoint.sh b/install/scripts/sidekiq_entrypoint.sh new file mode 100755 index 00000000..6068aae3 --- /dev/null +++ b/install/scripts/sidekiq_entrypoint.sh @@ -0,0 +1,12 @@ +#!/bin/sh -x + +# Check all the gems are installed or fails. +if ! bundle check; then + echo "❌ Gems in Gemfile are not installed. Installing them with \"bundle install\"..." + bundle install +else + echo "✅ Gems in Gemfile are installed" +fi + +echo "🚀" "$@" +exec "$@" diff --git a/install/up.sh b/install/up.sh new file mode 100644 index 00000000..892685b4 --- /dev/null +++ b/install/up.sh @@ -0,0 +1,23 @@ +#!/bin/bash +set -e +set -u +set -o pipefail + +ENV_FILE="${REPOSITORY_PATH}/.env" + +# Check if .env file exists +if [ ! -f "$ENV_FILE" ]; then + echo "❌ Error: .env file not found at $ENV_FILE" + echo " Please run the installation script first or create the .env file manually." + exit 1 +fi + +echo "🚀 Starting Decidim containers..." + +docker compose --env-file "$ENV_FILE" up -d + +echo "📋 Showing recent container logs..." +docker compose logs --tail=30 + +echo "✅ Containers started successfully!" +echo "🔍 You can monitor logs with: docker compose logs -f" diff --git a/scripts/entrypoint.sh b/scripts/entrypoint.sh deleted file mode 100644 index c48d1f87..00000000 --- a/scripts/entrypoint.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh -x - -USER_UID=$(stat -c %u /code/Gemfile) -USER_GID=$(stat -c %g /code/Gemfile) - -export USER_UID -export USER_GID - -usermod -u "$USER_UID" decidim 2> /dev/null -groupmod -g "$USER_GID" decidim 2> /dev/null -usermod -g "$USER_GID" decidim 2> /dev/null - -chown -R -h "$USER_UID" "$BUNDLE_PATH" -chgrp -R -h "$USER_GID" "$BUNDLE_PATH" - -/usr/bin/sudo -EH -u decidim "$@"