I wrote a few scripts to automate deploying Zola to Digitalocean VPS. I have an existing account with Digitalocean so I just went with them. This assumes you have already configured your domain to use Digialocean's nameservers as it will be needed when we register and obtain our SSL certificate from Lets Encrypt. Although, we will only provision our server once, I decided to just complete the flow from provisioning and deploying of our Zola blog.

We will use Ansible to automate our remote commands to the server. Check with your desktop operating system package manager on how to install Ansible. On systems I'm familiar with:

MacOS

brew install ansible

Debian/Ubuntu

apt-get install ansible

Fedora

dnf install ansible

Or better yet, consult the ansible documentation for detail instructions.

Let's start by creating an inventory file containing only the IP address of our VPS in the Zola root directory. Ansible will query this file for list of IP address to login into. The IP address can be found in our digitalocean project page, beside the droplet name.

# ./inventory
[webservers]
123.210.123.210

The ansible playbook (set of tasks in yaml format) below will install and configure Nginx. It will also install Certbot and its utilities to register/obtain SSL certificate and configure Nginx to use it. Additionally, it will create a user that we will use when uploading our blog content. We will be uploading our SSH public key so we will not be providing a username and password everytime we upload content later on. This user will have write permission our custom Nginx document root.

# ./provision.yml
---
- name: Provision server
  hosts: webservers
  become: yes
  vars:
    domains:
      - "atske.com"

  tasks:
    - name: Update apt cache and install nginx
      apt:
        name: nginx
        state: latest
        update_cache: yes

    - name: Install certbot
      apt:
        name: certbot
        state: latest
        update_cache: yes

    - name: Install python3-certbot-nginx
      apt:
        name: python3-certbot-nginx
        state: latest
        update_cache: yes

    - name: Create user
      user:
        name: atske
        groups: www-data
        shell: /bin/bash

    - name: Upload authorized key
      authorized_key:
        user: kates
        state: present
        key: "{{ lookup('file', '~/.ssh/do_ed25519.pub') }}"

    - name: Create document root
      file:
        path: "/var/www/{{ item }}/public"
        state: directory
      with_items: "{{ domains }}"

    - name: Set permissions for /var/www
      file:
        path: "/var/www"
        mode: "666"
        group: "www-data"

    - name: Create temporary index.html
      shell: |
        cat > /var/www/{{ item }}/public/index.html <<EOF
        <html>
          <head><title>{{ item }}</title></head>
          <body>
            <h1>{{ item }}</h1>
          </body>
        </html>
        EOF
      with_items: "{{ domains }}"

    - name: Create website confs
      shell: |
        cat > /etc/nginx/sites-available/{{ item }} <<EOF
        server {
          listen 80;
          listen [::]:80;

          server_name {{ item }} www.{{ item }};
          root /var/www/{{ item }}/public;
          index index.html;

          location / {
            try_files \$uri \$uri/ =404;
          }
        }
        EOF

        cat > /etc/nginx/sites-available/https-{{ item }} <<EOF
        server {
          listen 443 ssl;
          listen [::]:443 ssl;

          server_name {{ item }} www.{{ item }};
          root /var/www/{{ item }}/public;
          index index.html;

          location / {
            try_files \$uri \$uri/ =404;
          }

          ssl_certificate /etc/letsencrypt/live/{{ item }}/fullchain.pem; # managed by Certbot
          ssl_certificate_key /etc/letsencrypt/live/{{ item }}/privkey.pem; # managed by Certbot
          include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
          ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot
        }
        EOF
      with_items: "{{ domains }}"

    - name: Enable websites
      file:
        src: /etc/nginx/sites-available/{{ item }}
        dest: /etc/nginx/sites-enabled/{{ item }}
        state: link
      with_items: "{{ domains }}"

    - name: Restart Nginx
      systemd_service:
        name: nginx
        state: restarted

    - name: Obtain letsencrypt certificate
      shell: |
        certbot register --nginx --agree-tos --email myemail@gmail.com -d {{ item }} -d www.{{ item }}
        touch /etc/letsencrypt/.registered-{{ item }}
      args:
        creates: /etc/letsencrypt/.registered-{{ item }}
      tags:
        - nginx
        - certbot
      with_items: "{{ domains }}"

    - name: Restart Nginx
      systemd_service:
        name: nginx
        state: restarted

    - name: Enable https websites
      file:
        src: /etc/nginx/sites-available/https-{{ item }}
        dest: /etc/nginx/sites-enabled/https-{{ item }}
        state: link
      with_items: "{{ domains }}"

    - name: Restart Nginx
      systemd_service:
        name: nginx
        state: restarted

To provision our droplet, we will run the playbook with

ansible-playbook -i inventory provision.yml -u root

We only need to do this once.

Next is the ansible playbook that we will be using everytime we need to upload our blog content to our Digitalocean VPS. It is basically just a series of shell command to run build Zola and compress the output. It will the upload the generated file to our user's home directory. Delete the Nginx document root, then unpack the upload compressed file in the place of the old document root. Finally, make sure that the unpacked files are readable by Nginx by issuing the chown command.

# upload.yml
---
- name: Builder
  hosts: localhost
  tasks:
    - name: Build zola
      shell: zola build
    - name: Compress
      shell: tar -czf site.tar.gz public

- name: Uploader
  hosts: webservers
  remote_user: atske
  vars:
    domain: atske.com
  tasks:
    - name: Upload file
      copy:
        src: site.tar.gz
        dest: ~/site.tar.gz
        mode: "666"
    - name: Unpack and cleanup
      shell: |
        rm -rf /var/www/{{ domain }}/public
        tar -xzf site.tar.gz -C /var/www/{{ domain }}
        chown -R "$USER":www-data /var/www/{{ domain }}
        rm ~/site.tar.gz

- name: Cleanup local
  hosts: localhost
  tasks:
    - name: Cleanup
      shell: |
        rm site.tar.gz
        rm -rf public

Upload the whole zola project by running

ansible-playbook -i inventory upload.yml --verbose

Reason why we upload the whole zola project and not only content folder is that it's a lot simpler process. By doing so, we have a way of having our blog configuration changes reflected when we subsequently deploy.

Hope this will make blogging with Zola easier and more fun.