Your Own Website

It’s possible to set-up your own website for free (minus the cost of running a computer and the internet connection that you probably already have). Here are the steps I used.

Step 1

Create an account at noip.com (or any other similar service) and get a URL you like.

Get a DDNS Key

Step 2

Build a Linux server, either a physical one or on something like VirtualBox. You want to make sure it either has a static address or will always be given the same address by your DHCP server.

Step 3

Install all the required packages, users, databases and software. Running an Ansible playbook can make this much easier. I used the below playbook. The lookups for the hashi_vault values are just a way of keeping secrets secure if you have installed something like HashiCorp Vault. The secrets could also be in an Ansible vault or just in the playbook (although that’s not very secure).

---
- hosts: wordpress
  become: yes
  vars_prompt:
    - name: "installation_number"
      prompt: "Enter the installation number"
      private: no
  vars:
    # HashiCorp Vault configuration values
    vault_secret_path: "kv/data/wordpress{{ installation_number }}"  
    db_name: "{{ lookup('hashi_vault', 'secret=' ~ vault_secret_path)['db_name'] }}"
    db_user: "{{ lookup('hashi_vault', 'secret=' ~ vault_secret_path)['db_user'] }}"
    db_password: "{{ lookup('hashi_vault', 'secret=' ~ vault_secret_path)['db_password'] }}"
    db_root_password: "{{ lookup('hashi_vault', 'secret=' ~ vault_secret_path)['db_root_password'] }}"
    admin_email: "{{ lookup('hashi_vault', 'secret=' ~ vault_secret_path)['admin_email'] }}"
    wp_url: "{{ lookup('hashi_vault', 'secret=' ~ vault_secret_path)['wp_url'] }}"
    wordpress_install_path: "/var/www/html/{{ wp_url }}"
  tasks:
    - name: Fail if installation_number is not numeric
      ansible.builtin.fail:
        msg: "Installation number must be numeric."
      when: installation_number is not match("^[0-9]+$")

    - name: Install necessary packages
      apt:
        name:
          - apache2
          - mariadb-server
          - php
          - php-mysql
          - libapache2-mod-php
          - php-cli
          - php-curl
          - php-gd
          - php-mbstring
          - php-xml
          - php-xmlrpc
          - php-soap
          - php-intl
          - php-zip
          - php-mail
          - python3-pymysql
          - certbot
          - python3-certbot-apache
          - imagemagick
          - p7zip-full
          - php-imagick
          - optipng
          - libjpeg-progs
        state: present
        update_cache: yes

    - name: Start and enable Apache
      service:
        name: apache2
        state: started
        enabled: yes

    - name: Start and enable MariaDB
      service:
        name: mariadb
        state: started
        enabled: yes

    - name: Set initial MariaDB root password
      shell: |
        mysql -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '{{ db_root_password }}'; FLUSH PRIVILEGES;"
      ignore_errors: yes

    - name: Remove anonymous users
      community.mysql.mysql_user:
        name: ''
        host_all: yes
        state: absent
        login_user: root
        login_password: "{{ db_root_password }}"

    - name: Disallow root login remotely
      community.mysql.mysql_user:
        name: root
        host: "{{ item }}"
        state: absent
        login_user: root
        login_password: "{{ db_root_password }}"
      with_items:
        - "{{ ansible_hostname }}"
        - 127.0.0.1
        - ::1

    - name: Remove test database
      community.mysql.mysql_db:
        name: test
        state: absent
        login_user: root
        login_password: "{{ db_root_password }}"

    - name: Reload privilege tables
      community.mysql.mysql_user:
        name: root
        host_all: yes
        priv: '*.*:ALL,GRANT'
        state: present
        login_user: root
        login_password: "{{ db_root_password }}"

    - name: Create WordPress database
      community.mysql.mysql_db:
        name: "{{ db_name }}"
        state: present
        login_user: root
        login_password: "{{ db_root_password }}"

    - name: Create WordPress user
      community.mysql.mysql_user:
        name: "{{ db_user }}"
        password: "{{ db_password }}"
        priv: "{{ db_name }}.*:ALL"
        state: present
        login_user: root
        login_password: "{{ db_root_password }}"

    - name: Download WordPress
      get_url:
        url: https://wordpress.org/latest.tar.gz
        dest: /tmp/wordpress.tar.gz

    - name: Create target directory
      file:
        path: "{{ wordpress_install_path }}"
        state: directory
        owner: www-data
        group: www-data
        mode: '0755'

    - name: Extract WordPress archive without top-level directory
      unarchive:
        src: /tmp/wordpress.tar.gz
        dest: "{{ wordpress_install_path }}"
        remote_src: yes
        extra_opts: [ "--strip-components=1" ]

    - name: Set permissions on WordPress directory
      file:
        path: "{{ wordpress_install_path }}"
        owner: www-data
        group: www-data
        state: directory
        recurse: yes

    - name: Fetch WordPress salts
      uri:
        url: https://api.wordpress.org/secret-key/1.1/salt/
        return_content: yes
      register: wp_salts

    - name: Create wp-config.php
      template:
        src: wp-config.php.j2
        dest: "{{ wordpress_install_path }}/wp-config.php"
        owner: www-data
        group: www-data
        mode: '0644'

    - name: Remove WordPress archive
      file:
        path: /tmp/latest.tar.gz
        state: absent

    - name: Configure Apache http for WordPress
      copy:
        content: |
          <VirtualHost *:80>
              ServerAdmin webmaster@localhost
              DocumentRoot {{ wordpress_install_path }}
              ServerName {{ wp_url }}

              <Directory {{ wordpress_install_path }}>
                  Options FollowSymLinks
                  AllowOverride All
                  Require all granted
              </Directory>

              <Directory {{ wordpress_install_path }}/wp-content>
                  Options FollowSymLinks
                  Require all granted
              </Directory>

              ErrorLog ${APACHE_LOG_DIR}/error.log
              CustomLog ${APACHE_LOG_DIR}/access.log combined

              RewriteEngine on
              RewriteCond %{SERVER_NAME} ={{ wp_url }}
              RewriteRule ^ https://%{SERVER_NAME}%{REQUEST_URI} [END,NE,R=permanent]
          </VirtualHost>
        dest: /etc/apache2/sites-available/{{ wp_url }}.conf

    - name: Configure Apache https for WordPress
      copy:
        content: |
          <IfModule mod_ssl.c>
          <VirtualHost *:443>
              ServerAdmin webmaster@localhost
              DocumentRoot {{ wordpress_install_path }}
              ServerName {{ wp_url }}

              <Directory {{ wordpress_install_path }}>
                  Options FollowSymLinks
                  AllowOverride All
                  Require all granted
              </Directory>

              <Directory {{ wordpress_install_path }}/wp-content>
                  Options FollowSymLinks
                  Require all granted
              </Directory>

              ErrorLog ${APACHE_LOG_DIR}/error.log
              CustomLog ${APACHE_LOG_DIR}/access.log combined

          </VirtualHost>
          </IfModule>
        dest: /etc/apache2/sites-available/{{ wp_url }}-ssl.conf

    - name: Enable WordPress http site
      command: a2ensite {{ wp_url }}.conf

    - name: Enable WordPress https site
      command: a2ensite {{ wp_url }}-ssl.conf

    - name: Disable default Apache site
      command: a2dissite 000-default.conf

    - name: Enable Apache rewrite module
      command: a2enmod rewrite

    - name: Restart Apache
      service:
        name: apache2
        state: restarted

    - name: Obtain Let's Encrypt certificate
      command: >
        certbot --apache --non-interactive --agree-tos
        --redirect --email {{ admin_email }}
        -d {{ wp_url }}
      notify: Restart Apache

    - name: Set up auto-renewal cron job
      cron:
        name: "Renew Let's Encrypt certificates"
        job: "certbot renew --quiet"
        minute: "0"
        hour: "3"

  handlers:
    - name: Restart Apache
      service:
        name: apache2
        state: restarted

