├── images ├── drupal-pi-model-2.jpg └── drupal-pi-model-3.jpg ├── .gitignore ├── .github ├── FUNDING.yml └── workflows │ └── CI.yml ├── requirements.yml ├── .yamllint ├── ansible.cfg ├── example.inventory ├── tests ├── .vagrant │ └── rgloader │ │ └── loader.rb ├── README.md └── Vagrantfile ├── templates ├── proxy.conf.j2 ├── drupal.conf.j2 └── docker-compose.yml.j2 ├── reset.yml ├── tasks ├── docker-compose-setup.yml └── init.yml ├── LICENSE ├── main.yml ├── default.config.yml └── README.md /images/drupal-pi-model-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geerlingguy/drupal-pi/HEAD/images/drupal-pi-model-2.jpg -------------------------------------------------------------------------------- /images/drupal-pi-model-3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/geerlingguy/drupal-pi/HEAD/images/drupal-pi-model-3.jpg -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.retry 2 | .vagrant 3 | inventory 4 | config.yml 5 | hook-pre-tasks.yml 6 | hook-tasks.yml 7 | roles/* 8 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # These are supported funding model platforms 3 | github: geerlingguy 4 | patreon: geerlingguy 5 | -------------------------------------------------------------------------------- /requirements.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - src: geerlingguy.security 3 | - src: geerlingguy.firewall 4 | - src: geerlingguy.git 5 | - src: geerlingguy.nginx 6 | - src: geerlingguy.pip 7 | - src: geerlingguy.docker 8 | -------------------------------------------------------------------------------- /.yamllint: -------------------------------------------------------------------------------- 1 | --- 2 | extends: default 3 | 4 | rules: 5 | line-length: 6 | max: 180 7 | level: warning 8 | 9 | ignore: | 10 | .github/stale.yml 11 | config.yml 12 | roles/geerlingguy.* 13 | -------------------------------------------------------------------------------- /ansible.cfg: -------------------------------------------------------------------------------- 1 | [defaults] 2 | roles_path = ./roles 3 | nocows = 1 4 | retry_files_enabled = False 5 | callback_result_format = yaml 6 | inventory = inventory 7 | 8 | [ssh_connection] 9 | pipelining = True 10 | control_path = /tmp/ansible-ssh-%%h-%%p-%%r 11 | -------------------------------------------------------------------------------- /example.inventory: -------------------------------------------------------------------------------- 1 | [pi] 2 | 127.0.0.1 ansible_python_interpreter=/usr/bin/python3 3 | 4 | # Comment the default localhost line above, and uncomment the line below 5 | # (replacing the IP with your Pi's IP address) if running the playbook from a 6 | # separate workstation. 7 | #10.0.100.20 ansible_user=pi ansible_python_interpreter=/usr/bin/python3 8 | -------------------------------------------------------------------------------- /tests/.vagrant/rgloader/loader.rb: -------------------------------------------------------------------------------- 1 | # This file loads the proper rgloader/loader.rb file that comes packaged 2 | # with Vagrant so that encoded files can properly run with Vagrant. 3 | 4 | if ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"] 5 | require File.expand_path( 6 | "rgloader/loader", ENV["VAGRANT_INSTALLER_EMBEDDED_DIR"]) 7 | else 8 | raise "Encoded files can't be read outside of the Vagrant installer." 9 | end 10 | -------------------------------------------------------------------------------- /templates/proxy.conf.j2: -------------------------------------------------------------------------------- 1 | proxy_set_header X-Forwarded-For $remote_addr; 2 | proxy_set_header Host $host; 3 | proxy_redirect off; 4 | 5 | {% if nginx_proxy_cache %} 6 | proxy_cache_bypass $drupal_logged_in $http_cache_control; 7 | proxy_no_cache $drupal_logged_in $arg_nocache$arg_page; 8 | proxy_cache_use_stale error timeout invalid_header updating http_500 http_502 http_503 http_504; 9 | add_header X-Proxy-Cache $upstream_cache_status; 10 | {% endif %} 11 | -------------------------------------------------------------------------------- /tests/README.md: -------------------------------------------------------------------------------- 1 | # Vagrant testing configuration 2 | 3 | This Vagrantfile can be used to test the Drupal Pi configuration locally if you do not have access to a Raspberry Pi. 4 | 5 | To test locally: 6 | 7 | 1. Install required Ansible roles (up one directory): `ansible-galaxy install -r requirements.yml --force` 8 | 1. Start and provision a local VM (in this directory): `vagrant up` 9 | 1. Visit `http://local.drupalpi.test/` and follow the Drupal installer steps to install Drupal. 10 | -------------------------------------------------------------------------------- /reset.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: pi 3 | become: true 4 | 5 | vars_files: 6 | - default.config.yml 7 | 8 | tasks: 9 | - name: Include config override file, if it exists. 10 | include_vars: "{{ item }}" 11 | with_fileglob: 12 | - config.yml 13 | 14 | - name: Completely destroy docker-compose environment. 15 | command: > 16 | docker-compose down -v 17 | chdir={{ drupal_pi_app_directory }} 18 | 19 | - name: Delete the contents of the host Drupal files directory. 20 | shell: > 21 | rm -rf {{ drupal_files_directory_host }}/* 22 | warn=false 23 | changed_when: true 24 | 25 | # Can't use docker_service module until the following issue is resolved: 26 | # https://github.com/ansible/ansible/issues/26937 27 | - name: Rebuild the docker-compose environment. 28 | command: > 29 | docker-compose up -d --remove-orphans 30 | chdir={{ drupal_pi_app_directory }} 31 | -------------------------------------------------------------------------------- /tests/Vagrantfile: -------------------------------------------------------------------------------- 1 | # -*- mode: ruby -*- 2 | # vi: set ft=ruby : 3 | VAGRANTFILE_API_VERSION = '2' 4 | 5 | Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| 6 | config.vm.hostname = 'local.drupalpi.test' 7 | config.vm.network :private_network, ip: '192.168.29.42' 8 | 9 | # VirtualBox configuration. 10 | config.vm.provider :virtualbox do |v| 11 | v.memory = 1024 12 | v.cpus = 1 13 | v.customize ["modifyvm", :id, "--natdnshostresolver1", "on"] 14 | v.customize ["modifyvm", :id, "--ioapic", "on"] 15 | v.customize ['modifyvm', :id, '--audio', 'none'] 16 | end 17 | 18 | # SSH options. 19 | config.ssh.insert_key = false 20 | config.ssh.forward_agent = true 21 | 22 | # Vagrant box. 23 | config.vm.box = 'geerlingguy/debian13' 24 | 25 | config.vm.provision "ansible" do |ansible| 26 | ansible.playbook = "../main.yml" 27 | ansible.inventory_path = "inventory" 28 | ansible.limit = "all" 29 | ansible.compatibility_mode = "2.0" 30 | end 31 | 32 | # Set the name of the VM. See: http://stackoverflow.com/a/17864388/100134 33 | config.vm.define 'drupal-pi' do |t| 34 | end 35 | 36 | end 37 | -------------------------------------------------------------------------------- /tasks/docker-compose-setup.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Ensure Drupal Pi app directory exists. 3 | file: 4 | path: "{{ drupal_pi_app_directory }}" 5 | state: directory 6 | 7 | - name: Ensure Drupal files directory exists. 8 | file: 9 | path: "{{ drupal_files_directory_host }}" 10 | state: directory 11 | owner: "{{ drupal_files_directory_owner }}" 12 | group: "{{ drupal_files_directory_owner }}" 13 | 14 | - name: Ensure Docker Compose file is present. 15 | template: 16 | src: templates/docker-compose.yml.j2 17 | dest: "{{ drupal_pi_app_directory }}/docker-compose.yml" 18 | mode: 0644 19 | register: docker_compose_file_result 20 | 21 | - name: Stop all Docker containers if docker-compose.yml has changed. 22 | command: > 23 | docker compose stop 24 | chdir={{ drupal_pi_app_directory }} 25 | when: docker_compose_file_result.changed 26 | 27 | # Can't use docker_service module until the following issue is resolved: 28 | # https://github.com/ansible/ansible/issues/26937 29 | - name: Bring up the Docker containers. 30 | command: > 31 | docker compose up -d --remove-orphans 32 | chdir={{ drupal_pi_app_directory }} 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Jeff Geerling 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /tasks/init.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - name: Update apt cache if needed. 3 | apt: 4 | update_cache: true 5 | cache_valid_time: 900 6 | 7 | - name: Ensure /usr/local/bin exists. 8 | file: 9 | path: /usr/local/bin 10 | state: directory 11 | mode: 0775 12 | 13 | - name: Ensure auth.log file is present. 14 | copy: 15 | dest: /var/log/auth.log 16 | content: "" 17 | force: false 18 | 19 | - name: Ensure dependencies are installed. 20 | apt: 21 | name: 22 | - curl 23 | - openssh-server 24 | - openssh-client 25 | - libffi-dev 26 | - libssl-dev 27 | # For some flavors of Debian without cron installed by default. 28 | - cron 29 | state: present 30 | 31 | # TODO: We can eventually just default to geerlingguy/drupal:latest once this 32 | # issue is resolved: https://github.com/geerlingguy/drupal-container/issues/25 33 | - name: Override Drupal Docker image if not on arm and default is set. 34 | set_fact: 35 | drupal_docker_image: 'geerlingguy/drupal:latest' 36 | when: 37 | - ansible_architecture is not search('armv7') 38 | - ansible_architecture is not search('aarch64') 39 | - "'geerlingguy/drupal:latest-' in drupal_docker_image" 40 | -------------------------------------------------------------------------------- /templates/drupal.conf.j2: -------------------------------------------------------------------------------- 1 | {% if nginx_proxy_cache %} 2 | map $http_cookie $drupal_logged_in { 3 | default 0; 4 | ~SESS 1; 5 | } 6 | {% endif %} 7 | 8 | {% if not nginx_use_as_lb %} 9 | {# Normal configuration for single Drupal Pi. #} 10 | server { 11 | listen 80 default; 12 | server_name {{ drupal_url }}; 13 | 14 | gzip on; 15 | gzip_static on; 16 | 17 | {% if nginx_proxy_cache %} 18 | proxy_cache cache; 19 | proxy_cache_key $scheme$host$uri$is_args$args; 20 | {% endif %} 21 | 22 | access_log {{ nginx_access_log }}; 23 | 24 | location / { 25 | proxy_pass {{ nginx_proxy_pass }}; 26 | include /etc/nginx/conf.d/proxy.conf; 27 | } 28 | } 29 | {% else %} 30 | {# Configuration for using Drupal Pis as load balancer for Pi Dramble. #} 31 | upstream backend { 32 | {% for host in nginx_lb_backends %} 33 | server {{ host }} max_fails=3; 34 | {% endfor %} 35 | } 36 | 37 | server { 38 | listen 80 default; 39 | 40 | gzip on; 41 | gzip_static on; 42 | 43 | {% if nginx_proxy_cache %} 44 | proxy_cache cache; 45 | proxy_cache_key $scheme$host$uri$is_args$args; 46 | {% endif %} 47 | 48 | location / { 49 | proxy_pass http://backend; 50 | include /etc/nginx/conf.d/proxy.conf; 51 | } 52 | } 53 | {% endif %} 54 | -------------------------------------------------------------------------------- /templates/docker-compose.yml.j2: -------------------------------------------------------------------------------- 1 | --- 2 | services: 3 | drupal: 4 | image: {{ drupal_docker_image }} 5 | container_name: drupal 6 | environment: 7 | DRUPAL_DATABASE_HOST: 'mysql' 8 | DRUPAL_DATABASE_PORT: '3306' 9 | DRUPAL_DATABASE_NAME: 'drupal' 10 | DRUPAL_DATABASE_USERNAME: 'drupal' 11 | DRUPAL_DATABASE_PASSWORD: '{{ drupal_database_password }}' 12 | DRUPAL_HASH_SALT: '{{ drupal_hash_salt }}' 13 | DRUPAL_DOWNLOAD_IF_NOT_PRESENT: '{{ drupal_download_if_not_present }}' 14 | DRUPAL_DOWNLOAD_VERIFY_CERT: 'false' 15 | PIDRAMBLE_ANALYTICS_ENABLED: '{{ drupal_pidramble_analytics_enabled }}' 16 | volumes: 17 | - {{ drupal_files_directory_host }}:{{ drupal_files_directory_container }}:rw 18 | ports: 19 | - "8080:80" 20 | restart: always 21 | 22 | mysql: 23 | image: {{ mysql_docker_image }} 24 | container_name: drupal-mysql 25 | environment: 26 | MYSQL_ROOT_PASSWORD: '{{ mysql_root_password }}' 27 | MYSQL_DATABASE: drupal 28 | MYSQL_USER: drupal 29 | MYSQL_PASSWORD: '{{ drupal_database_password }}' 30 | 31 | MARIADB_ROOT_PASSWORD: '{{ mysql_root_password }}' 32 | MARIADB_DATABASE: drupal 33 | MARIADB_USERNAME: drupal 34 | MARIADB_PASSWORD: '{{ drupal_database_password }}' 35 | ports: 36 | - "3306:3306" 37 | restart: always 38 | volumes: 39 | - /var/lib/mysql 40 | -------------------------------------------------------------------------------- /main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | - hosts: pi 3 | become: true 4 | 5 | vars_files: 6 | - default.config.yml 7 | 8 | pre_tasks: 9 | - name: Include config override file, if it exists. 10 | include_vars: "{{ item }}" 11 | with_fileglob: 12 | - config.yml 13 | tags: ['always'] 14 | 15 | - name: Include hook-pre-tasks.yml if it exists. 16 | include_tasks: "{{ item }}" 17 | with_first_found: 18 | - files: 19 | - hook-pre-tasks.yml 20 | skip: true 21 | tags: ['always'] 22 | 23 | - import_tasks: tasks/init.yml 24 | 25 | roles: 26 | - geerlingguy.security 27 | - geerlingguy.firewall 28 | - geerlingguy.git 29 | - name: geerlingguy.nginx 30 | tags: ['nginx'] 31 | - geerlingguy.pip 32 | - geerlingguy.docker 33 | 34 | tasks: 35 | - name: Include hook-tasks.yml if it exists. 36 | include_tasks: "{{ item }}" 37 | with_first_found: 38 | - files: 39 | - hook-tasks.yml 40 | skip: true 41 | 42 | - import_tasks: tasks/docker-compose-setup.yml 43 | 44 | - name: Copy nginx configuration into place. 45 | template: 46 | src: "templates/{{ item.src }}" 47 | dest: "{{ item.dest }}" 48 | mode: 0644 49 | notify: restart nginx 50 | with_items: 51 | - src: proxy.conf.j2 52 | dest: /etc/nginx/conf.d/proxy.conf 53 | - src: drupal.conf.j2 54 | dest: /etc/nginx/sites-enabled/drupal.conf 55 | tags: ['nginx'] 56 | -------------------------------------------------------------------------------- /default.config.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # Raspberry Pi configuration. 3 | drupal_pi_processor_count: 4 4 | drupal_pi_app_directory: /opt/drupal-pi 5 | 6 | # Security configuration. 7 | security_sudoers_passwordless: ['pi'] 8 | firewall_allowed_tcp_ports: 9 | - 22 10 | - 80 11 | - 443 12 | 13 | # Drupal configuration. 14 | drupal_url: www.drupalpi.test 15 | drupal_docker_image_armv7: 'geerlingguy/drupal:latest-arm32v7' 16 | drupal_docker_image_arm64: 'geerlingguy/drupal:latest-arm64' 17 | drupal_docker_image: "{{ drupal_docker_image_armv7 if 'armv7' in ansible_architecture else drupal_docker_image_arm64 }}" 18 | drupal_database_password: supersecure 19 | drupal_files_directory_host: /var/drupal/files 20 | drupal_files_directory_container: /var/www/html/sites/default/files 21 | drupal_files_directory_owner: "33" 22 | drupal_hash_salt: JcWFc2Z29LQIAeorS0IL9qB4SxOCjiYMd909AM3E2U 23 | drupal_pidramble_analytics_enabled: 'false' 24 | drupal_account_name: admin 25 | drupal_account_pass: admin 26 | drupal_download_if_not_present: 'true' 27 | 28 | # MySQL configuration. 29 | mysql_docker_image: 'webhippie/mariadb:latest' 30 | mysql_root_password: rootpasswordhere 31 | 32 | # Nginx configuration. 33 | nginx_worker_processes: "{{ drupal_pi_processor_count }}" 34 | nginx_worker_connections: "512" 35 | nginx_client_max_body_size: "64m" 36 | nginx_keepalive_timeout: "65" 37 | nginx_remove_default_vhost: true 38 | nginx_vhosts: [] 39 | nginx_access_log: "off" 40 | nginx_proxy_pass: http://127.0.0.1:8080/ 41 | nginx_proxy_cache: false 42 | nginx_proxy_cache_path: "/var/cache/nginx keys_zone=cache:32m max_size=1g inactive=15m" 43 | 44 | # Python configuration. 45 | pip_package: python3-pip 46 | pip_executable: pip3 47 | docker_pip_executable: '{{ pip_executable }}' 48 | 49 | # Nginx load balancing configuration (disabled by default). 50 | nginx_use_as_lb: false 51 | nginx_lb_backends: 52 | - '10.0.100.62' 53 | - '10.0.100.63' 54 | - '10.0.100.64' 55 | -------------------------------------------------------------------------------- /.github/workflows/CI.yml: -------------------------------------------------------------------------------- 1 | --- 2 | name: CI 3 | 'on': 4 | pull_request: 5 | push: 6 | branches: 7 | - master 8 | schedule: 9 | - cron: "30 4 * * 0" 10 | 11 | jobs: 12 | 13 | lint: 14 | name: Lint 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Check out the codebase. 18 | uses: actions/checkout@v2 19 | 20 | - name: Set up Python 3. 21 | uses: actions/setup-python@v2 22 | with: 23 | python-version: '3.x' 24 | 25 | - name: Install test dependencies. 26 | run: pip3 install yamllint 27 | 28 | - name: Lint code. 29 | run: | 30 | yamllint . 31 | 32 | ci: 33 | name: CI 34 | runs-on: ubuntu-latest 35 | 36 | env: 37 | HOSTNAME: www.drupalpi.test 38 | MACHINE_NAME: drupalpi 39 | PLAYBOOK_DIR: /tmp/drupal-pi 40 | 41 | steps: 42 | - name: Check out the codebase. 43 | uses: actions/checkout@v2 44 | 45 | - name: Run container in detached state. 46 | run: >- 47 | docker run --detach --name drupalpi 48 | --add-host "${{ env.HOSTNAME }}$(echo -e "\t")${{ env.MACHINE_NAME }}":127.0.0.1 49 | --volume="${PWD}":${{ env.PLAYBOOK_DIR }}/:rw --privileged --volume=/var/lib/docker 50 | --volume=/sys/fs/cgroup:/sys/fs/cgroup:rw --cgroupns=host 51 | geerlingguy/docker-debian13-ansible:latest /lib/systemd/systemd 52 | 53 | - name: Copy inventory file into place. 54 | run: >- 55 | docker exec drupalpi cp ${{ env.PLAYBOOK_DIR }}/example.inventory ${{ env.PLAYBOOK_DIR }}/inventory 56 | 57 | - name: Install requirements. 58 | run: >- 59 | docker exec --tty drupalpi ansible-galaxy install -r ${{ env.PLAYBOOK_DIR }}/requirements.yml 60 | 61 | - name: Ansible syntax check. 62 | run: >- 63 | docker exec --tty drupalpi ansible-playbook -i ${{ env.PLAYBOOK_DIR }}/inventory ${{ env.PLAYBOOK_DIR }}/main.yml --syntax-check 64 | 65 | - name: Run the playbook. 66 | run: >- 67 | docker exec drupalpi 68 | env ANSIBLE_FORCE_COLOR=true 69 | ansible-playbook -i ${{ env.PLAYBOOK_DIR }}/inventory 70 | --connection=local 71 | --extra-vars "{'firewall_enable_ipv6':false}" 72 | ${{ env.PLAYBOOK_DIR }}/main.yml 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Drupal Pi 2 | 3 | [](https://github.com/geerlingguy/drupal-pi/actions/workflows/CI.yml) 4 | 5 | **Drupal on Docker on a Raspberry Pi** 6 | 7 |