You will also need a Jinja2 template for the php config called wp-config.php.j2 in a templates directory (just down from the playbook). It contains the following.

<?php
define('DB_NAME', '{{ db_name }}');
define('DB_USER', '{{ db_user }}');
define('DB_PASSWORD', '{{ db_password }}');
define('DB_HOST', 'localhost');
define('DB_CHARSET', 'utf8');
define('DB_COLLATE', '');
$table_prefix = 'wp_';
define('WP_DEBUG', false);

{{ wp_salts.content }}

if ( !defined('ABSPATH') )
    define('ABSPATH', dirname(__FILE__) . '/');
require_once(ABSPATH . 'wp-settings.php');

Step 4

Connect to your server via a browser, where you can setup an admin user (preferably don’t call it admin and don’t make it the account you are going to publish under, that can be set-up later).

Step 5

Check if your ISP is blocking ports 80 and 443 by default. If they are, hopefully they will let you turn that off in their management console. Configure your router to forward ports 80 and 443 to your webserver (almost all routers have this as a virtual host, port forwarding, poxy or something similar).

Step 6

On your new webserver, install the noip-duc client to update your DDNS IP address. First check your architecture.

uname -m

Get the package.

wget --content-disposition https://www.noip.com/download/linux/latest
tar xf noip-duc_3.3.0.tar.gz

Install the correct version

cd /home/$USER/noip-duc_3.3.0/binaries && sudo apt install ./noip-duc_3.3.0_{architecture}.deb

You should then test the following command and set it up to run regularly in the cron.

noip-duc -g all.ddnskey.com --username <DDNS Key Username> --password <DDNS Key Password>

Step 7

Certbot should already have been configured to get a letsencrypt certificate and keep it updated, but if you are installing manually you can run the following command.

certbot --apache

It will configure it’s own cron job, so you don’t have to worry about that.

Conclusion

You should then be able to connect with a secure encrypted connection to your new webserver at the URL you configured. The cron jobs will maintain your DDNS address (if your IP changes) and a valid certificate. No-ip will send you an email every 30 days to renew your address. If you want to help support them and avoid the hassle of renewals, you can get a subscription for a small fee, but this isn’t essential.

Extras, Themes and Plugins

That’s all you really need to get the site running, but then you can customise it to your needs. There are hundreds of different themes, so you can pick the look you want. I went with Septera, because it was used on some of the SCA pages, and I liked the look of it. I also installed the following Plug-ins:

  • All in One WP Security – It patched some obvious security holes and gave some nice features like Time-based One Time Passwords (TOTP).
  • Code Click-to-Copy by WPJohnny – Which lets users click on the code examples to copy them.
  • Edit Author Slug – Which fixes the one security issue that doesn’t seem to be fixed by the all in one plugin above, in that you can hide the actual user ids and use the author’s name instead.
  • EWWW Image Optimizer – To get a good overview of the different resolutions of images you have pre-rendered.
  • FluentSMTP – To send email via an existing account. I did manage to get native Postfix to send and receive via the website’s domain, but there were still a few issues and problems, this plugin is much simpler.
  • Imsanity – So I don’t have to set specific limits or worry about converting images before I upload them, this plugin automatically converts them to a sensible size and resolution.
  • Ninja Forms – It’s just the contact form at the moment, but I will probably use it for other things.
  • PrettyLinks – You can create short links on your site that point to other URLs. Good for if the address might change or just so you don’t have to remember a complicated address. Also, handy if you have something to sell (which I don’t).
  • Responsive Lightbox & Gallery – Lets users click on the pictures to view the full-size image and cycle through all the pictures in a post (if you sort them into folders).
  • SearchWP Modal Search Form – I seemed to need this to get the site search link to work.
  • WP Statistics – A simple statistics app that keeps the data local and lets you know if anyone is visiting the site.
  • Yoast SEO – Helps search engines index your site and helps people find it (maybe, I guess that takes a while).

I chose all these plug-ins, because they had the functionality I needed in a free edition. Most of them have a premium, pro or subscription option, but so far I haven’t needed that.

Leave a Reply

Your email address will not be published. Required fields are marked *