wordpress http://tech.oeru.org/ en Building a Course Site with WordPress on Ubuntu 20.04 using Docker Compose http://tech.oeru.org/building-course-site-wordpress-ubuntu-2004-using-docker-compose <span class="field field--name-title field--type-string field--label-hidden">Building a Course Site with WordPress on Ubuntu 20.04 using Docker Compose</span> <div class="field field-node--field-blog-tags field-name-field-blog-tags field-type-entity-reference field-label-above"> <h3 class="field__label">Blog tags</h3> <div class="field__items"> <div class="field__item field__item--wordpress"> <span class="field__item-wrapper"><a href="/taxonomy/term/35" hreflang="en">wordpress</a></span> </div> <div class="field__item field__item--docker"> <span class="field__item-wrapper"><a href="/taxonomy/term/16" hreflang="en">docker</a></span> </div> <div class="field__item field__item--docker-compose"> <span class="field__item-wrapper"><a href="/taxonomy/term/49" hreflang="en">docker-compose</a></span> </div> <div class="field__item field__item--ubuntu-linux"> <span class="field__item-wrapper"><a href="/taxonomy/term/12" hreflang="en">ubuntu linux</a></span> </div> <div class="field__item field__item--_004"> <span class="field__item-wrapper"><a href="/taxonomy/term/75" hreflang="en">20.04</a></span> </div> <div class="field__item field__item--multisite"> <span class="field__item-wrapper"><a href="/taxonomy/term/36" hreflang="en">multisite</a></span> </div> <div class="field__item field__item--redis"> <span class="field__item-wrapper"><a href="/taxonomy/term/21" hreflang="en">redis</a></span> </div> <div class="field__item field__item--nginx"> <span class="field__item-wrapper"><a href="/taxonomy/term/30" hreflang="en">nginx</a></span> </div> <div class="field__item field__item--php"> <span class="field__item-wrapper"><a href="/taxonomy/term/40" hreflang="en">php</a></span> </div> <div class="field__item field__item--lets-encrypt"> <span class="field__item-wrapper"><a href="/taxonomy/term/17" hreflang="en">let&#039;s encrypt</a></span> </div> </div> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><a title="View user profile." href="/user/1" lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Tue 24/08/2021 - 16:00</span> <div class="field field-node--field-image field-name-field-image field-type-image field-label-hidden has-multiple"> <figure class="field-type-image__figure image-count-1"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2021-09/Screenshot%202021-09-03%20at%2015-48-16%20https%20course%20oeru%20org_register-enrol_anon.png?itok=yna1rg6z" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;An anonymous view of an OERu course with Register Enrol link shown (top right).&quot;}" role="button" title="An anonymous view of an OERu course with Register Enrol link shown (top right)." data-colorbox-gallery="gallery-field_image-Dzutx_-P7XA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;An anonymous view of an OERu course with Register Enrol link shown (top right).&quot;}"><img src="/sites/default/files/styles/medium/public/2021-09/Screenshot%202021-09-03%20at%2015-48-16%20https%20course%20oeru%20org_register-enrol_anon.png?itok=CY0E87Dh" width="220" height="156" alt="An anonymous view of an OERu course with Register Enrol link shown (top right)." loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-2"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2021-09/Screenshot%202021-09-03%20at%2015-49-57%20https%20course%20oeru%20org_register-enrol_loggedin.png?itok=DyccmnI7" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;An OERu course with a logged in and registered user, see Register Enrol link (top right)&quot;}" role="button" title="An OERu course with a logged in and registered user, see Register Enrol link (top right)" data-colorbox-gallery="gallery-field_image-Dzutx_-P7XA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;An OERu course with a logged in and registered user, see Register Enrol link (top right)&quot;}"><img src="/sites/default/files/styles/medium/public/2021-09/Screenshot%202021-09-03%20at%2015-49-57%20https%20course%20oeru%20org_register-enrol_loggedin.png?itok=31IN-ffG" width="182" height="220" alt="An OERu course with a logged in and registered user, see Register Enrol link (top right)" loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-3"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2021-09/Screenshot%202021-09-06%20at%2014-54-40%20https%20course%20oeru%20org_lida101_wenotesfeed.png?itok=54vVTHcL" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;An OERu course WEnotes feed aggregation page. Note the sources from which feed messages are drawn - our WEnotes aggregator checks many sources including personal blogs that learners have registered.&quot;}" role="button" title="An OERu course WEnotes feed aggregation page. Note the sources from which feed messages are drawn - our WEnotes aggregator checks many sources including personal blogs that learners have registered." data-colorbox-gallery="gallery-field_image-Dzutx_-P7XA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;An OERu course WEnotes feed aggregation page. Note the sources from which feed messages are drawn - our WEnotes aggregator checks many sources including personal blogs that learners have registered.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-09/Screenshot%202021-09-06%20at%2014-54-40%20https%20course%20oeru%20org_lida101_wenotesfeed.png?itok=07Lv9rfB" width="220" height="156" alt="An OERu course WEnotes feed aggregation page. Note the sources from which feed messages are drawn - our WEnotes aggregator checks many sources including personal blogs that learners have registered." loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> </div> <div class="clearfix text-formatted field field-node--body field-name-body field-type-text-with-summary field-label-hidden"> <div class="field__items"> <div class="field__item"><p>The <a href="https://oeru.org">OERu</a> offers accredited tertiary (University level) courses to learners anywhere on the Internet, whether they are using desktop computers or mobile devices.</p> <p>Instead of using a Learning Management System to frame the courses, the OERu Courses are hosted in a <a href="https://wordpress.org">WordPress</a> <a href="https://wordpress.org/support/article/create-a-network/">MultiSite</a> (originally called a "Network" of sites within a single WordPress installation) implementation, with each course a 'subsite' represented by sub-directory below the main site. For example, the first <a href="https://oeru.org/learning-in-a-digital-age/">Learning in a Digital Age</a> micro-course, "<a href="https://oeru.org/oeru-partners/otago-polytechnic/digital-literacies-for-online-learning/">Digital literacies for online learning</a>" with the course code <em>LiDA 101</em>, can be found at <a href="https://course.oeru.org/lida101">https://course.oeru.org/lida101</a>. This post explains how you can replicate our fully Free and Open Source Software large-scale Open Educational Resource (OER) course delivery platform at negligible cost.</p> <ul class="table-of-contents"><li> <p><a href="#step-one---a-suitable-host">Step one - a suitable host</a></p> <ul><li> <p><a href="#get-your-domain-lined-up">Get your Domain lined up</a></p> </li> <li> <p><a href="#log-into-your-server">Log into your server</a></p> </li> <li> <p><a href="#sort-out-your-details">Sort out your details</a></p> </li> </ul></li> <li> <p><a href="#step-two---prepare-the-host">Step two - prepare the host</a></p> <ul><li> <p><a href="#set-up-docker-and-docker-compose">Set up Docker and Docker-Compose</a></p> </li> </ul></li> <li> <p><a href="#step-three---configure-your-domain">Step three - configure your domain</a></p> <ul><li> <p><a href="#set-up-nginx-reverse-proxy">Set up Nginx reverse proxy</a></p> </li> <li> <p><a href="#set-up-lets-encrypt-for-the-domain">Set up Let's Encrypt for the domain</a></p> </li> </ul></li> <li> <p><a href="#step-four---get-the-code">Step four - get the code</a></p> <ul><li> <p><a href="#wordpress-source-code">WordPress source code</a></p> </li> <li> <p><a href="#oeru-theme">OERu Theme</a></p> </li> <li> <p><a href="#customise-wp-configphp">Customise wp-config.php</a></p> </li> </ul></li> <li> <p><a href="#step-five---set-up-docker-compose-for-the-site">Step five - set up Docker Compose for the site</a></p> <ul><li> <p><a href="#create-the-nginx-containers-configuration">Create the NGINX container's configuration</a></p> </li> <li> <p><a href="#launch-containers">Launch containers</a></p> </li> </ul></li> <li> <p><a href="#step-six---set-up-your-site">Step Six - set up your site</a></p> <ul><li> <p><a href="#install-wordpress">Install WordPress</a></p> </li> <li> <p><a href="#enable-the-oeru-course-theme">Enable the OERu Course theme</a></p> </li> <li> <p><a href="#create-an-admin-user">Create an admin user</a></p> </li> <li> <p><a href="#enable-multisite">Enable multisite</a></p> </li> <li> <p><a href="#enable-the-relevant-plugins">Enable the relevant plugins</a></p> </li> <li> <p><a href="#third-party-plugins">Third Party plugins</a></p> </li> <li> <p><a href="#oeru-plugins">OERu plugins</a></p> </li> </ul></li> <li> <p><a href="#step-seven---celebrate">Step Seven - celebrate!</a></p> </li> <li> <p><a href="#snapshotting-your-first-oer-course">Snapshotting your first OER course!</a></p> </li> <li> <p><a href="#enabling-oerus-register-enrol-functionality">Enabling OERu's Register Enrol functionality</a></p> </li> <li> <p><a href="#data-backups">Data Backups</a></p> </li> <li> <p><a href="#staying-up-to-date">Staying up-to-date</a></p> </li> <li> <p><a href="#acknowledgements">Acknowledgements</a></p> </li> </ul><p>To host our WordPress Multisite, we use a quartet of <a href="https://docs.docker.com/get-started/overview/">Docker</a> containers - a <a href="https://en.wikipedia.org/wiki/Redis">Redis</a> caching server, an <a href="https://en.wikipedia.org/wiki/Nginx">Nginx</a> webserver, and two servers running the <a href="https://en.wikipedia.org/wiki/PHP">PHP script interpreter</a> in <a href="https://www.php.net/manual/en/install.fpm.php">FPM</a> mode, with the first being the main workhorse responding to queries sent to it by the Nginx server... The other PHP container is responsible for running cron (scheduled) tasks in the background. It's all driven by <a href="https://docs.docker.com/compose/">Docker Compose</a> (which coordinates the individual Docker containers) on an Ubuntu Linux host. The host also runs an Nginx instance to act as a '<a href="https://www.nginx.com/resources/glossary/reverse-proxy-server/">reverse proxy</a>', and the endpoint of our <a href="https://en.wikipedia.org/wiki/Transport_Layer_Security#SSL_1.0,_2.0,_and_3.0">SSL</a> (we use <a href="/node/11">Let's Encrypt</a> certificates). This is all consistent with our <a href="/node/39">FOSS Docker-based hosting conventions</a>.</p> <p>Although our process to get one of the these sites up and running is made up of simple steps, there are a bunch of them, so get comfortable and buckle yourself in and prepare for a fun ride! <em>Just beware - this is a pretty audacious tutorial describing an advanced production-ready system with lots of moving parts doing a serious amount of stuff behind the scenes, so this process is likely to take a few hours from start to finish and isn't for the faint of heart.</em></p> <h2><a id="user-content-step-one---a-suitable-host" href="#step-one---a-suitable-host" name="step-one---a-suitable-host" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Step one - a suitable host</h2> <p>The first step is to get yourself an entry-level virtual server or compute instance somewhere.</p> <p>I generally use <a href="https://digitalocean.com">DigitalOcean</a> (I have no affiliation with the company), but there are many other commodity hosting services (check out <a href="https://vultr.com">Vultr</a> or <a href="https://linode.com">Linode</a>, for example) around the world which offer comparably (or better) spec'd servers for USD5.00/month, or USD60.00/year. For that you get a Gigabyte (GB) of RAM, a processor, and 40GB of SSD (Static Storage Device = faster) storage.</p> <p>A server (a "Droplet" in Digital Ocean parlance) with a GB of RAM and 20+ GB of disk space will be sufficient for this sort of service. If you expect it to have heavy traffic, or you might want to add more services, you might want to invest in a higher-spec server up-front (because, among other things, it'll offer you more disk space). Most of our servers are USD40/month instances (USD480/year) which buys 8GB of RAM, 4 virtual processors, and 160GB of disk space.</p> <p>I suggest you create an account for yourself on your chosen hosting provider (and I encourage you to use Two Factor Authentication, aka 2FA, on your hosting account so that no one can log in as you and, say, delete your server unexpectedly - you'll find instructions on how to set up 2FA on your hosting provider's site) and create an Ubuntu 20.04 (or the most recent 'Long Term Support' (LTS) version - the next will be 22.04, in April 2022) in the zone nearest to you.</p> <p>If you don't already have an SSH key on your computer, I encourage you to <a href="https://support.atlassian.com/bitbucket-cloud/docs/set-up-an-ssh-key/">create one</a> and specify the <strong>public key</strong> in the process of creating your server - that should allow you to log in without needing a password!</p> <p>You'll need to note the server's IPv4 address (it'll be a series of 4 numbers, 0-254, separated by full stops, e.g. 103.99.72.244), and you should also be aware that your server will have a newer IPv6 address, which will be a set of 8 four hex character values [each hex character can have one of 16 values: 0-9,A-F] separated by colons, e.g. 2604:A880:0002:00D0:0000:0000:20DE:9001. With one or the other of those IPs, you can <a href="https://www.digitalocean.com/community/tutorials/how-to-use-ssh-to-connect-to-a-remote-server-in-ubuntu">log into it via SSH</a>.</p> <p>Once you get logged in, it's worth doing an upgrade of your server's Ubuntu system! Do that as follows:</p> <p><code>sudo apt-get update &amp;&amp; sudo apt-get dist-upgrade</code></p> <h3><a id="user-content-get-your-domain-lined-up" href="#get-your-domain-lined-up" name="get-your-domain-lined-up" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Get your Domain lined up</h3> <p>You will want to have a domain to point at your server, so you don't have to remember the IP number. There're are thousands of domain "registrars" in the world who'll help you do that... You just need to "register" a name, and you pay yearly fee (usually between USD10-30 depending on the country and the "TLD" (Top Level Domain. There're national ones like .nz, .au, .uk, .tv, .sa, .za, etc., or international domains (mostly associated with the US) like .com, .org, .net, and a myriad of others. Countries decide on how much their domains wholesale for and registrars add a margin for the registration service).</p> <p>Here in NZ, I use the services of Metaname (they're local to me in Christchurch, and I know them personally and trust their technical capabilities). If you're not sure who to use, ask your friends. Someone's bound to have recommendations (either positive or negative, in which case you'll know who to avoid).</p> <p>If you want to use your domain for other things besides your WordPress instance, I'd encourage you to use a subdomain, like (my usual choice) is "course.domainname", namely the subdomain "course" of "domainname".</p> <p>Once you have selected and registered your domain, you can set up (usually through a web interface provided by the registrar) an "A Record" which associates your website's name to the IPv4 address of your server. So you should just be able to enter your server's IPv4 address, the domain name (or sub-domain) you want to use for your WordPress service. Nowadays, <em>if your Domain Name host offers it (some don't, meaning they're way behind the times),</em> it's also important to define an IPv6 record, which is called an "AAAA Record"... you put in your IPv6 address instead of your IPv4 one.</p> <p>You might be asked to set a "Time-to-live" (which has to do with the length of time Domain Name Servers are asked to "cache" the association that the A Record specifies) in which case you can put in 3600 seconds or an hour depending on the time units your interface requests... but in most cases that'll be set to a default of an hour automatically.</p> <h3><a id="user-content-log-into-your-server" href="#log-into-your-server" name="log-into-your-server" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Log into your server</h3> <p>You should be able to test that your A and AAAA Records have been set correctly by logging into your server via SSH using your domain name rather than the IPv4 or IPv6 address you used previously. It should (after you accept the SSH warning that the server's name has a new name) work the same way your original SSH login did. On Linux, you'd SSH via a terminal and enter <code>ssh root@[domain name]</code>. I think you can do similar on MacOS and on Windows, I believe people typically use software called Putty...</p> <p>This will log you into your server as the 'root' user. It's not considered good practice to access your server as root (it's too easy to completely screw it up). Best practice is to create a separate 'non-root' user who has 'sudo' privileges and the ability to log in via SSH. If you are <em>currently logged in as 'root'</em>, you can create a normal user for yourself via (replace [username] with your chosen username):</p> <p><code>U=[username]</code><br /><code>adduser $U</code><br /><code>adduser $U ssh</code><br /><code>adduser $U sudoers</code></p> <p>You'll also want to a set a password for user [username]:</p> <p><code>passwd $U</code></p> <p>then become that user temporarily (note, the root user can 'become' another user without needing to enter a password) and create an SSH key and, in the process, the <code>.ssh</code> directory (directories starting with a '.' are normally 'hidden') for the file into which to put your public SSH key:</p> <p><code>su $U</code> <code>ssh-keygen -t rsa -b 2048</code> <code>nano ~/.ssh/authorized_keys</code></p> <p>and in that file, copy and paste (without spaces on either end) your <em>current computer's</em> <strong>public</strong> ssh key (<em>never publish</em> your private key anywhere!), save and close the file.</p> <p>From that point, you should be able to SSH to your server via <code>ssh [username]@[domain name]</code> without needing to enter a password.</p> <p>These instructions use 'sudo' in front of commands because I assume you're using a non-root user. The instructions will still work fine even if you're logged in as 'root'.</p> <h3><a id="user-content-sort-out-your-details" href="#sort-out-your-details" name="sort-out-your-details" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Sort out your details</h3> <p>You'll replace the similarly [named] variables in the configuration files and command lines below. These are the values you need to find or create.</p> <ul><li> <strong>[domain name]</strong> - the fully qualified domain name or subdomain by which you want your WPMS to be accessed. You must have full domain management ability on this domain. Example: <code>course.oeru.org</code> </li> <li> <strong>[port]</strong> - this is an unused port, i.e. not used by any other service, that will be used by the Nginx reverse proxy to talk to your Docker Nginx webserver container. A conventional option would be 8080... If that's already in use, try 8081, etc. You can check what ports are in use with the command <code>sudo netstat -punta</code> - the ports are listed after the ':' in each case</li> <li>MariaDB/MySQL database details <ul><li> <strong>[database root password]</strong> - the administrative user (root) password for this server - see <a href="/node/43">our tutorial on creating strong random passwords</a> </li> <li> <strong>[your database password]</strong> - if you, optionally, want to set up an admin user for yourself on this server. Paired with <strong>[your username]</strong> on the server.</li> <li> <strong>[database name]</strong> - the name of the database for this specific WordPress site - example <code>wordpress</code> </li> <li> <strong>[database user]</strong> - a separate username for this WordPress database - example <code>wordpress</code> (but it can be different too)- <strong>note</strong>: you'll be creating <em>two</em> database users with this same username and password, but that's intended.</li> <li> <strong>[database password]</strong> a separate password for this WordPress database user</li> </ul></li> <li>Authenticating SMTP details - this is required so your WordPress site can send emails to users - crucial things like email address validation and password recovery emails... <ul><li> <strong>[smtp host]</strong> - the domain name or IPv4 or IPv6 address of an SMTP server</li> <li> <strong>[smtp reply-to-email address]</strong> - a monitored email to which people can send email related to this WordPress site, e.g. webmaster@[domain name]</li> <li> <strong>[smtp user]</strong> - the username (often an email address) used to authenticate against your SMTP server, provided by your email provider.</li> <li> <strong>[smtp password]</strong> - the accompanying password, provided by your email provider.</li> </ul></li> <li>Wordpress configuration values <ul><li> <strong>[redis password]</strong> - another random password, this time for the caching service we'll set up to make your WordPress site faster than billy-o.</li> <li> <strong>[wordpress keys and salts]</strong> - a series of random numbers to make your WordPress site far more secure than it would otherwise be - <em>these can be generated automatically using the approach described below!</em>.</li> </ul></li> <li>For those incorporating our WEnotes system, you'll need the following (these values are optional and can be ignored!) <ul><li> <strong>[couchdb host]</strong> - the domain name of the couchdb host - in the case of the OERu, our host is couch.oerfoundation.org. Yours might be different.</li> <li> <strong>[couchdb mention database]</strong> - the name of the designated 'mention' database on the couchdb host.</li> <li> <strong>[couchdb user]</strong> - a couchdb username, provided by whoever manages your couchdb host</li> <li> <strong>[couchdb password]</strong> - a couchdb password, also provided by whoever manages your couchdb host</li> </ul></li> <li>Docker.com login details (you'll need a username and password).</li> </ul><p><strong>Note</strong>: not all values in all files surrounded by [] need to be replaced! If they're not included in the list above, leave them as you find them!</p> <h2><a id="user-content-step-two---prepare-the-host" href="#step-two---prepare-the-host" name="step-two---prepare-the-host" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Step two - prepare the host</h2> <p>Preparing the host involves ensuring your firewall, UFW, is configured properly, installing the Nginx webserver to act as your host's reverse proxy, and installing MariaDB (MySQL compatible but better, for a variety of reasons including that it's not controlled by Oracle) and configuring it properly. Then we can create the MariaDB database specifically for this WordPress installation. We also suggest you install Postfix so your server can send out email to you, and finally, we'll ensure that your server knows how to launch Docker containers and manage them with Docker Compose.</p> <p>Before we do anything else, let's make sure your Ubuntu package repository is up-to-date.</p> <p><code>sudo apt-get update</code></p> <p>If you pause this build process for more than a few hours, it pays to run it again before you continue on.</p> <h4><a id="user-content-firewall-with-ufw" href="#firewall-with-ufw" name="firewall-with-ufw" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Firewall with UFW</h4> <p>No computer system is ever full secure - there're always exploits waiting to be found, so security is a process of maintaining vigilance. Part of that is reducing exposure - minimising your "attack surface". Use a firewall - <code>ufw</code> is installed on Ubuntu by default and is easy to set up and maintain. Make sure you've got exceptions for SSH (without them, you could lock yourself out of your machine! Doh!).</p> <p>Run the following commands to allow your Docker containers to talk to other services on your host.</p> <p><code>sudo ufw allow in on docker0</code><br /><code>sudo ufw allow from 172.0.0.0/8 to any</code></p> <p>Specifically for Docker's benefit, you need to tweak the default Forwarding rule (I use <code>vim</code> as my editor. An alternative, also installed by default on Ubuntu, <code>nano</code>, is probably easier to use for simple edits like this, so I'll use <code>nano</code> here):</p> <p><code>sudo nano /etc/default/ufw</code></p> <p>and copy the line <code>DEFAULT_FORWARD_POLICY="DROP"</code> tweak it to look like this (commenting out the default, but leaving it there for future reference!):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co0">#DEFAULT_FORWARD_POLICY="DROP"</span> <span class="re2">DEFAULT_FORWARD_POLICY</span>=<span class="st0">"ACCEPT"</span></pre></div></div> <p>and then save and exit the file (CTRL-X and then 'Y').</p> <p>You also have to edit <code>/etc/ufw/sysctl.conf</code> and remove the "#" at the start of the following lines, so they look like this:</p> <p><code>sudo nano /etc/ufw/sysctl.conf</code></p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co0"># Uncomment this to allow this host to route packets between interfaces</span> net<span class="sy0">/</span>ipv4<span class="sy0">/</span><span class="re2">ip_forward</span>=<span class="nu0">1</span> net<span class="sy0">/</span>ipv6<span class="sy0">/</span>conf<span class="sy0">/</span>default<span class="sy0">/</span><span class="re2">forwarding</span>=<span class="nu0">1</span> net<span class="sy0">/</span>ipv6<span class="sy0">/</span>conf<span class="sy0">/</span>all<span class="sy0">/</span><span class="re2">forwarding</span>=<span class="nu0">1</span></pre></div></div> <p>and finally restart the network stack and ufw on your server</p> <p><code>sudo systemctl restart systemd-networkd</code><br /><code>sudo service ufw restart</code></p> <h4><a id="user-content-installing-the-nginx-webserverreverse-proxy" href="#installing-the-nginx-webserverreverse-proxy" name="installing-the-nginx-webserverreverse-proxy" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Installing the Nginx webserver/reverse proxy</h4> <p>In the configuration I'm describing here, you'll need a webserver running on the server - it'll be acting as a reverse proxy for the Docker-based Nginx instance described below. I prefer the efficiency of Nginx and clarity of Nginx configurations over those of Apache and other open source web servers. Here's how you install it.</p> <p><code>sudo apt-get install nginx-full</code></p> <p>To allow <code>nginx</code> to be visible via ports 80 and 443, run</p> <p><code>sudo ufw allow "Nginx Full"</code></p> <p>To check that all worked, you can put <code>http://[domain name]</code> into your browser's address bar, and you should see a default "NGINX" page...</p> <p>Note: make sure your hosting service is not blocking these ports at some outer layer (depending on who's providing that hosting service you may have to set up port forwarding).</p> <h4><a id="user-content-installing-mariadb" href="#installing-mariadb" name="installing-mariadb" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Installing MariaDB</h4> <p>MariaDB is effectively a drop-in alternative to MySQL and we prefer it because it's not controlled by Oracle and has a more active developer community. On Ubuntu, MariaDB pretends to be MySQL for compatibility purposes, so don't be weirded out by the interchangeable names below. Install the latest versions of the server and client like this.</p> <p><code>sudo apt-get install mariadb-server mariadb-client</code></p> <p>You need to set a root (admin) user password - you might want to create a <code>/root/.my.cnf</code> file containing the following (replacing [mysql root password]) to let you access MariaDB without a password from the commandline:</p> <p><code>sudo nano /root/.my.cnf </code></p> <p>and put the following info into it</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="br0">[</span>client<span class="br0">]</span> <span class="re2">user</span>=root <span class="re2">password</span>=<span class="br0">[</span>database root password<span class="br0">]</span></pre></div></div> <p>You should now be able to type <code>mysql</code> at the command prompt (note, the name mysql is used for backward compatibility with many implementations where MariaDB is being used to replace MySQL).</p> <p><strong>Optional MySQL non-root user</strong></p> <p>If you're happy to use your root user to access MySQL, e.g. <code>sudo mysql</code> (which uses 'sudo' to access it via the root user), then you can safely ignore the rest of this section.</p> <p>If you're accessing the server via a non-root user (which is a good idea, and is the reason we use <code>sudo</code> in this howto), you might want to create a similar <code>~/.my.cnf</code> file in your directory , with your username in place of <code>root</code>, and a <em>different password</em>. That will allow you to work with the MariaDB client without needing to enter the root credentials each time.</p> <p>To make it work, you'll need to run the following as the MySQL admin user - this should be the default on this new install - remember to replace your [tokens]!). This creates <em>two</em> users with the same credentials that will allow you to log in either from the same server (i.e. 'localhost') or from any of your Docker containers (often useful for debugging!), namely the wildcard '%'. <strong>Remember</strong>: if you change the user's details, you'll have to do it for both the localhost and '%' users.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">CREATE USER <span class="st0">"[your username]"</span><span class="sy0">@</span><span class="st0">"localhost"</span> IDENTIFIED BY <span class="st0">"[your database password]"</span>; CREATE USER <span class="st0">"[your username]"</span><span class="sy0">@</span><span class="st0">"%"</span> IDENTIFIED BY <span class="st0">"[your database password]"</span>; GRANT ALL ON <span class="sy0">*</span>.<span class="sy0">*</span> to <span class="st0">"[your username]"</span><span class="sy0">@</span><span class="st0">"localhost"</span> WITH GRANT OPTION; GRANT ALL ON <span class="sy0">*</span>.<span class="sy0">*</span> to <span class="st0">"[your username]"</span><span class="sy0">@</span><span class="st0">"%"</span> WITH GRANT OPTION; FLUSH PRIVILEGES;</pre></div></div> <p>Don't be alarmed if MySQL tells you "0 rows affected" when you create a user - unless you see a specific 'error', it's still creating them.</p> <p><strong>End optional MySQL non-root user section</strong></p> <p>Tweak the configuration so that it's listening on the right internal network device.</p> <p><code>sudo nano /etc/mysql/mariadb.conf.d/50-server.cnf</code></p> <p>and copy the bind-address line and adjust so it looks like this - we want MariaDB to be listening on all interfaces, not just localhost (<code>127.0.0.1</code>)...</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co0"># Instead of skip-networking the default is now to listen only on</span> <span class="co0"># localhost which is more compatible and is not less secure.</span> <span class="co0">#bind-address = 127.0.0.1</span> bind-address = 0.0.0.0</pre></div></div> <p>Then restart MariaDB:</p> <p><code>sudo service mysql restart</code></p> <p>It should now be listening on MySQL/MariaDB's default port <code>3306</code> on all interfaces, i.e. <code>0.0.0.0</code>. For safety's sake, external access to the MariaDB server is blocked by your UFW firewall.</p> <p>Now set up the database which will hold WordPress' data. Log into the MariaDB client on the host (if you've created a <code>.my.cnf</code> file in your home directory as describe above, you won't need to enter your username and password):</p> <p><code>mysql -u root -p</code></p> <p>Enter your root password when prompted and then replace the following [database-related tokens] to create the database with the right language encoding, along with access to the right separate user:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">CREATE DATABASE <span class="br0">[</span>database name<span class="br0">]</span> CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER <span class="st0">"[database user]"</span><span class="sy0">@</span><span class="st0">"%"</span> IDENTIFIED BY <span class="st0">"[database password]"</span>; GRANT ALL ON <span class="br0">[</span>database name<span class="br0">]</span>.<span class="sy0">*</span> to <span class="st0">"[database user]"</span><span class="sy0">@</span><span class="st0">"%"</span>; FLUSH PRIVILEGES;</pre></div></div> <p>The last line ensures that MariaDB has updated its internal permissions to recognise your new user. To exit the SQL client, just type <code>\q</code> and ENTER.</p> <h4><a id="user-content-sending-emails" href="#sending-emails" name="sending-emails" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Sending emails</h4> <p>Because a server like this one is set up to perform lots of rather complex jobs to perform, it's vital that your server has the ability to send you emails to alert you of problems, like failed updates or backups. We encourage you to follow our instructions on <a href="/node/28">how to configure your server to use the <code>Postfix</code> SMTP server to send out email, using your Authenticating SMTP details</a>.</p> <h4><a id="user-content-regular-automatic-database-backups" href="#regular-automatic-database-backups" name="regular-automatic-database-backups" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Regular automatic database backups</h4> <p>Finally, it's a good idea (but optional - if you're in a hurry, you can do this later) to make sure that your server is maintaining backups of your database - in this case, we'll use the <code>automysqlbackup</code> script to automatically maintain a set of dated daily database backups. It's easy to install, and the database backups will be in <code>/var/lib/automysqlbackup</code> in dated folders and files. <strong>If you haven't set up Postfix in the previous step, just beware you will be asked to set it up when installing automysqlbackup</strong>.</p> <p><code>sudo apt-get install automysqlbackup</code></p> <p>That's all there is to it. It should run automatically every night and store a set of historical SQL snapshots that may well save your bacon sometime down the track!</p> <h3><a id="user-content-set-up-docker-and-docker-compose" href="#set-up-docker-and-docker-compose" name="set-up-docker-and-docker-compose" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up Docker and Docker-Compose</h3> <p>First, you need to set up <a href="https://docs.docker.com/install/linux/docker-ce/ubuntu/">Docker support</a> on your server - use the 'repository method' for Ubuntu 20.04 and choose the 'x86_64 / amd64' tab!</p> <p>Also, if you're using a non-root user, follow the complete instructions including <em><a href="https://docs.docker.com/engine/install/linux-postinstall/">setting up Docker for your non-root user</a></em>.</p> <p>The way I implement this set of containers is to use <a href="https://docs.docker.com/compose/">Docker Compose</a>) which depends on the Python script interpreter (version 3+). I suggest using the latest installation instructions provided by the Docker community. Of the options provided, I use the 'alternative instructions', employing <a href="https://docs.docker.com/compose/install/#install-using-pip">the 'pip' approach</a>. This is what I usually do (to summarise the pip instructions):</p> <p>The firrst step is to Install Ubuntu's Python3 pip which is a bit outdated...</p> <p><code>sudo apt install python3-pip</code></p> <p>use the Ubuntu instance, called pip3 to install the latest Python 3 pip</p> <p><code>sudo pip install -U pip</code></p> <p>and (finally) install the docker-compose script:</p> <p><code>sudo pip install -U docker-compose</code></p> <h4><a id="user-content-set-up-our-conventional-directories" href="#set-up-our-conventional-directories" name="set-up-our-conventional-directories" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up our conventional directories</h4> <p>To set up your server, I recommend setting up a place for your Docker containers as per our <a href="/node/39">Docker-related conventions</a>:</p> <p><code>sudo mkdir -p /home/data/[domain name]</code><br /><code>sudo mkdir -p /home/docker/[domain name]</code></p> <h2><a id="user-content-step-three---configure-your-domain" href="#step-three---configure-your-domain" name="step-three---configure-your-domain" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Step three - configure your domain</h2> <h3><a id="user-content-set-up-nginx-reverse-proxy" href="#set-up-nginx-reverse-proxy" name="set-up-nginx-reverse-proxy" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up Nginx reverse proxy</h3> <p>A reverse proxy is a website intermediary. It accepts requests from the Internet, like a normal webserver would, but instead of having direct access to the data being requested, it, in turn, makes a request from another webserver either on the same host, or on a different one, and passes the results of that request back to the requester. In this case, the Nginx instance on the host is making a request of a <em>different host</em> that happens to reside on the same host instance as a Docker 'container'.</p> <p>Our convention is to create an Nginx reverse proxy configuration file with the same name as our [domain name], so in the case of, say, our Course WordPress Multisite, the file would be <code>/etc/nginx/sites-available/course.oeru.org</code>. Create a file in your <code>/etc/nginx/sites-available</code> with the following (again, replacing the [values] with your own values.</p> <p><code>sudo nano /etc/nginx/sites-available/[domain name]</code></p> <p>and copy and paste this in (and remember to replace the [tokens] with your relevant variables!):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co0">#</span> <span class="co0"># Set [domain name] and [port] below to make this work</span> <span class="co0">#</span> <span class="co0"># HTTP does *soft* redirect to HTTPS</span> <span class="co0">#</span> server <span class="br0">{</span> <span class="co0"># add [IP-Address:]80 in the next line if you want to limit this to a single interface</span> listen <span class="nu0">80</span>; listen <span class="br0">[</span>::<span class="br0">]</span>:<span class="nu0">80</span>; server_name <span class="br0">[</span>domain name<span class="br0">]</span>; root <span class="sy0">/</span>home<span class="sy0">/</span>data<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>scr; index index.php;   <span class="co0"># change the file name of these logs to include your server name</span> <span class="co0"># if hosting many services...</span> access_log <span class="sy0">/</span>var<span class="sy0">/</span>log<span class="sy0">/</span>nginx<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span>_access.log; error_log <span class="sy0">/</span>var<span class="sy0">/</span>log<span class="sy0">/</span>nginx<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span>_error.log;   <span class="co0"># for let's encrypt renewals!</span> include includes<span class="sy0">/</span>letsencrypt.conf;   <span class="co0"># redirect all HTTP traffic to HTTPS.</span> location <span class="sy0">/</span> <span class="br0">{</span> <span class="kw3">return</span> <span class="nu0">302</span> https:<span class="sy0">//</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="re1">$request_uri</span>; <span class="br0">}</span> <span class="br0">}</span> <span class="co0">#</span> <span class="co0"># HTTPS</span> <span class="co0">#</span> <span class="co0"># This assumes you're using Let's Encrypt for your SSL certs (and why wouldn't</span> <span class="co0"># you!?)... https://letsencrypt.org</span> server <span class="br0">{</span> <span class="co0"># add [IP-Address:]443 ssl in the next line if you want to limit this to a single interface</span> listen <span class="nu0">443</span> ssl; listen <span class="br0">[</span>::<span class="br0">]</span>:<span class="nu0">443</span> ssl; <span class="co0"># Note: these are *temporary* certificates, created when your host was set up</span> <span class="co0"># they are only in use to get Nginx to start up properly and let you create your let's encrypt certificates!</span> ssl_certificate <span class="sy0">/</span>etc<span class="sy0">/</span>ssl<span class="sy0">/</span>certs<span class="sy0">/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span class="sy0">/</span>etc<span class="sy0">/</span>ssl<span class="sy0">/</span>private<span class="sy0">/</span>ssl-cert-snakeoil.key; <span class="co0"># these will be used after we finish the Let's Encrypt process</span> <span class="co0">#ssl_certificate /etc/letsencrypt/live/[domain name]/fullchain.pem;</span> <span class="co0">#ssl_certificate_key /etc/letsencrypt/live/[domain name]/privkey.pem;</span> ssl_protocols TLSv1 TLSv1.1 TLSv1.2; <span class="co0"># to create this, see https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html</span> ssl_dhparam <span class="sy0">/</span>etc<span class="sy0">/</span>ssl<span class="sy0">/</span>certs<span class="sy0">/</span>dhparam.pem; keepalive_timeout 20s;   server_name <span class="br0">[</span>domain name<span class="br0">]</span>; root <span class="sy0">/</span>home<span class="sy0">/</span>data<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>src; index index.php;   <span class="co0"># change the file name of these logs to include your server name</span> <span class="co0"># if hosting many services...</span> access_log <span class="sy0">/</span>var<span class="sy0">/</span>log<span class="sy0">/</span>nginx<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span>_access.log; error_log <span class="sy0">/</span>var<span class="sy0">/</span>log<span class="sy0">/</span>nginx<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span>_error.log;   location <span class="sy0">/</span> <span class="br0">{</span> <span class="co0"># a good value for [port] is 8080, unless it's already in use by another service on your server...</span> proxy_pass http:<span class="sy0">//</span>127.0.0.1:<span class="br0">[</span>port<span class="br0">]</span>; proxy_set_header Upgrade <span class="re1">$http_upgrade</span>; proxy_set_header Connection <span class="st0">"upgrade"</span>; proxy_set_header Host <span class="re1">$http_host</span>; proxy_set_header X-Real-IP <span class="re1">$remote_addr</span>; proxy_set_header X-Forwarded-For <span class="re1">$proxy_add_x_forwarded_for</span>; proxy_set_header X-Forwarded-Host <span class="re1">$server_name</span>; proxy_set_header X-Forwarded-Proto https; proxy_connect_timeout <span class="nu0">900</span>; proxy_send_timeout <span class="nu0">900</span>; proxy_read_timeout <span class="nu0">900</span>; send_timeout <span class="nu0">900</span>; <span class="br0">}</span> <span class="co0">#</span> <span class="co0"># These "harden" your security</span> add_header <span class="st_h">'Access-Control-Allow-Origin'</span> <span class="st0">"*"</span>; <span class="co0"># from https://gist.github.com/Stanback/7145487</span> add_header <span class="st_h">'Access-Control-Allow-Credentials'</span> <span class="st_h">'true'</span> always; add_header <span class="st_h">'Access-Control-Allow-Methods'</span> <span class="st_h">'GET, POST, PUT, DELETE, OPTIONS'</span> always; add_header <span class="st_h">'Access-Control-Allow-Headers'</span> <span class="st_h">'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'</span> always; <span class="co0">#</span> <span class="co0"># for H5P embedding</span> <span class="co0">#add_header 'X-Frame-Options' 'ALLOW-FROM https://h5p.oeru.org';</span> <span class="co0"># required to be able to read Authorization header in frontend</span> add_header <span class="st_h">'Access-Control-Expose-Headers'</span> <span class="st_h">'Authorization'</span> always; <span class="co0"># tested at https://csp-evaluator.withgoogle.com/</span> <span class="co0"># works, but only B+ on MozOBs https://observatory.mozilla.org/analyze.html</span> add_header X-XSS-Protection <span class="st0">"1; mode=block"</span>; <span class="br0">}</span></pre></div></div> <p>Having created that file, we now have to create the <code>ssl_dhparam</code> file we referenced, staring by installing OpenSSL tools:</p> <p><code>sudo apt-get install openssl</code></p> <p>by running (warning - this can take quite a long time - like 5-15 minutes in my experience, depending on a lot of factors - the system needs to generate sufficient entropy to achieve acceptable randomness):</p> <p><code>sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096</code></p> <p>if you're short on time, you can create a key half the size far more quickly (a few seconds, typically):</p> <p><code>sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048</code></p> <p>When that's done, you should see there's a file here: <code>ls -l /etc/ssl/certs/dhparam.pem</code></p> <h3><a id="user-content-set-up-lets-encrypt-for-the-domain" href="#set-up-lets-encrypt-for-the-domain" name="set-up-lets-encrypt-for-the-domain" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up Let's Encrypt for the domain</h3> <p>We've already written a guide to setting up and securing your domain with <a href="/node/11">Let's Encrypt</a> but here're the relevant details:</p> <p><code>sudo mkdir /etc/nginx/includes</code></p> <p>and edit the following file</p> <p><code>sudo nano /etc/nginx/includes/letsencrypt.conf</code></p> <p>to make sure it has the following content:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co0"># Rule for legitimate ACME Challenge requests</span> location ^~ <span class="sy0">/</span>.well-known<span class="sy0">/</span>acme-challenge<span class="sy0">/</span> <span class="br0">{</span> default_type <span class="st0">"text/plain"</span>; <span class="co0"># this can be any directory, but this name keeps it clear</span> root <span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>letsencrypt; <span class="br0">}</span>   <span class="co0"># Hide /acme-challenge subdirectory and return 404 on all requests.</span> <span class="co0"># It is somewhat more secure than letting Nginx return 403.</span> <span class="co0"># Ending slash is important!</span> location = <span class="sy0">/</span>.well-known<span class="sy0">/</span>acme-challenge<span class="sy0">/</span> <span class="br0">{</span> <span class="kw3">return</span> <span class="nu0">404</span>; <span class="br0">}</span></pre></div></div> <p>Next, make sure your designated Let's Encrypt directory exists (note - you only need to do this once on a given host):</p> <p><code>sudo mkdir /var/www/letsencrypt</code></p> <p>Now, we'll make sure Nginx is aware of your configuration, which you do like this (substituting [domain name] with your domain!):</p> <p><code>sudo ln -sf /etc/nginx/sites-available/[domain name] /etc/nginx/sites-enabled</code></p> <p>then make sure Nginx is happy with your configuration syntax:</p> <p><code>sudo nginx -t</code></p> <p>and fix any typos which might've crept in. If it says your configurations are okay, then make your configuration live:</p> <p><code>sudo service nginx reload</code></p> <p>Once this is done, you can check to see if things are working properly by entering <code>http://[domain name]</code> in your browser's address bar. It <em>should</em> redirect you to <code>http**s**://[domain name]</code> <em>and</em> give you an error that there's a 'mismatch' in your certificates... which there is: my configuration approach means the server and your reverse proxy configuration are <em>temporarily</em> using the default "SnakeOil" SSL certificate pair (required to get Nginx to start in SSL mode), but your Nginx configuration is working to the extent required for us to request our Let's Encrypt certificate!</p> <p>And here's how we actually generate your SSL certificat via Let's Encrypt (replacing [domain name] as appropriate):</p> <p><code>sudo letsencrypt certonly --webroot -w /var/www/letsencrypt -d [domain name]</code></p> <p>If it works, it gratifyingly results in a message that starts with "Congratulations"! Well done if you got that! Note, if you get an error, make sure your domain is properly configured to point to your server! Also, there could be a delay in that configuration change taking effect due to the vagueries of the DNS system. If it worked, you should have a set of certificates in the directories, currently commented out with leading "#"s, in the Nginx configuration file above. You'll now need to re-edit it</p> <p><code>sudo nano /etc/nginx/sites-available/[domain name]</code></p> <p>to change it so the relevant part looks like this (again, ensuring your [domain name] is in place in the relevant locations!):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"> <span class="co0"># Note: these are *temporary* certificates, created when your host was set up</span> <span class="co0"># they are only in use to get Nginx to start up properly and let you create your let's encrypt certificates!</span> <span class="co0">#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;</span> <span class="co0">#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;</span> <span class="co0"># these will be used after we finish the Let's Encrypt process</span> ssl_certificate <span class="sy0">/</span>etc<span class="sy0">/</span>letsencrypt<span class="sy0">/</span>live<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>fullchain.pem; ssl_certificate_key <span class="sy0">/</span>etc<span class="sy0">/</span>letsencrypt<span class="sy0">/</span>live<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>privkey.pem;</pre></div></div> <p>and then we need to do our obligatory configuration test:</p> <p><code>sudo nginx -t</code></p> <p>and if Nginx is happy, reload the configuration to put it into effect:</p> <p><code>sudo service nginx reload</code></p> <p>Now, if you point your browser at <code>http://[domain name]</code>, it should automatically redirect to <code>https://[domain name]</code> without any errors, except that it won't yet have any content to show you, so you might get a 404 error, which is expected!</p> <h2><a id="user-content-step-four---get-the-code" href="#step-four---get-the-code" name="step-four---get-the-code" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Step four - get the code</h2> <p>Next we have to get all the relevant code for the WordPress site and its dependencies.</p> <h3><a id="user-content-wordpress-source-code" href="#wordpress-source-code" name="wordpress-source-code" class="heading-permalink" aria-hidden="true" title="Permalink"></a>WordPress source code</h3> <p>The latest source code for WordPress is always available from <a href="https://wordpress.org/latest.tar.gz">https://wordpress.org/latest.tar.gz</a> - we'll get a copy of it now and put it in the right place:</p> <p><code>cd /home/data/[domain name]</code><br /><code>sudo wget https://wordpress.org/latest.tar.gz</code><br /><code>sudo tar xvfz latest.tar.gz &amp;&amp; sudo mv wordpress src</code></p> <p>After this, you should find a directory, <code>src</code> in your [domain name] directory. That contains all the default source code for WordPress including default themes and plugins.</p> <p>Next, we have to make sure the default theme is in place, and then set up the WordPress multisite configuration process.</p> <p>Later we'll get the assortment of third party plugins and custom OERu plugins needed to flesh out the WordPress functionality we need.</p> <h3><a id="user-content-oeru-theme" href="#oeru-theme" name="oeru-theme" class="heading-permalink" aria-hidden="true" title="Permalink"></a>OERu Theme</h3> <p>First, we'll get the OERu theme, which is specially designed to provide an accessible desktop <em>or mobile</em> experience, given that many - even a majority - of our learners are in the developing world, where mobile computing is dominant!</p> <p>We'll go to the theme directory - you should already be in <code>/home/data/[domain name]</code>. From there we go into the theme directory:</p> <p><code>cd src/wp-content/themes</code></p> <p>and we issue a <code>git clone</code> command to retrieve the latest version of the OERu theme from our git repository:</p> <p><code>sudo git clone https://git.oeru.org/oeru/oeru_course.git oeru-course</code></p> <p>Then it's time to customise our WordPress configuration.</p> <h3><a id="user-content-customise-wp-configphp" href="#customise-wp-configphp" name="customise-wp-configphp" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Customise wp-config.php</h3> <p>First we need to create your wp-config.php file in your <code>src</code> directory:</p> <p><code>sudo nano wp-config.php</code></p> <p>Note: you'll need your password for Redis and a set of <strong>[wordpress keys and salts]</strong>, both of which are essentially just random numbers that are used to make you site far more secure. You can use the <a href="https://api.wordpress.org/secret-key/1.1/salt/">this link</a> to generate a set of random values suitable for copying-and-pasting into you <code>wp-config.php</code> file in your <code>src</code> directory - here's an example of the output I just requested (<strong>don't use these, generate your own</strong>):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">define<span class="br0">(</span><span class="st_h">'AUTH_KEY'</span>, <span class="st_h">'?4^Huc~R1=WW+T_p~.0dH$XJ`&gt;U*MoreMmZ{@tORSFG3aX37#ZS+0ou{j^DS3{f&lt;'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'SECURE_AUTH_KEY'</span>, <span class="st_h">'7.k4htjPnrA/?6JJlogA4Wp*o|,&amp;&amp;&gt;;20ppqeqHq#gI &lt;%gDz[o( hpRRB|!jws%'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'LOGGED_IN_KEY'</span>, <span class="st_h">'fTX,WkI=doAUE%?{zHp5.?fN%WWtBuy~`Scntr&lt;]I1WvlF6i=7J kjO0Z%%~Z-`N'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'NONCE_KEY'</span>, <span class="st_h">'?p&amp;]/*(G-+W!0#[&amp;Y6KKj)j Ok5QI(SUc@@rv,ivtF&gt; AR;Yv+Yu#&gt;$B$&lt;P9Ld|j'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'AUTH_SALT'</span>, <span class="st_h">'H6s2H~KP]Z7YXTFt|8[Lgz[1~5wF+PJzxR^KW$|he+9|RF/vi@}/|&lt;8bkC:w)qW%'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'SECURE_AUTH_SALT'</span>, <span class="st_h">'@- j6Kn CjP/mdbmLXtkC+&gt;1&gt;+H-8pXETlJ4+]b-9x_/t*.D}VA1w&lt;^A?0 R&lt;f+1'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'LOGGED_IN_SALT'</span>, <span class="st_h">' y9:oh)]nA$}%9N-xk1MAQN1bH 8z{UD/e~K|G5{(9y|,n2E*,KwYPIf~HwhHT J'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'NONCE_SALT'</span>, <span class="st_h">'|B?p#Q4|.=[VL8)2AX;zy-R2;x#dqIo=!C3,;OACT%-uaQ7Li5KSVSSnLahwlZ+o'</span><span class="br0">)</span>;</pre></div></div> <p>This is what your <code>wp-config.php</code> file should look like - copy and paste the following into the file, replacing the relevant [tokens] with your versions (in particular note that the above wordpress keys and salts need to go where I've got the token [wordpress keys and salts] below):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="sy0">&lt;</span>?php <span class="sy0">/**</span> <span class="sy0">*</span> The base configuration <span class="kw1">for</span> WordPress <span class="sy0">*</span> <span class="sy0">*</span> The wp-config.php creation script uses this <span class="kw2">file</span> during the <span class="sy0">*</span> installation. You don<span class="st_h">'t have to use the web site, you can * copy this file to "wp-config.php" and fill in the values. * * This file contains the following configurations: * * * MySQL settings * * Secret keys * * Database table prefix * * ABSPATH * * @link https://codex.wordpress.org/Editing_wp-config.php * * @package WordPress */   // ** MySQL settings - You can get this info from your web host ** // /** The name of the database for WordPress */ define('</span>DB_NAME<span class="st_h">', '</span><span class="br0">[</span>database name<span class="st_h">');   /** MySQL database username */ define('</span>DB_USER<span class="st_h">', '</span><span class="br0">[</span>database user<span class="br0">]</span><span class="st_h">');   /** MySQL database password */ define('</span>DB_PASSWORD<span class="st_h">', '</span><span class="br0">[</span>database password<span class="br0">]</span><span class="st_h">');   /** MySQL hostname */ //define('</span>DB_HOST<span class="st_h">', '</span>10.10.10.1<span class="st_h">'); define('</span>DB_HOST<span class="st_h">', '</span>172.17.0.1<span class="st_h">');   /** Database Charset to use in creating database tables. */ define('</span>DB_CHARSET<span class="st_h">', '</span>utf8mb4<span class="st_h">'); //define('</span>DB_CHARSET<span class="st_h">', '</span>utf8<span class="st_h">');   /** The Database Collate type. Don'</span>t change this <span class="kw1">if</span> <span class="kw1">in</span> doubt. <span class="sy0">*/</span> define<span class="br0">(</span><span class="st_h">'DB_COLLATE'</span>, <span class="st_h">''</span><span class="br0">)</span>;   <span class="sy0">/**</span><span class="co0">#@+</span> <span class="sy0">*</span> Authentication Unique Keys and Salts. <span class="sy0">*</span> <span class="sy0">*</span> Change these to different unique phrases<span class="sy0">!</span> <span class="sy0">*</span> You can generate these using the <span class="br0">{</span><span class="sy0">@</span><span class="kw2">link</span> https:<span class="sy0">//</span>api.wordpress.org<span class="sy0">/</span>secret-key<span class="sy0">/</span><span class="nu0">1.1</span><span class="sy0">/</span>salt<span class="sy0">/</span> WordPress.org secret-key service<span class="br0">}</span> <span class="sy0">*</span> You can change these at any point <span class="kw1">in</span> <span class="kw1">time</span> to invalidate all existing cookies. This will force all <span class="kw2">users</span> to have to log <span class="kw1">in</span> again. <span class="sy0">*</span> <span class="sy0">*</span> <span class="sy0">@</span>since 2.6.0 <span class="sy0">*/</span> <span class="br0">[</span>wordpress keys and salts<span class="br0">]</span>   <span class="sy0">/**</span> <span class="sy0">*</span> WordPress Database Table prefix. <span class="sy0">*</span> <span class="sy0">*</span> You can have multiple installations <span class="kw1">in</span> one database <span class="kw1">if</span> you give each <span class="sy0">*</span> a unique prefix. Only numbers, letters, and underscores please<span class="sy0">!</span> <span class="sy0">*/</span> <span class="re1">$table_prefix</span> = <span class="st_h">'wp_'</span>;   <span class="sy0">/**</span> <span class="sy0">*</span> For developers: WordPress debugging mode. <span class="sy0">*</span> <span class="sy0">*</span> Change this to <span class="kw2">true</span> to <span class="kw3">enable</span> the display of notices during development. <span class="sy0">*</span> It is strongly recommended that plugin and theme developers use WP_DEBUG <span class="sy0">*</span> <span class="kw1">in</span> their development environments. <span class="sy0">*</span> <span class="sy0">*</span> For information on other constants that can be used <span class="kw1">for</span> debugging, <span class="sy0">*</span> visit the Codex. <span class="sy0">*</span> <span class="sy0">*</span> <span class="sy0">@</span><span class="kw2">link</span> https:<span class="sy0">//</span>codex.wordpress.org<span class="sy0">/</span>Debugging_in_WordPress <span class="sy0">*/</span> define<span class="br0">(</span><span class="st_h">'WP_DEBUG'</span>, <span class="kw2">false</span><span class="br0">)</span>; <span class="sy0">//</span>define<span class="br0">(</span><span class="st_h">'WP_DEBUG'</span>, <span class="kw2">true</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'CONCATENATE_SCRIPTS'</span>, <span class="kw2">false</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'SCRIPT_DEBUG'</span>, <span class="kw2">true</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'WP_DEBUG'</span>, <span class="kw2">true</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'WP_DISABLE_FATAL_ERROR_HANDLER'</span>, <span class="kw2">true</span> <span class="br0">)</span>; <span class="sy0">//</span> <span class="nu0">5.2</span> and later   <span class="sy0">/*</span> We are behind a reverse proxy <span class="sy0">*/</span> <span class="kw1">if</span> <span class="br0">(</span>isset<span class="br0">(</span><span class="re1">$_SERVER</span><span class="br0">[</span><span class="st_h">'HTTP_X_FORWARDED_FOR'</span><span class="br0">]</span><span class="br0">)</span><span class="br0">)</span> <span class="br0">{</span> <span class="re1">$forwarded_address</span> = explode<span class="br0">(</span><span class="st_h">','</span>, <span class="re1">$_SERVER</span><span class="br0">[</span><span class="st_h">'HTTP_X_FORWARDED_FOR'</span><span class="br0">]</span><span class="br0">)</span>; <span class="re1">$_SERVER</span><span class="br0">[</span><span class="st_h">'REMOTE_ADDR'</span><span class="br0">]</span> = <span class="re1">$forwarded_address</span><span class="br0">[</span><span class="nu0">0</span><span class="br0">]</span>; <span class="br0">}</span> <span class="kw1">if</span> <span class="br0">(</span>isset<span class="br0">(</span><span class="re1">$_SERVER</span><span class="br0">[</span><span class="st_h">'HTTP_X_FORWARDED_PROTO'</span><span class="br0">]</span><span class="br0">)</span> <span class="sy0">&amp;&amp;</span> <span class="re1">$_SERVER</span><span class="br0">[</span><span class="st_h">'HTTP_X_FORWARDED_PROTO'</span><span class="br0">]</span> == <span class="st_h">'https'</span><span class="br0">)</span> <span class="br0">{</span> <span class="re1">$_SERVER</span><span class="br0">[</span><span class="st_h">'HTTPS'</span><span class="br0">]</span> = <span class="st_h">'on'</span>; <span class="br0">}</span>   <span class="sy0">/*</span> Disable the default <span class="st_h">'ad hoc'</span> cron mechanism. We<span class="st_h">'ll use actual cron instead. */ define('</span>DISABLE_WP_CRON<span class="st_h">', true);   /* That'</span>s all, stop editing<span class="sy0">!</span> Happy blogging. <span class="sy0">*/</span>   <span class="sy0">/**</span> Absolute path to the WordPress directory. <span class="sy0">*/</span> <span class="kw1">if</span> <span class="br0">(</span> <span class="sy0">!</span>defined<span class="br0">(</span><span class="st_h">'ABSPATH'</span><span class="br0">)</span> <span class="br0">)</span> define<span class="br0">(</span><span class="st_h">'ABSPATH'</span>, <span class="kw2">dirname</span><span class="br0">(</span>__FILE__<span class="br0">)</span> . <span class="st_h">'/'</span><span class="br0">)</span>;   <span class="sy0">/*</span> Multisite <span class="sy0">*/</span> <span class="sy0">//</span> see https:<span class="sy0">//</span>wordpress.org<span class="sy0">/</span>support<span class="sy0">/</span>article<span class="sy0">/</span>create-a-network<span class="sy0">/</span> define<span class="br0">(</span><span class="st_h">'WP_ALLOW_MULTISITE'</span>, <span class="kw2">true</span><span class="br0">)</span>; <span class="sy0">/*</span>define<span class="br0">(</span><span class="st_h">'MULTISITE'</span>, <span class="kw2">true</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'SUBDOMAIN_INSTALL'</span>, <span class="kw2">false</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'DOMAIN_CURRENT_SITE'</span>, <span class="st_h">'[domain name]'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'PATH_CURRENT_SITE'</span>, <span class="st_h">'/'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'SITE_ID_CURRENT_SITE'</span>, <span class="nu0">1</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'BLOG_ID_CURRENT_SITE'</span>, <span class="nu0">1</span><span class="br0">)</span>;<span class="sy0">*/</span>   <span class="sy0">/*</span> disable trash, immediately permanently delete <span class="sy0">*/</span> define<span class="br0">(</span><span class="st_h">'EMPTY_TRASH_DAYS'</span>, <span class="nu0">0</span><span class="br0">)</span>; <span class="sy0">/*</span> <span class="kw1">set</span> the default theme <span class="kw1">for</span> the network <span class="sy0">*/</span> define<span class="br0">(</span><span class="st_h">'WP_DEFAULT_THEME'</span>, <span class="st_h">'oeru_course'</span><span class="br0">)</span>;   <span class="sy0">/**</span> Caching-related configuration <span class="sy0">*/</span> <span class="sy0">/**</span> Redis <span class="sy0">*/</span> define<span class="br0">(</span><span class="st_h">'WP_REDIS_HOST'</span>, <span class="st_h">'redis'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'WP_REDIS_PASSWORD'</span>, <span class="st_h">'[redis password]'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'WP_REDIS_PATH'</span>, <span class="st_h">'/tmp/cache'</span><span class="br0">)</span>;   <span class="sy0">/*</span> WEnotes plugin configuration, commented out by default <span class="sy0">*/</span> <span class="sy0">/*</span> this is optional<span class="br0">(</span><span class="sy0">!!</span><span class="br0">)</span> - only use <span class="kw1">if</span> you<span class="st_h">'re deploying the OERu WEnotes stack - contact us if you want help! */ /*define('</span>WENOTES_HOST<span class="st_h">', '</span><span class="br0">[</span>couchdb host<span class="br0">]</span><span class="st_h">'); define('</span>WENOTES_PORT<span class="st_h">', '</span><span class="nu0">80</span><span class="st_h">'); define('</span>WENOTES_DB<span class="st_h">', '</span><span class="br0">[</span>couchdb mention database<span class="br0">]</span><span class="st_h">'); define('</span>WENOTES_USER<span class="st_h">', '</span><span class="br0">[</span>couchdb user<span class="br0">]</span><span class="st_h">'); define('</span>WENOTES_PASS<span class="st_h">', '</span><span class="br0">[</span>couchdb password<span class="br0">]</span><span class="st_h">');*/   // go to /wp-admin/maint/repair.php to see if a repair is needed... //define( '</span>WP_ALLOW_REPAIR<span class="st_h">', true );     /** Sets up WordPress vars and included files. */ require_once(ABSPATH . '</span>wp-settings.php<span class="st_h">');</span></pre></div></div> <h2><a id="user-content-step-five---set-up-docker-compose-for-the-site" href="#step-five---set-up-docker-compose-for-the-site" name="step-five---set-up-docker-compose-for-the-site" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Step five - set up Docker Compose for the site</h2> <p>The recipe for the four Docker containers that together provide the WordPress multisite service is pretty straightforward. All you need to do is go into <code>/home/docker/[domain name]</code> and</p> <p><code>sudo nano docker-compose.yml</code></p> <p>and enter the following, replacing the [tokens] as usual - note, if you don't have the <em>same value</em> for [port] specified below as you do in your 'reverse proxy' configuration above, <em>nothing</em> will work:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">version: <span class="st0">"3"</span>   services: redis: image: redis:alpine command: redis-server <span class="re5">--requirepass</span> <span class="br0">[</span>redis password<span class="br0">]</span> networks: default: aliases: - redis.<span class="br0">[</span>domain name<span class="br0">]</span> php: image: oeru<span class="sy0">/</span>php74-fpm-wpms links: - redis volumes: - <span class="sy0">/</span>home<span class="sy0">/</span>data<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>src:<span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html environment: - <span class="re2">SMTP_HOST</span>=<span class="br0">[</span>smtp host<span class="br0">]</span> - <span class="re2">SMTP_PORT</span>=<span class="nu0">587</span> - <span class="re2">SMTP_REPLYTO_EMAIL</span>=<span class="br0">[</span>smtp reply-to email address<span class="br0">]</span> - <span class="re2">SMTP_AUTH_USER</span>=<span class="br0">[</span>smtp user<span class="br0">]</span> - <span class="re2">SMTP_AUTH_PASSWORD</span>=<span class="br0">[</span>smtp password<span class="br0">]</span> restart: unless-stopped networks: default: aliases: - <span class="br0">[</span>domain name<span class="br0">]</span> nginx: image: oeru<span class="sy0">/</span>nginx-buster-wp links: - php - redis ports: - <span class="st0">"127.0.0.1:[port]:80"</span> volumes: - <span class="sy0">/</span>home<span class="sy0">/</span>data<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>nginx<span class="sy0">/</span>conf.d:<span class="sy0">/</span>etc<span class="sy0">/</span>nginx<span class="sy0">/</span>conf.d - <span class="sy0">/</span>home<span class="sy0">/</span>data<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>nginx<span class="sy0">/</span>cache:<span class="sy0">/</span>var<span class="sy0">/</span>cache<span class="sy0">/</span>nginx - <span class="sy0">/</span>home<span class="sy0">/</span>data<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>src:<span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html restart: unless-stopped networks: default: aliases: - nginx.<span class="br0">[</span>domain name<span class="br0">]</span> cron: image: oeru<span class="sy0">/</span>php74-fpm-wpms-cron links: - php - nginx volumes: - <span class="sy0">/</span>home<span class="sy0">/</span>data<span class="sy0">/</span><span class="br0">[</span>domain name<span class="br0">]</span><span class="sy0">/</span>src:<span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html environment: - <span class="re2">SMTP_HOST</span>=<span class="br0">[</span>smtp host<span class="br0">]</span> - <span class="re2">SMTP_PORT</span>=<span class="nu0">587</span> - <span class="re2">SMTP_REPLYTO_EMAIL</span>=<span class="br0">[</span>smtp reply-to email address<span class="br0">]</span> - <span class="re2">SMTP_AUTH_USER</span>=<span class="br0">[</span>smtp user<span class="br0">]</span> - <span class="re2">SMTP_AUTH_PASSWORD</span>=<span class="br0">[</span>smtp password<span class="br0">]</span> restart: unless-stopped networks: default: aliases: - cron.<span class="br0">[</span>domain name<span class="br0">]</span></pre></div></div> <h3><a id="user-content-create-the-nginx-containers-configuration" href="#create-the-nginx-containers-configuration" name="create-the-nginx-containers-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Create the NGINX container's configuration</h3> <p>In our Docker Compose file, we've specified that our NGINX container will have its configuration in <code>/home/data/[domain name]/nginx/conf.d</code>, so let's put it there.</p> <p>Run the following:</p> <p><code>sudo mkdir -p /home/data/[domain name]/nginx/conf.d</code></p> <p>to create the relevant directories, and then create the default configuration for the container:</p> <p><code>sudo nano /home/data/[domain name]/nginx/conf.d/default.conf</code></p> <p>Copy and paste this</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="co0"># Caching configuration</span> <span class="co0"># https://easyengine.io/wordpress-nginx/tutorials/multisite/subdirectories/fastcgi-cache-with-purging/</span> <span class="co0">#</span> fastcgi_cache_path <span class="sy0">/</span>var<span class="sy0">/</span>cache<span class="sy0">/</span>nginx <span class="re2">levels</span>=<span class="nu0">1</span>:<span class="nu0">2</span> <span class="re2">keys_zone</span>=WORDPRESS:500m <span class="re2">inactive</span>=60m; fastcgi_cache_key <span class="st0">"<span class="es2">$scheme</span><span class="es2">$request_method</span><span class="es2">$host</span><span class="es2">$request_uri</span>"</span>; fastcgi_cache_use_stale error timeout invalid_header http_500;   <span class="co0"># from https://www.nginx.com/resources/wiki/start/topics/recipes/wordpress/</span> <span class="co0">#</span> <span class="co0"># using subdir approach, not subdomain...</span> map <span class="re1">$uri</span> <span class="re1">$blogname</span><span class="br0">{</span> ~^<span class="br0">(</span>?P<span class="sy0">&lt;</span>blogpath<span class="sy0">&gt;/</span><span class="br0">[</span>^<span class="sy0">/</span><span class="br0">]</span>+<span class="sy0">/</span><span class="br0">)</span>files<span class="sy0">/</span><span class="br0">(</span>.<span class="sy0">*</span><span class="br0">)</span> <span class="re1">$blogpath</span> ; <span class="br0">}</span>   map <span class="re1">$blogname</span> <span class="re1">$blogid</span><span class="br0">{</span> default <span class="re5">-999</span>; <span class="co0">#Ref: http://wordpress.org/extend/plugins/nginx-helper/</span> include <span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html<span class="sy0">/</span>wp-content<span class="sy0">/</span>uploads<span class="sy0">/</span>nginx-helper<span class="sy0">/</span>map.conf; <span class="br0">}</span>   <span class="co0"># statements for each of your virtual hosts to this file</span> server <span class="br0">{</span> listen <span class="nu0">80</span>; root <span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html; index index.php index.html index.htm;   <span class="co0">#</span> <span class="co0"># Caching configuration</span> <span class="co0">#</span> <span class="co0">#fastcgi_cache start</span> <span class="kw1">set</span> <span class="re1">$skip_cache</span> <span class="nu0">1</span>;   <span class="co0"># POST requests and urls with a query string should always go to PHP</span> <span class="kw1">if</span> <span class="br0">(</span><span class="re1">$request_method</span> = POST<span class="br0">)</span> <span class="br0">{</span> <span class="kw1">set</span> <span class="re1">$skip_cache</span> <span class="nu0">1</span>; <span class="br0">}</span> <span class="kw1">if</span> <span class="br0">(</span><span class="re1">$query_string</span> <span class="sy0">!</span>= <span class="st0">""</span><span class="br0">)</span> <span class="br0">{</span> <span class="kw1">set</span> <span class="re1">$skip_cache</span> <span class="nu0">1</span>; <span class="br0">}</span>   <span class="co0"># Don't cache uris containing the following segments</span> <span class="kw1">if</span> <span class="br0">(</span><span class="re1">$request_uri</span> ~<span class="sy0">*</span> <span class="st0">"(/wp-admin/|/xmlrpc.php|/wp-(app|cron|login|register|mail).php|wp-.*.php|/feed/|index.php|wp-comments-popup.php|wp-links-opml.php|wp-locations.php|sitemap(_index)?.xml|[a-z0-9_-]+-sitemap([0-9]+)?.xml)"</span><span class="br0">)</span> <span class="br0">{</span> <span class="kw1">set</span> <span class="re1">$skip_cache</span> <span class="nu0">1</span>; <span class="br0">}</span> <span class="co0"># Don't use the cache for logged in users or recent commenters</span> <span class="kw1">if</span> <span class="br0">(</span><span class="re1">$http_cookie</span> ~<span class="sy0">*</span> <span class="st0">"comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in"</span><span class="br0">)</span> <span class="br0">{</span> <span class="kw1">set</span> <span class="re1">$skip_cache</span> <span class="nu0">1</span>; <span class="br0">}</span> <span class="co0"># end main Caching functionality</span>   <span class="co0">#</span> <span class="co0"># Flow of serving pages</span> <span class="co0">#</span> <span class="co0"># from https://www.nginx.com/resources/wiki/start/topics/recipes/wordpress/</span>   <span class="co0">#avoid php readfile()</span> location ^~ <span class="sy0">/</span>blogs.dir <span class="br0">{</span> internal; <span class="kw3">alias</span> <span class="sy0">/</span>var<span class="sy0">/</span>www<span class="sy0">/</span>html<span class="sy0">/</span>wp-content<span class="sy0">/</span>blogs.dir; access_log off; log_not_found off; expires max; <span class="br0">}</span>   <span class="kw1">if</span> <span class="br0">(</span><span class="sy0">!</span>-e <span class="re1">$request_filename</span><span class="br0">)</span> <span class="br0">{</span> rewrite <span class="sy0">/</span>wp-admin$ <span class="re1">$scheme</span>:<span class="sy0">//</span><span class="re1">$host</span><span class="re1">$uri</span><span class="sy0">/</span> permanent; rewrite ^<span class="br0">(</span><span class="sy0">/</span><span class="br0">[</span>^<span class="sy0">/</span><span class="br0">]</span>+<span class="br0">)</span>?<span class="br0">(</span><span class="sy0">/</span>wp-.<span class="sy0">*</span><span class="br0">)</span> <span class="re4">$2</span> <span class="kw2">last</span>; rewrite ^<span class="br0">(</span><span class="sy0">/</span><span class="br0">[</span>^<span class="sy0">/</span><span class="br0">]</span>+<span class="br0">)</span>?<span class="br0">(</span><span class="sy0">/</span>.<span class="sy0">*</span>\.php<span class="br0">)</span> <span class="re4">$2</span> <span class="kw2">last</span>; <span class="br0">}</span>   <span class="co0"># from https://www.digitalocean.com/community/tutorials/how-to-install-wordpress-with-nginx-on-ubuntu-14-04</span> <span class="co0"># with other bits from https://premium.wpmudev.org/blog/wordpress-multisite-wordpress-nginx/</span> location <span class="sy0">/</span> <span class="br0">{</span> try_files <span class="re1">$uri</span> <span class="re1">$uri</span><span class="sy0">/</span> <span class="sy0">/</span>index.php?<span class="re1">$args</span>; <span class="br0">}</span> error_page <span class="nu0">404</span> <span class="sy0">/</span><span class="nu0">404</span>.html;   <span class="co0"># Directives to send expires headers and turn off 404 error logging.</span> location ~<span class="sy0">*</span> ^.+\.<span class="br0">(</span>xml<span class="sy0">|</span>ogg<span class="sy0">|</span>ogv<span class="sy0">|</span>svg<span class="sy0">|</span>svgz<span class="sy0">|</span>eot<span class="sy0">|</span>otf<span class="sy0">|</span>woff<span class="sy0">|</span>mp4<span class="sy0">|</span>ttf<span class="sy0">|</span>css<span class="sy0">|</span>rss<span class="sy0">|</span>atom<span class="sy0">|</span>js<span class="sy0">|</span>jpg<span class="sy0">|</span>jpeg<span class="sy0">|</span>gif<span class="sy0">|</span>png<span class="sy0">|</span>ico<span class="sy0">|</span><span class="kw2">zip</span><span class="sy0">|</span>tgz<span class="sy0">|</span>gz<span class="sy0">|</span>rar<span class="sy0">|</span>bz2<span class="sy0">|</span>doc<span class="sy0">|</span>xls<span class="sy0">|</span>exe<span class="sy0">|</span>ppt<span class="sy0">|</span><span class="kw2">tar</span><span class="sy0">|</span>mid<span class="sy0">|</span>midi<span class="sy0">|</span>wav<span class="sy0">|</span>bmp<span class="sy0">|</span>rtf<span class="br0">)</span>$ <span class="br0">{</span> access_log off; log_not_found off; expires max; <span class="br0">}</span>   location ~ \.php$ <span class="br0">{</span> try_files <span class="re1">$uri</span> =<span class="nu0">404</span>; fastcgi_pass php:<span class="nu0">9000</span>; fastcgi_split_path_info ^<span class="br0">(</span>.+\.php<span class="br0">)</span><span class="br0">(</span><span class="sy0">/</span>.+<span class="br0">)</span>$; fastcgi_keep_conn on; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME <span class="re1">$document_root</span><span class="re1">$fastcgi_script_name</span>; fastcgi_param PATH_INFO <span class="re1">$fastcgi_path_info</span>; fastcgi_intercept_errors on; fastcgi_buffer_size 128k; fastcgi_buffers <span class="nu0">256</span> 16k; fastcgi_busy_buffers_size 256k; fastcgi_temp_file_write_size 256k; fastcgi_read_timeout <span class="nu0">500</span>; <span class="co0">#</span> <span class="co0"># caching functionality</span> fastcgi_cache_bypass <span class="re1">$skip_cache</span>; fastcgi_no_cache <span class="re1">$skip_cache</span>; fastcgi_cache WORDPRESS; fastcgi_cache_valid 60m; <span class="br0">}</span> <span class="co0">#</span> <span class="co0"># caching functionality</span> location ~ <span class="sy0">/</span>purge<span class="br0">(</span><span class="sy0">/</span>.<span class="sy0">*</span><span class="br0">)</span> <span class="br0">{</span> fastcgi_cache_purge WORDPRESS <span class="st0">"<span class="es2">$scheme</span><span class="es2">$request_method</span><span class="es2">$host</span>$1"</span>; <span class="br0">}</span> location = <span class="sy0">/</span>favicon.ico <span class="br0">{</span> log_not_found off; access_log off; <span class="br0">}</span> location = <span class="sy0">/</span>robots.txt <span class="br0">{</span> allow all; log_not_found off; access_log off; <span class="br0">}</span> location ~ ^<span class="br0">(</span><span class="sy0">/</span><span class="br0">[</span>^<span class="sy0">/</span><span class="br0">]</span>+<span class="sy0">/</span><span class="br0">)</span>?files<span class="sy0">/</span><span class="br0">(</span>.+<span class="br0">)</span> <span class="br0">{</span> try_files <span class="sy0">/</span>wp-content<span class="sy0">/</span>blogs.dir<span class="sy0">/</span><span class="re1">$blogid</span><span class="sy0">/</span>files<span class="sy0">/</span><span class="re4">$2</span> <span class="sy0">/</span>wp-includes<span class="sy0">/</span>ms-files.php?<span class="re2">file</span>=<span class="re4">$2</span> ; access_log off; log_not_found off; expires max; <span class="br0">}</span> <span class="br0">}</span></pre></div></div> <p>and save and exit the file.</p> <p>And now we have to create the following directory and file:</p> <p><code>sudo mkdir -p /home/data/[domain name]/src/wp-content/uploads/nginx-helper</code></p> <p><code>sudo touch /home/data/[domain name]/src/wp-content/uploads/nginx-helper/map.conf</code></p> <p>Once that's done, we're ready to launch our containers... Phew.</p> <h3><a id="user-content-launch-containers" href="#launch-containers" name="launch-containers" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Launch containers</h3> <p>Once that's done you can download the relevant containers (you may need to set up an account on <a href="https://hub.docker.com">https://hub.docker.com</a> first - do that and then run <code>docker login</code> before doing the following):</p> <p><code>docker-compose pull &amp;&amp; docker-compose up -d &amp;&amp; docker-compose logs -f</code></p> <p>which will show you the combined logs of the four containers and should give you some insights if something goes wrong. If all is well, you can type <code>CTRL-C</code> to exit it. The Docker containers (to see what's running, you can run <code>docker-compose ps</code>) will run until you explicitly stop them (<code>docker-compose stop</code>). Unless you stop them prior to lock down, they will automatically restart anytime your server reboots.</p> <h2><a id="user-content-step-six---set-up-your-site" href="#step-six---set-up-your-site" name="step-six---set-up-your-site" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Step Six - set up your site</h2> <h3><a id="user-content-install-wordpress" href="#install-wordpress" name="install-wordpress" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install WordPress</h3> <p>You should now be able to point your browser at <code>https://[your domain]</code> and you should automatically be redirected to the WordPress site install script, with all the database details already entered. You'll need to fill in the configuration fields and create an admin user.</p> <h3><a id="user-content-enable-the-oeru-course-theme" href="#enable-the-oeru-course-theme" name="enable-the-oeru-course-theme" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Enable the OERu Course theme</h3> <p>You should already be in the <code>/home/docker/[domain name]</code> directory, but if not,</p> <p><code>cd /home/docker/[domain name]</code></p> <p>and then you can enable the OERu Course theme, <code>oeru-course</code>, using WordPress's command line utility, <code>wp</code>, via your PHP container:</p> <p><code>docker-compose exec -u www-data php wp theme enable oeru-course</code></p> <p>You should get the message <code>Success: Enabled the 'OERu Course' theme.</code> if all went well.</p> <h3><a id="user-content-create-an-admin-user" href="#create-an-admin-user" name="create-an-admin-user" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Create an admin user</h3> <p>Your admin user should probably be an account not normally used by a person. I usually create an admin user with a username of 'admin', a random password (using pwgen as above) and a role-based email like webmaster@[domain name] (if your domain name has email services) or some similarly useful generic email (using that protects against the 'tyranny of the individual', e.g. if you leave the organisation in whose interest you're setting up this site, and those left behind will have to make sense of this site and keep it running!</p> <p>Later one, if you want to log into the site, you can always get a login prompt by going to https://[domain name]/admin/ ...</p> <h3><a id="user-content-enable-multisite" href="#enable-multisite" name="enable-multisite" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Enable multisite</h3> <p>The WordPress configuration we've set up in the wp-config.php file should have enabled the inbuilt functionality for enabling 'multisite' mode (or 'network', as it used to be called, i.e. for a "Network of Blogs", back when WordPress was more exclusively used for blogging). <a href="https://wordpress.org/support/article/create-a-network/">WordPress developer documentation on 'Creating a network'</a> might be a useful reference if you run into any trouble.</p> <p>Logged in as your admin user, in the admin menu structure go to Administration &gt;Tools &gt; Network Setup</p> <p>You will be asked ot provide your Network Title and Network Admin Email (which can be the same as your current admin email).</p> <p>You will also be asked whether you want to use the 'Sub-domains' or 'Sub-directories' structure for your network. Select the <strong>Sub-directories</strong> option (you will need to make changes your Nginx configurations and the wp-config.php configuration to use Sub-domains, which are beyond the scope of this howto), which means that each of your subsites will have a separate directory under your main site [domain name]. For example, on the OERu Course site, course.oeru.org, the Learning in a Digital Age 101 course sub-site is course.oeru.org/lida101. If you chose to use the Sub-domain option, you would instead reference that course as lida101.course.oeru.org.</p> <p>Once you've enabled multisite, you need to update your wp-config.php -</p> <p><code>sudo nano wp-config.php</code></p> <p>and make the following change, to comment out the WP_ALLOW_MULTISITE variable, and uncomment the various MULTISITE-related settings:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="sy0">/*</span> Multisite <span class="sy0">*/</span> <span class="sy0">//</span> see https:<span class="sy0">//</span>wordpress.org<span class="sy0">/</span>support<span class="sy0">/</span>article<span class="sy0">/</span>create-a-network<span class="sy0">/</span> <span class="sy0">//</span>define<span class="br0">(</span><span class="st_h">'WP_ALLOW_MULTISITE'</span>, <span class="kw2">true</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'MULTISITE'</span>, <span class="kw2">true</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'SUBDOMAIN_INSTALL'</span>, <span class="kw2">false</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'DOMAIN_CURRENT_SITE'</span>, <span class="st_h">'[domain name]'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'PATH_CURRENT_SITE'</span>, <span class="st_h">'/'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'SITE_ID_CURRENT_SITE'</span>, <span class="nu0">1</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'BLOG_ID_CURRENT_SITE'</span>, <span class="nu0">1</span><span class="br0">)</span>;</pre></div></div> <p>After you've saved that, and refreshed the page on your WordPress site, you should be looking at a fully functioning multisite!</p> <h3><a id="user-content-enable-the-relevant-plugins" href="#enable-the-relevant-plugins" name="enable-the-relevant-plugins" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Enable the relevant plugins</h3> <h3><a id="user-content-third-party-plugins" href="#third-party-plugins" name="third-party-plugins" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Third Party plugins</h3> <p>Now we need to get the specific OERu plugins that are not (yet) available through the WordPress plugin site.</p> <p>The ones we use by default are</p> <p><code> advanced-responsive-video-embedder, check-email, disable-comments, h5p, hypothesis, redis-cache, safe-redirect-manager, unconfirmed, wp-security-audit-log</code></p> <p>As we did with the OERu Course theme, we'll install and active these plugins using WordPress's command line utility, <code>wp</code>, via your PHP container:</p> <p><code>cd /home/docker/[domain name]</code></p> <p>you can see the names of all your running containers by running this:</p> <p><code>docker-compose ps</code></p> <p>and you can download and activate all these plugins in 'network' (aka multisite) mode like this:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> advanced-responsive-video-embedder <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> check-email <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> disable-comments <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> h5p <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> hypothesis <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> redis-cache <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> safe-redirect-manager <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> unconfirmed <span class="re5">--activate-network</span> docker-compose <span class="kw3">exec</span> <span class="re5">-u</span> www-data php wp plugin <span class="kw2">install</span> wp-security-audit-log <span class="re5">--activate-network</span></pre></div></div> <p>These plugins should work as required without any specific configuration, but you're welcome to have a look at what they're doing and tweak them as you see fit.</p> <h3><a id="user-content-oeru-plugins" href="#oeru-plugins" name="oeru-plugins" class="heading-permalink" aria-hidden="true" title="Permalink"></a>OERu plugins</h3> <p>The final step is to install and enable the custom-developed plugins required to make this WordPress installation meet the requirements of an OERu Course site.</p> <p>At the OERu (and the OER Foundation, who coordinates the OERu and employs me) we make extensive use of Git to manage our source code and to deploy it on our various servers. We'll use it here.</p> <p>You'll need to go back to the plugin directory:</p> <p><code>cd /home/data/[domain name]/src/wp-content/plugins</code></p> <p>and run the following:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="kw2">sudo</span> <span class="kw2">git clone</span> https:<span class="sy0">//</span>git.oeru.org<span class="sy0">/</span>oeru<span class="sy0">/</span>blog-feed-finder.git blog-feed-finder <span class="kw2">sudo</span> <span class="kw2">git clone</span> https:<span class="sy0">//</span>git.oeru.org<span class="sy0">/</span>oeru<span class="sy0">/</span>oeru-h5p-tools.git oeru-h5p-tools <span class="kw2">sudo</span> <span class="kw2">git clone</span> https:<span class="sy0">//</span>git.oeru.org<span class="sy0">/</span>oeru<span class="sy0">/</span>register-enrol.git register-enrol <span class="kw2">sudo</span> <span class="kw2">git clone</span> https:<span class="sy0">//</span>git.oeru.org<span class="sy0">/</span>oeru<span class="sy0">/</span>wenotes.git wenotes <span class="kw2">sudo</span> <span class="kw2">git clone</span> https:<span class="sy0">//</span>git.oeru.org<span class="sy0">/</span>oeru<span class="sy0">/</span>wpms-activity-register.git wpms-activity-register <span class="kw2">sudo</span> <span class="kw2">git clone</span> https:<span class="sy0">//</span>git.oeru.org<span class="sy0">/</span>oeru<span class="sy0">/</span>wpms-mautic.git wpms-mautic</pre></div></div> <p>and the WEnotes plugin has a further dependency, our WEnotes Aggregator code.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="kw3">cd</span> wenotes <span class="kw2">sudo</span> <span class="kw2">git clone</span> https:<span class="sy0">//</span>git.oeru.org<span class="sy0">/</span>oeru<span class="sy0">/</span>wenotes-aggregator.git wenotes-aggregator</pre></div></div> <p>We need ot make sure the plugins are all readable by the webserver, so</p> <p><code>cd .. &amp;&amp; sudo chown -R www-data ../plugins</code></p> <p>And we have to make a couple tweaks to configurations of a couple plugins.</p> <h4><a id="user-content-wenotes-tweaks" href="#wenotes-tweaks" name="wenotes-tweaks" class="heading-permalink" aria-hidden="true" title="Permalink"></a>WEnotes tweaks</h4> <p>You'll want to set your WENOTES_SOURCE_NAME and WENOTES_SOURCE_URL so that your WEnotes are properly attributed.</p> <p>[geshifilter-]cd /home/data/[domain name]/src/wp-content/plugins sudo nano wenotes/wenotes.php [/geshifilter-]</p> <p>and edit the following values (between the ' ') to be suitable for your organisation! Put in your preferred URL as well.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1">define<span class="br0">(</span><span class="st_h">'WENOTES_SOURCE_NAME'</span>, <span class="st_h">'OERu Course Site'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'WENOTES_SOURCE_URL'</span>, <span class="st_h">'course.oeru.org'</span><span class="br0">)</span>;</pre></div></div> <h4><a id="user-content-register-enrol-tweaks" href="#register-enrol-tweaks" name="register-enrol-tweaks" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Register Enrol tweaks</h4> <p>Similarly, you are likely to want to change the following settings in the Register Enrol plugin configuration to reflect your site and organisation. First, edit <code>register-enrol.php</code></p> <p><code>sudo nano register-enrol/register-enrol.php</code></p> <p>and alter the following values (between the ' ') to suit (we don't recommend change other settings unless you know what you're doing!):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre class="de1"><span class="sy0">//</span> support <span class="kw2">link</span> <span class="kw1">for</span> <span class="kw2">users</span> of this plugin... define<span class="br0">(</span><span class="st_h">'ORE_SUPPORT_FORUM'</span>, <span class="st_h">'https://forums.oeru.org/t/register-enrol'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'ORE_SUPPORT_BLOG'</span>, <span class="st_h">'https://course.oeru.org/support/studying-courses/register-enrol/'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'ORE_SUPPORT_CONTACT'</span>, <span class="st_h">'https://oeru.org/contact-us/'</span><span class="br0">)</span>; define<span class="br0">(</span><span class="st_h">'ORE_SUPPORT_PASSWORD_MANAGER'</span>, <span class="st_h">'https://course.oeru.org/lida102/learning-pathways/digital-environments/online-hygiene/#Password_managers'</span><span class="br0">)</span>;</pre></div></div> <p>and</p> <p>define('ORE_DEFAULT_FROM_EMAIL', '<a href="mailto:webmaster@oerfoundation.org">webmaster@oerfoundation.org</a>');</p> <h4><a id="user-content-mautic-integration-tweaks" href="#mautic-integration-tweaks" name="mautic-integration-tweaks" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Mautic Integration tweaks</h4> <p>You might also want to set up an OERu Partner record for your organisation (if you're not already a partner!) in our <a href="https://mautic.com">Mautic</a> integration plugin in the file <code>wpms-mautic/includes/mautic-sync.php</code> in the <code>$partner_names</code> array and then and set your <code>MAUTIC_DEFAULT_PARTNER</code> to have your partner name in <code>wpms-mautic/mautic-app.php</code>.</p> <h4><a id="user-content-reasserting-ownership" href="#reasserting-ownership" name="reasserting-ownership" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Reasserting ownership</h4> <p>And a final step - this is crucial, as it will allow you to keep your WordPress instance up-to-date using the various WordPress standard approaches - is to ensure that the webserver user, <code>www-data</code> is the owner of the source code in your site:</p> <p><code>sudo chown -R www-data /home/data/[domain name]/src</code></p> <p>That's it. Done. Phew.</p> <h2><a id="user-content-step-seven---celebrate" href="#step-seven---celebrate" name="step-seven---celebrate" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Step Seven - celebrate!</h2> <p>You've done it! You've got yourself a new Course WordPress multisite! Which, if I do say so myself, is an impressive accomplishment in it's own right.</p> <p>Of course, it might not be as exciting as actually having a real live course hosted on your Course WordPress multisites... so there's one more step (maybe take a quick break to celebrate getting to this milestone before proceeding).</p> <h2><a id="user-content-snapshotting-your-first-oer-course" href="#snapshotting-your-first-oer-course" name="snapshotting-your-first-oer-course" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Snapshotting your first OER course!</h2> <p>There are quite a few courses (fully accredited!) available to push to your Course multisite on <a href="https://wikieducator.org">WikiEducator</a> where we work with educators around the world to assemble OER-based courses, usually split into discrete 'micro-courses' that can be assembled to meet the credit requirement conventions in different parts of the world.</p> <p>Here is an example course you can add to verify the process: the first micro-course in our (award winning) <a href="">Learning in a digital age</a>, called <a href="https://wikieducator.org/Learning_in_a_digital_age/_Outline_LiDA101">Digital literacies for online learning</a>. That link points to the <em>Outline</em> for the course, which in turn shows all the resources making up the course materials in the hierarchy that will become the navigation of the resulting Course site on your WordPress Multisite. The way e get it there is by using our "<a href="/node/15">Course Snapshot</a>" process which converts that WikiEducator content in the outline into a WordPress content archive that it then pushes onto your designated Course subsite.</p> <p>Before we can run a snapshot, you'll need to</p> <ol><li> <a href="https://wikieducator.org/Special:RequestAccount">request a WikiEducator account</a> - unfortunately this is a moderated process (i.e. a person at the OER Foundation has to review your request) because we have lots of problems with would-be spammers. So this could take a fair while depending on when you request it (we're on NZ time). So much for instant gratification. Sorry about that.</li> <li>you'll need to create a subsite (you don't want to replace your multisite's base site).</li> </ol><p>To do the latter, when you're logged into your WordPress multisite as the admin user, use the top menu to go to 'My Sites' -&gt; 'Network Admin' -&gt; 'Sites' and click the 'Add New' button. You'll need to specify a "Site Address (URL)" which is just the path following [domain name] in referencing your site. For example, the LiDA 101 course on the OERu's Course site is <code>https://course.oeru.org/lida101</code> where <code>lida101</code> is that path. You could use that same path here if you want (it's appropriate for this course).</p> <p>You'll also need a title - you could use "Digital literacies for online learning" - and pick a Site Language (the default is probably what you want). For the 'Admin Email', use the email of your admin user, as WordPress will then make the user with that email, namely your admin user, the administrator of that lida1010 subsite, which is what we want.</p> <p>Next, we go back to the WikiEducator page for the LiDA 101 outline above and find the 'Request snapshot' button near the top of the page. This is the important bit - <strong>Note: doing this will remove any existing content in the subsite you specify! Make sure that's what you're intending</strong> - clicking it will give you a dialog box into which you'll enter the full location of your subsite, which will look like <code>https://[domain name]/[subsite name]</code> (for example, in the OERu case it is <code>https://course.oeru.org/lida101</code>) , making suitable [token] substitutions, of course, and enter as your <em>WordPress</em> admin user and password. Clicking "Push snapshot to WordPress" sets things in motion, and, all going well, you should receive an email in a few minutes (5-20, usually) when the system has got to your request (it does them on a first-come, first-served basis) and processed it successfully. If you get that email, have another look at your site! It should look very similar to what you see on <code>https://course.oeru.org/lida101</code> with the main difference being (possibly) the colour scheme and the lack of a Login/Register prompt (top right). That can be fixed as I explain next.</p> <h2><a id="user-content-enabling-oerus-register-enrol-functionality" href="#enabling-oerus-register-enrol-functionality" name="enabling-oerus-register-enrol-functionality" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Enabling OERu's Register Enrol functionality</h2> <p>The interface the OER Foundation has developed to help learners register for the site and enrol in specific courses is not enabled by default and needs to be turned on for any given Course sub-site (this is useful for courses that are visible on the site, but not yet ready to accept enrolments). To do so, go to the relevant course site dashboard in the WordPress menu, and select Appearance -&gt; Customize. In the resulting theme customisation side panel, selelct 'Site Navigation', and then set 'Show the login option?' to 'Yes' and 'Publish' to save the setting. Your login prompt should show near the top right corner of all the pages <em>in that course site</em> at that point.</p> <p>Also note, you can change the pre-set colour palette for your course sites on a per-site basis using the menu combination OERu Theme -&gt; Colour Scheme...</p> <h2><a id="user-content-data-backups" href="#data-backups" name="data-backups" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Data Backups</h2> <p>Your MariaDB will already be getting backed up daily via the <code>automysqlbackup</code> script installed towards the start of this process, but you also want to have backups of files and configurations on your server, and you want to have <em>backups on a different server</em>, i.e. remote to your server, as a matter of disaster-recovery prudence.</p> <p>We will be describing how we do remote, encrypted, incremental backups in a separate how-to and will link to it here as soon as it's available.</p> <h2><a id="user-content-staying-up-to-date" href="#staying-up-to-date" name="staying-up-to-date" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Staying up-to-date</h2> <p>You should log into your site as your admin user or as your personal user (granted Administrator permissions by your admin user!) periodically to see if there are any updates available for your site - if there are, you can update them as per the instructions provided by the site.</p> <p>The OERu plugins and themes undergo periodic improvements or bug fixes. To find out about them, you'll need to keep track of the various Git repositories on <a href="https://git.oeru.org/explore/projects">our Gitlab instance</a>.</p> <p>These are the relevant repositories:</p> <ul><li> <a href="https://git.oeru.org/oeru/oeru_course">OERu's Course Theme</a> </li> <li> <a href="https://git.oeru.org/oeru/blog-feed-finder">OERu's Blog Feed Finder</a> </li> <li> <a href="https://git.oeru.org/oeru/oeru-h5p-tools">OERu's H5P tools</a> </li> <li> <a href="https://git.oeru.org/oeru/register-enrol">OERu's Register Enrol</a> </li> <li> <a href="https://git.oeru.org/oeru/wenotes">OERu's WEnotes</a> </li> <li> <a href="https://git.oeru.org/oeru/wenotes-aggregator">OERu's WEnotes Aggregator</a> </li> <li> <a href="https://git.oeru.org/oeru/wpms-activity-register">OERu's Activity Register</a> </li> <li> <a href="https://git.oeru.org/oeru/wpms-mautic">OERu's Mautic Integration</a> </li> </ul><p>We're always delighted to <a href="/contact">hear from folks</a> using any of these components and invite people to collaborate with us on improving any and all of these software projects, including providing access to our Gitlab!</p> <p>Also, from time to time, we might update the Docker containers we use for this service, and you're always welcome to make use of our updated containers. If you have any questions, feel free to <a href="/contact">get in touch</a> or leave a comment below!!</p> <h2><a id="user-content-acknowledgements" href="#acknowledgements" name="acknowledgements" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Acknowledgements</h2> <p>Many thanks to the good folks at the Samoan Ministry for Education, Sport, and Culture and UNESCO in Apia for motivating me to write this up, and to educational technologist luminary <a href="https://downes.ca">Stephen Downes</a> for unexpectedly finding this tutorial and then (even more unexpectedly) heroically and comprehensively going through it and providing extensive editorial input (<a href="https://www.youtube.com/watch?v=PBTkZvKf080">part 1</a> and <a href="https://www.youtube.com/watch?v=rqryD1Qi-ws">part 2</a>), most (if not all) of which I've since incorporated! Thanks Stephen - see people were listening (just not in real-time)!</p></div> </div> </div> <section class="field field-node--field-blog-comments field-name-field-blog-comments field-type-comment field-label-above comment-wrapper"> <a name="comments"></a> <div class="comment-form-wrapper"> <h2 class="comment-form__title">Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=38&amp;2=field_blog_comments&amp;3=comment" token="pD7Al-LnIZTaG3QCT3T8JzCIHMbfkyXNmQSpAk-VQmo"></drupal-render-placeholder> </div> </section> Tue, 24 Aug 2021 04:00:22 +0000 dave 38 at http://tech.oeru.org WikiEducator Notes: OERu's course feed aggregation and messaging system http://tech.oeru.org/wikieducator-notes-oerus-course-feed-aggregation-and-messaging-system <span class="field field--name-title field--type-string field--label-hidden">WikiEducator Notes: OERu&#039;s course feed aggregation and messaging system</span> <div class="field field-node--field-blog-tags field-name-field-blog-tags field-type-entity-reference field-label-above"> <h3 class="field__label">Blog tags</h3> <div class="field__items"> <div class="field__item field__item--wenotes"> <span class="field__item-wrapper"><a href="/taxonomy/term/41" hreflang="en">wenotes</a></span> </div> <div class="field__item field__item--couchdb"> <span class="field__item-wrapper"><a href="/taxonomy/term/42" hreflang="en">couchdb</a></span> </div> <div class="field__item field__item--faye"> <span class="field__item-wrapper"><a href="/taxonomy/term/43" hreflang="en">faye</a></span> </div> <div class="field__item field__item--javascript"> <span class="field__item-wrapper"><a href="/taxonomy/term/44" hreflang="en">javascript</a></span> </div> <div class="field__item field__item--nodejs"> <span class="field__item-wrapper"><a href="/taxonomy/term/39" hreflang="en">node.js</a></span> </div> <div class="field__item field__item--docker"> <span class="field__item-wrapper"><a href="/taxonomy/term/16" hreflang="en">docker</a></span> </div> <div class="field__item field__item--docker-compose"> <span class="field__item-wrapper"><a href="/taxonomy/term/25" hreflang="en">docker compose</a></span> </div> <div class="field__item field__item--mastodon"> <span class="field__item-wrapper"><a href="/taxonomy/term/31" hreflang="en">mastodon</a></span> </div> <div class="field__item field__item--hypothesis"> <span class="field__item-wrapper"><a href="/taxonomy/term/45" hreflang="en">hypothesis</a></span> </div> <div class="field__item field__item--oeru"> <span class="field__item-wrapper"><a href="/taxonomy/term/46" hreflang="en">oeru</a></span> </div> <div class="field__item field__item--lida101"> <span class="field__item-wrapper"><a href="/taxonomy/term/47" hreflang="en">lida101</a></span> </div> <div class="field__item field__item--mediawiki"> <span class="field__item-wrapper"><a href="/taxonomy/term/38" hreflang="en">mediawiki</a></span> </div> <div class="field__item field__item--wikieducator"> <span class="field__item-wrapper"><a href="/taxonomy/term/34" hreflang="en">wikieducator</a></span> </div> <div class="field__item field__item--wordpress"> <span class="field__item-wrapper"><a href="/taxonomy/term/35" hreflang="en">wordpress</a></span> </div> </div> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><a title="View user profile." href="/user/1" lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Thu 24/08/2017 - 09:21</span> <div class="field field-node--field-image field-name-field-image field-type-image field-label-hidden has-multiple"> <figure class="field-type-image__figure image-count-1"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-08/WEnotes_diagram.png?itok=_aB7Lt-Y" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A diagram describing the function of WikiEducator Notes (WEnotes)&quot;}" role="button" title="A diagram describing the function of WikiEducator Notes (WEnotes)" data-colorbox-gallery="gallery-field_image-JYvEy6KQPN4" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A diagram describing the function of WikiEducator Notes (WEnotes)&quot;}"><img src="/sites/default/files/styles/medium/public/2017-08/WEnotes_diagram.png?itok=EncLVnTc" width="174" height="220" alt="A diagram describing the function of WikiEducator Notes (WEnotes)" loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-2"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-09/WENotesPostMW.png?itok=sAdOfXNT" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Example of a WEnotes feed and post widget on WikiEducator - MediaWiki&quot;}" role="button" title="Example of a WEnotes feed and post widget on WikiEducator - MediaWiki" data-colorbox-gallery="gallery-field_image-JYvEy6KQPN4" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Example of a WEnotes feed and post widget on WikiEducator - MediaWiki&quot;}"><img src="/sites/default/files/styles/medium/public/2017-09/WENotesPostMW.png?itok=0QqAZWQf" width="220" height="182" alt="Example of a WEnotes feed and post widget on WikiEducator - MediaWiki" loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-3"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-09/WENotesPostWP.png?itok=_58SHpuz" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Example of a WEnotes feed and post widget on Course - WordPress Multisite&quot;}" role="button" title="Example of a WEnotes feed and post widget on Course - WordPress Multisite" data-colorbox-gallery="gallery-field_image-JYvEy6KQPN4" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Example of a WEnotes feed and post widget on Course - WordPress Multisite&quot;}"><img src="/sites/default/files/styles/medium/public/2017-09/WENotesPostWP.png?itok=QQl6B5hF" width="220" height="154" alt="Example of a WEnotes feed and post widget on Course - WordPress Multisite" loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> </div> <div class="clearfix text-formatted field field-node--body field-name-body field-type-text-with-summary field-label-hidden"> <div class="field__items"> <div class="field__item"><p>Here at the OERu, we provide a service, attached to all of our online courses (and available to all of our partners - or anyone else for that matter) which allows anyone involved in those courses to communicate with their peers from any one of a dazzling array of online "places" with WikiEducator Notes (aka WEnotes). The entire system is free and open source software (FOSS). </p> <p>The magic glue is "tags" - most social media and online systems support "tagging" in one way or another. Tools like Twitter, <a href="https://github.com/tootsuite/mastodon" title="A FOSS alternative to Twitter, but distributed rather than centralised. Ruby-on-Rails and React.js are the underlying technology.">Mastodon</a>, and G+ support "hashtags" like "#OERu" as shorthand. Other tools like blog engines (<a href="https://medium.com" title="A proprietary blogging cloud service and aggregator">Medium</a>, <a href="https://wordpress.org" title="The worlds most widely used web platform. FOSS blogging engine used by 25% of websites.">WordPress</a>, and others), forums (like <a href="https://discourse.org" title="A next generation forum engine, FOSS, built on Ruby on Rails and Ember.js for the front end.">Discourse</a>), content annotation services (like <a href="https://web.hypothes.is/" title="FOSS website annotation system - review any web page, sentence by sentence. ">Hypothesis</a>), social bookmarking services (like <a href="http://semanticscuttle.sourceforge.net/" title="The software underlying our https://bookmarks.oeru.org service">Semantic Scuttle</a>), and instant messaging services (like <a href="https://rocket.chat" title="A FOSS private messaging system built on Meteor and React.JS, with MongoDB">Rocket.Chat</a>, <a href="https://about.riot.im/" title="A FOSS React.js-based private messaging client system built on top of the Matrix (matrix.org) messaging standard and the &quot;Synapse&quot; server (Python + Twisted).">Riot</a> + <a href="https://matrix.org/docs/projects/server/synapse.html" title="The Matrix server used by the Riot messaging client. Written in Python + Twisted.">Synapse</a>, <a href="https://zulip.org" title="A FOSS private messaging service originally created by Dropbox for internal use. Written in Python with the Django framework and Electron clients.">Zulip</a>, and <a href="https://about.mattermost.com" title="A FOSS private messaging service built on Go (aka Golang)">Mattermost</a>, among others) support adding  "tags", "labels" or "categories" to any given post. WEnotes is sensitive to those tags, and thanks to the modern trend towards services providing feeds, it can happily "harvest" relevant content from all over the web, in real time (or nearly, depending on the technology).</p> <h2>How WEnotes components fit together</h2> <p>WEnotes is a collection of a significant number of separate FOSS components. See the graphical diagram of WEnotes alongside this article for an overview of the WEnotes component architecture. To provide further explanation, WEnotes is made up of several functional components:</p> <ol><li>A set of feed harvesting scripts that are part of the <a href="https://bitbucket.org/wikieducator/wenotes-tools">WEnotes-tools project</a> (written in Javascript and Python, for those interested in that sort of detail, the former ideal for those sites producing JSON feeds and real time updates, the latter our choice for periodic scans - every 10 minutes by default at the moment - of RSS and ATOM feeds)</li> <li>A central <a href="http://couchdb.apache.org/">CouchDB</a> instance, which stores all posts harvested, along with metadata like their source, a unique ID (to ensure we don't re-harvest it) and some useful info to ensure we can push the messages to subscribers effectively.</li> <li>The WEnotes client for MediaWiki or WordPress, part of the <a href="https://bitbucket.org/wikieducator/wenotes">WEnotes project</a>, which provides a real-time feed of posts, either a full feed (for example <a href="https://wikieducator.org/WENotesCompleteFeed">this one</a>) or specific to a tag (for example a specific <a href="https://course.oeru.org/lida101/interactions/course-feed/">OERu Course</a>). The client also offers a posting interface, allowing logged in users to post new posts (up to 300 characters) automatically tagged in the relevant context. See two screenshots attached.</li> <li>A "publish-subscribe" or "Pub-Sub" service, <a href="https://bitbucket.org/wikieducator/wenotes-server">WEnotes-server project</a>, a Node.JS based <a href="https://faye.jcoglan.com/">Faye</a> implementation of the <a href="https://docs.cometd.org/current/reference/index.html#_bayeux">Bayeux protocol</a>. It uses <a href="https://en.wikipedia.org/wiki/WebSocket">websockets</a> to push out real-time updates to subscribed WEnotes clients.</li> <li> <p>The "couchwatch.js" script, also part of the WEnotes-tools project, watches the CouchDB and alerts Faye whenever a new post is received, which in turn pushes it out to subscribed WENote clients.</p> </li> </ol><h2>Whence WEnotes posts come</h2> <p>Here are three scenarios which outline the process by which a WEnote post is created.</p> <h3>Handy WEnotes post form</h3> <p>The most obvious way to create a WEnotes post is to go to a WEnote feed page - for <a href="https://course.oeru.org/lida101/interactions/course-feed/">instance</a> - and log into the system (assuming you have authentication credentials or can create some). If you're logged in, you should see a simple form as illustrated in the two attached screen shots. The system then knows who you are so that your post can be properly attributed, and it knows the "context" for the feed you're looking at (namely the associated tag). You can enter a message of up to 300 characters, and hit the "Post a WEnote" button, and this will send - direct from your browser - a suitably formatted post (a simple JSON object - example below) into the WEnotes CouchDB where it is then available for inclusion in relevant feed displays.</p> <h3>Personal blog post, appropriately tagged</h3> <p>If you register for our <a href="https://course.oeru.org" title="The main OERu course delivery platform for OERu itself and many partner institutions.">Course site</a>, or any course on that site, you have the option of adding a "Blog URL" - that's the web address of your blog (the address of <em>this </em>blog is <code>https://tech.oeru.org</code>, for instance). Because most widely used blogging platforms, like the open source <a href="https://wordpress.org">WordPress</a> platform (which is the most widely use web platform in the world, the basis for <a href="https://w3techs.com/blog/entry/wordpress-powers-25-percent-of-all-websites">25% of <em>all</em> websites</a>!! For the record, this website uses <a href="https://drupal.org">Drupal</a>, which is the 3rd biggest web platform, and is also open source), and proprietary cloud blogging platforms like Blogger and Medium, provide both "tagging" of posts and a built-in <a href="http://www.whatisrss.com/">RSS feed</a>. When you know what to look for, you'll start to see them all over the place (this blog has <a href="/blog/feed">one</a> - the logo is the little grey icon on the bottom left corner of <a href="/blog" title="The Tech Blog index page.">this page</a>).</p> <p>When you create a blog post on your registered blog and tag it with a course-specific tag (each course we run has one, it's usually the short code immediately after the main course website URL: <code>https://course.oeru.org/[course code here]/...</code>) the WEnotes feed harvesters will periodically visit your blog's RSS feed and look for new posts tagged with a course tag. If it finds one, it will create a new post in CouchDB with all the necessary info to provide an informative post in the WEnotes feed, proper author attribution (and an "avatar" picture of you where available!), and a way for interested readers to get to your original post.</p> <h3>Tagged/Hashtagged social media reference</h3> <p>Because the attention of many people is heavily focused on social media - dominated by "network effect" companies like Twitter and Facebook, for better or worse, but also by an array of more open and egalitarian services (which we, on principle, prefer, but recognise that they're far less well marketed due mostly to not taking anyone's money) - we also provide a variety of social media specific "feed harvesters".  Some of these periodically scan specific social media technologies for new relevant posts, others perpetually monitor their sources providing instant updates.</p> <p>If you</p> <ul><li><a href="https://mastodon.oeru.org" title="OERu's Instance of the open source Mastodon social media platform.">Toot</a> including a relevant hashtag (e.g. #oeru or #lida101 for the "Learning in a Digital Age 101" course),</li> <li>use <a href="https://hypothes.is">Hypothesis</a> to annotate a web page, tagging it appropriately (e.g. oeru or lida101),</li> <li>add a <a href="http://semanticscuttle.sourceforge.net/">Semantic Scuttle</a> "<a href="https://bookmarks.oeru.org" title="Our Semantic Scuttle instance.">Bookmark</a>" to a web reference of interest, tagging it appropriately,</li> <li>add a topic or reply on our <a href="https://community.oeru.org" title="Our Discourse forum for educators and OER developers">Community</a> or <a href="https://forums.oeru.org" title="The OERu's Discourse forum for learners">Forums</a> Discourse instances also suitably tagged, or</li> <li>create a <a href="https://moodle.org/" title="Market leading learning management system (it's also open source).">Moodle</a> post on a Moodle site running a suitable "student bot",</li> </ul><p>the WEnotes feed harvesters will find it and create a suitable post in the CouchDB.</p> <h2>How WEnotes feeds are made</h2> <p>Once you have a bunch of WEnotes posts stored in CouchDB, how do we create a feed? At present, we have "WEnotes client" scripts that run either on MediaWiki instances, like Wikieducator, or on WordPress sites, like our Course multisite, for which content is usually provisioned per-course using our "<a href="/oeru-mediawiki-wordpress-snapshot-toolchain">Snapshot</a>" toolchain. In both cases, the client is invoked via the inclusion of a "Widget" in the MediaWiki content that will either be viewed on the <a href="https://wikieducator.org/WENotesCompleteFeed" title="An example of the WENotes Widget, in this case a full feed, not restricted to one or more tags.">MediaWiki instance itself</a>, or on a target <a href="https://course.oeru.org/lida101/interactions/course-feed/" title="This is an example of the widget being transferred to a WordPress course subsite. Note the 'Content' link at the bottom of the page, which points to the Wikieducator page which contains the actual Widget.">WordPress course subsite</a>.</p> <p>After you log into the Wikieducator site, and create or find a page on which you want a feed, you "Edit source" and you can add both a <a href="https://wikieducator.org/Widget:WEnotesPost" title="The WENotes Post Widget how-to documentation.">WEnotes Post template</a> and <a href="https://wikieducator.org/Widget:WEnotes" title="The WENotes Feed Widget documentation">WEnotes Feed template</a> two simple one line recipes like this:</p> <p><code>{{#widget:WEnotesPost|tag=lida101}}<br /> {{#widget:WEnotes|tag=lida101|count=20}} </code></p> <p>This invocation will create a page showing a WEnotes Post Widget, where any submitted post will automatically be tagged by "lida101", and directly below that, a feed showing up to 20 posts from the CouchDB tagged with "lida101". Note, any <em>new</em> suitably tagged posts will pop into the feed in realtime as they're added to CouchDB thanks to couchwatch.js and Faye - invoking a WEnotes Feed Widget automatically subscribes you to realtime updates for that tag.</p> <p>You can also create a cross-tag feed using the magic <code>"_"</code> designator. WEnotes Posts require a tag (the default, if none is specified, is "wikieducator"):</p> <p><code>{{#widget:WEnotes|tag=_|count=20}} </code></p> <h2>WEnotes technical details</h2> <p>This is a tech-blog, so I'm not going to shy away from the technical details. Here're some useful bits and pieces for folks wanting to get a better understanding of this system. If you're scared of technical details, you can stop reading here.</p> <h3>WEnotes technology infrastructure</h3> <p>We implement the services making up the WEnotes stack using a set of three Docker containers as well as our main MediaWiki implementation, WikiEducator, and our main WordPress multisite implementation, the OERu Course multisite (we also maintain development instance of each of these). The Docker containers are managed with <a href="/docker-compose-better-way-deploy-rocketchat-wekan-and-mongodb">Docker Compose</a> and the roles are divided  We've created a separate <a href="https://github.com/oeru/wenotes-docker">code repository on GitHub </a>for our evolving WEnotes stack to facilitate others making use of some or all of it! The three containers currently being deployed do the following:</p> <ol><li>run CouchDB version 2.0 or later, as well as its "Fauxton" interactive web front-end.</li> <li>run the Faye Pub-Sub websocket destination for realtime feed updates, implemented as a Node.JS service running under <a href="http://pm2.keymetrics.io/">pm2</a>.</li> <li>run the Javascript (using pm2) and Python-based (using cron to run periodically) feed harvesters and the Javascript "couchwatch.js" script which alerts Faye to the addition of new posts.</li> </ol><p>Once configured (we're working on making that a straight forward "out-of-the-box" experience, although it's not quite there yet), the three containers can be deployed together with a single invocation of Docker Compose - at the moment, though, this system is quite dependent on a properly configured "options.json" file, the format of which is still in a state of flux.</p> <h3>WENotes JSON object</h3> <p>A WEnotes post is stored in our CouchDB as a JSON object (JSON = <a href="json.org" title="JSON is rapidly superseding XML as a way of storing and propogating machine-readable structured data. ">JavaScript Object Notation</a>). This is an example of a Post as it's stored in our CouchDB:</p> <p><blockcode>{<br />   "_id": "3e1c16eb1c99ae59899761a680006f4a",<br />   "_rev": "1-7462f14a0515d98b5985b9f4b8c39dd5",<br />   "media_attachments": [<br />     {<br />       "url": "<a href="https://mastodon.oeru.org/system/media_attachments/files/000/000/017/original/c3e85e56142c5c4d.png?1505016473">https://mastodon.oeru.org/system/media_attachments/files/000/000/017/or…</a>",<br />       "text_url": "<a href="https://mastodon.oeru.org/media/jvUoIpj6OAt-e5t-H8k">https://mastodon.oeru.org/media/jvUoIpj6OAt-e5t-H8k</a>",<br />       "preview_url": "<a href="https://mastodon.oeru.org/system/media_attachments/files/000/000/017/small/c3e85e56142c5c4d.png?1505016473">https://mastodon.oeru.org/system/media_attachments/files/000/000/017/sm…</a>",<br />       "meta": {<br />         "small": {<br />           "width": 400,<br />           "size": "400x321",<br />           "aspect": 1.2461059190031152,<br />           "height": 321<br />         },<br />         "original": {<br />           "width": 1148,<br />           "size": "1148x921",<br />           "aspect": 1.246471226927253,<br />           "height": 921<br />         }<br />       },<br />       "type": "image",<br />       "id": 17,<br />       "remote_url": ""<br />     }<br />   ],<br />   "truncated": true,<br />   "reblog": null,<br />   "id": 21,<br />   "in_reply_to_id": null,<br />   "content": "&lt;p&gt;Productive weekend fixing hashtags and instructions for &lt;a href=\"<a href="https://mastodon.oeru.org/tags/lida101">https://mastodon.oeru.org/tags/lida101</a>\" class=\"mention hashtag\" rel=\"tag\"&gt;#&lt;span&gt;LiDA101&lt;/span&gt;&lt;/a&gt; photo challenges . Super cool to see mastodon.oeru,org toots in the course feed. &lt;a href=\"<a href="https://mastodon.oeru.org/tags/lida101photo">https://mastodon.oeru.org/tags/lida101photo</a>\" class=\"mention hashtag\" rel=\"tag\"&gt;#&lt;span&gt;lida101photo&lt;/span&gt;&lt;/a&gt; &lt;a href=\"<a href="https://mastodon.oeru.org/media/jvUoIpj6OAt-e5t-H8k">https://mastodon.oeru.org/media/jvUoIpj6OAt-e5t-H8k</a>\" rel=\"nofollow noopener\" target=\"_blank\"&gt;&lt;span class=\"invisible\"&gt;https://&lt;/span&gt;&lt;span class=\"ellipsis\"&gt;mastodon.oeru.org/media/jvUoIp&lt;/span&gt;&lt;span class=\"invisible\"&gt;j6OAt-e5t-H8k&lt;/span&gt;&lt;/a&gt;&lt;/p&gt;",<br />   "application": null,<br />   "text": "Productive weekend fixing hashtags and instructions for #LiDA101 photo challenges . Super cool to see mastodon.oeru,org toots in the course feed. #lida101photo...",<br />   "profile_url": "<a href="https://mastodon.oeru.org/@mackiwg">https://mastodon.oeru.org/@mackiwg</a>",<br />   "tags": [<br />     {<br />       "url": "<a href="https://mastodon.oeru.org/tags/lida101">https://mastodon.oeru.org/tags/lida101</a>",<br />       "name": "lida101"<br />     },<br />     {<br />       "url": "<a href="https://mastodon.oeru.org/tags/lida101photo">https://mastodon.oeru.org/tags/lida101photo</a>",<br />       "name": "lida101photo"<br />     }<br />   ],<br />   "visibility": "public",<br />   "we_tags": [<br />     "lida101"<br />   ],<br />   "user": {<br />     "screen_name": "mackiwg",<br />     "profile_image_url": "<a href="https://mastodon.oeru.org/system/accounts/avatars/000/000/002/original/1575afda498ac49c.jpg?1494995233">https://mastodon.oeru.org/system/accounts/avatars/000/000/002/original/…</a>",<br />     "name": "Wayne Mackintosh"<br />   },<br />   "spoiler_text": "",<br />   "we_timestamp": "2017-09-10T04:09:05.000Z",<br />   "account": {<br />     "username": "mackiwg",<br />     "display_name": "Wayne Mackintosh",<br />     "statuses_count": 10,<br />     "following_count": 1,<br />     "url": "<a href="https://mastodon.oeru.org/@mackiwg">https://mastodon.oeru.org/@mackiwg</a>",<br />     "locked": false,<br />     "created_at": "2017-04-30T01:51:59.778Z",<br />     "avatar_static": "<a href="https://mastodon.oeru.org/system/accounts/avatars/000/000/002/original/1575afda498ac49c.jpg?1494995233">https://mastodon.oeru.org/system/accounts/avatars/000/000/002/original/…</a>",<br />     "note": "&lt;p&gt;Open sourcing education at OERu&lt;/p&gt;",<br />     "header": "<a href="https://mastodon.oeru.org/headers/original/missing.png">https://mastodon.oeru.org/headers/original/missing.png</a>",<br />     "followers_count": 1,<br />     "avatar": "<a href="https://mastodon.oeru.org/system/accounts/avatars/000/000/002/original/1575afda498ac49c.jpg?1494995233">https://mastodon.oeru.org/system/accounts/avatars/000/000/002/original/…</a>",<br />     "header_static": "<a href="https://mastodon.oeru.org/headers/original/missing.png">https://mastodon.oeru.org/headers/original/missing.png</a>",<br />     "acct": "mackiwg",<br />     "id": 2<br />   },<br />   "favourites_count": 0,<br />   "language": "en",<br />   "url": "<a href="https://mastodon.oeru.org/@mackiwg/21">https://mastodon.oeru.org/@mackiwg/21</a>",<br />   "we_source": "mastodon",<br />   "in_reply_to_account_id": null,<br />   "uri": "tag:mastodon.oeru.org,2017-09-10:objectId=21:objectType=Status",<br />   "reblogs_count": 0,<br />   "sensitive": false,<br />   "mentions": [],<br />   "created_at": "2017-09-10T04:09:58.197Z"<br /> }</blockcode></p> <p>The most crucial things for WEnotes are the values that start with '<code>we_</code>'. They are used by the system which filters and displays WEnotes posts. </p> <h3>The OERu Tag List</h3> <p>The list of tags which OERu's WEnotes looks for changes from time-to-time, usually growing as we add new course tags or other tags of business. Currently, the list is being updated manually, but it is <a href="http://wenotes.oeru.org/resources/feed-sources.json" title="WENotes Tag List">available as a web feed</a> in JSON format. We intend to provide both a simple editing interface for this list and to provide an API to automate adding new tags. Eventually, when we create a new course on our Course multisite, we expect to be able to automatically register the new course "tag" with the tag list.</p> <h2>Credits</h2> <p>The entire underlying mechanics and most of the cleverness of WikiEducator Notes are thanks to the ingenuity and hard work of <a href="https://wikieducator.org/User:JimTittsler">Jim Tittsler</a>, my predecessor as OER Foundation Technical Lead.</p> </div> </div> </div> <section class="field field-node--field-blog-comments field-name-field-blog-comments field-type-comment field-label-above comment-wrapper"> <a name="comments"></a> <div class="comment-form-wrapper"> <h2 class="comment-form__title">Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=16&amp;2=field_blog_comments&amp;3=comment" token="PxBEEjMNnN6bxJwA5uTvkIiXXSU76SK-WRBHv4dgcXc"></drupal-render-placeholder> </div> </section> Wed, 23 Aug 2017 21:21:04 +0000 dave 16 at http://tech.oeru.org OERu MediaWiki to WordPress Snapshot Toolchain http://tech.oeru.org/oeru-mediawiki-wordpress-snapshot-toolchain <span class="field field--name-title field--type-string field--label-hidden">OERu MediaWiki to WordPress Snapshot Toolchain</span> <div class="field field-node--field-blog-tags field-name-field-blog-tags field-type-entity-reference field-label-above"> <h3 class="field__label">Blog tags</h3> <div class="field__items"> <div class="field__item field__item--wikieducator"> <span class="field__item-wrapper"><a href="/taxonomy/term/34" hreflang="en">wikieducator</a></span> </div> <div class="field__item field__item--wordpress"> <span class="field__item-wrapper"><a href="/taxonomy/term/35" hreflang="en">wordpress</a></span> </div> <div class="field__item field__item--multisite"> <span class="field__item-wrapper"><a href="/taxonomy/term/36" hreflang="en">multisite</a></span> </div> <div class="field__item field__item--snapshot"> <span class="field__item-wrapper"><a href="/taxonomy/term/37" hreflang="en">snapshot</a></span> </div> <div class="field__item field__item--mediawiki"> <span class="field__item-wrapper"><a href="/taxonomy/term/38" hreflang="en">mediawiki</a></span> </div> <div class="field__item field__item--nodejs"> <span class="field__item-wrapper"><a href="/taxonomy/term/39" hreflang="en">node.js</a></span> </div> <div class="field__item field__item--php"> <span class="field__item-wrapper"><a href="/taxonomy/term/40" hreflang="en">php</a></span> </div> </div> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><a title="View user profile." href="/user/1" lang="" about="/user/1" typeof="schema:Person" property="schema:name" datatype="" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Fri 16/06/2017 - 11:19</span> <div class="field field-node--field-image field-name-field-image field-type-image field-label-hidden has-multiple"> <figure class="field-type-image__figure image-count-1"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-06/WikiEducatorSnapshotExample.png?itok=RUIJFzD9" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Example of a WikiEducator Outline Page with the Snapshot button&quot;}" role="button" title="Example of a WikiEducator Outline Page with the Snapshot button" data-colorbox-gallery="gallery-field_image-td7JBFb-R3A" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Example of a WikiEducator Outline Page with the Snapshot button&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/WikiEducatorSnapshotExample.png?itok=4cDqr9-n" width="220" height="125" alt="Example of a WikiEducator Outline Page with the Snapshot button" loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-2"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-06/WikiEducatorSnapshotAuthenticationExample.png?itok=y_-fZCbJ" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Example of Snapshot authentication for remote WordPress site&quot;}" role="button" title="Example of Snapshot authentication for remote WordPress site" data-colorbox-gallery="gallery-field_image-td7JBFb-R3A" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Example of Snapshot authentication for remote WordPress site&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/WikiEducatorSnapshotAuthenticationExample.png?itok=iCXP2eXj" width="220" height="125" alt="Example of Snapshot authentication for remote WordPress site" loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-3"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-06/WikiEducatorSnapshotSampleCoursePage.png?itok=4dq_F2B7" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A sample course page showing the adaptive layout (mobile-friendly) interface and navigation.&quot;}" role="button" title="A sample course page showing the adaptive layout (mobile-friendly) interface and navigation." data-colorbox-gallery="gallery-field_image-td7JBFb-R3A" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A sample course page showing the adaptive layout (mobile-friendly) interface and navigation.&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/WikiEducatorSnapshotSampleCoursePage.png?itok=DOT2vvTM" width="220" height="145" alt="A sample course page showing the adaptive layout (mobile-friendly) interface and navigation." loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-4"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-06/WikiEducatorSnapshotSampleCoursePageContentLink.png?itok=Mx5-yzwM" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Sample of the link (#1) back to the WikiEducator page from which this WordPress Course page is derived&quot;}" role="button" title="Sample of the link (#1) back to the WikiEducator page from which this WordPress Course page is derived" data-colorbox-gallery="gallery-field_image-td7JBFb-R3A" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Sample of the link (#1) back to the WikiEducator page from which this WordPress Course page is derived&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/WikiEducatorSnapshotSampleCoursePageContentLink.png?itok=PTtiGfSy" width="220" height="145" alt="Sample of the link (#1) back to the WikiEducator page from which this WordPress Course page is derived" loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-5"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2017-06/WikiEducatorSnapshotSampleCoursePageNarrowLayout.png?itok=RUWJTmk0" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The same Course Page example viewed on a narrower screen, demonstrating the adaptive &quot;mobile-friendly&quot; layout.&quot;}" role="button" title="The same Course Page example viewed on a narrower screen, demonstrating the adaptive &quot;mobile-friendly&quot; layout." data-colorbox-gallery="gallery-field_image-td7JBFb-R3A" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The same Course Page example viewed on a narrower screen, demonstrating the adaptive &quot;mobile-friendly&quot; layout.&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/WikiEducatorSnapshotSampleCoursePageNarrowLayout.png?itok=m5NnfsfY" width="65" height="220" alt="The same Course Page example viewed on a narrower screen, demonstrating the adaptive &quot;mobile-friendly&quot; layout." loading="lazy" typeof="foaf:Image" class="image-style-medium" /> </a> </div> </figure> </div> <div class="clearfix text-formatted field field-node--body field-name-body field-type-text-with-summary field-label-hidden"> <div class="field__items"> <div class="field__item"><p>One of the OERu's most compelling technological capabilities is the set of internally developed open source tools that allow us to automatically transform and inject a collection of MediaWiki content on <a href="https://wikieducator.org">WikiEducator</a> (usually in form of micro-courses) making up a course into a mobile-friendly, easy-to-navigate WordPress site, usually situated on OERu's <a href="https://course.oeru.org">Course WordPress Multisite</a> implementation.</p> <h2>How it works</h2> <p>Educators, usually working with one of our <a href="https://oeru.org/oeru-partners/">OERu partner institutions</a>, can assemble the materials (either written by them or recruited from existing OERs on Wikieducator) into an "outline" which references all the content in one place. If they want to make the course available (either a cohort-based group or a "rolling" enrolment, either affiliated with their institution or under the OERu banner) they can do so either on a subsite on OERu's WordPress instance or some other WordPress, for example one hosted by their institution.</p> <h3>MediaWiki Widget</h3> <p>The process of linking an outline to a WordPress site and pushing a "snapshot" of the course content outlined to the WordPress site is remarkably simple. First, the author of the outline page includes a one line MediaWiki template or "widget" - which is managed by the open source <a href="https://bitbucket.org/wikieducator/wesnapshot" title="Open Source MediaWiki plugin component of the OERu Snapshotting process">OERu MediaWiki plugin</a> (written in <a href="http://php.net">PHP</a>, like the rest of <a href="https://www.mediawiki.org/wiki/MediaWiki">MediaWiki</a>) developed by <a href="http://wikieducator.org/User:JimTittsler">Jim Tittsler</a> - that will look something like this (usually in one line, expanded here for clarity):</p> <p><code>{{ #widget:Snapshot |<br /> url=http://course.oeru.org/shortcode/ |<br /> logo=https://wikieducator.org/images/f/f1/MyInstitutionLogo.png |<br /> link=http://oeru.org/oeru-partners/my-institution-profile/}}</code></p> <p>This defines</p> <ul><li>the URL of the WordPress site, using the "shortcode" the educator has defined for that course (like the institution's course code),</li> <li>a logo image to display at the bottom of each page of the WordPress course site to identify the relevant institution, and</li> <li>the link to which a user will be sent if they click on the logo.</li> </ul><p>There is a <a href="https://wikieducator.org/Widget:Snapshot">handy instruction page</a> for the widget if you want a bit more detail.</p> <h3>Course Snapshot Script</h3> <p>When the page containing this template is saved, the resulting page will have a "Request snapshot" button at the top of it (see attached screenshot). Clicking this will popup a modal dialog box into which the user will be asked to enter their login and password <em>for an admin user on the listed WordPress site</em> (either multisite or stand-alone implementation).</p> <p>If the WordPress site in question accepts these credentials, a new Snapshot task is added to WikiEducator's task list, handing over control to Jim's rather clever open source <a href="https://bitbucket.org/wikieducator/course">Course Snapshot</a> script which runs in the background, and checks the task list ever minute or two. When it identifies as snapshot request, it uses the outline page and WikiEducator's API to grab all the relevant content automatically, and it builds a structured data archive (in a variant of XML) which, when completed, is imported directly into a WordPress site via WordPress' XMLRPC capabilities, replacing any existing content.</p> <p>Note: the default export format used by the Course Snapshot script is a WordPress-importable data object, however for historical reasons, it also has the ability to generate (more manually at this stage) a static, internally linked HTML version of the content.</p> <h3>WordPress Mobile-Friendly Course Site</h3> <p>The resulting WordPress course site uses the OERu-created open source mobile-friendly <a href="https://github.com/oeru/oeru_course">Course WordPress theme</a> - see the sample page screen shot. Each page includes a link back to the original page in WikiEducator from which it was generated - see sample page screen shot with the link identified. Also see the sample image of the same page as it would look on a mobile device with a narrower (lower pixel resolution) screen width.</p> <p>Note: anyone can copy an outline page written by someone else and use it as the starting point for assembling their own version of the course - they will, however, have to have admin rights on the target WordPress site (and log in to it to initiate the process) protecting any existing snapshot sites from being overwritten by someone else's course content.</p> <h3>More examples</h3> <p><a href="http://wikieducator.org/OERu/OERu_authoring_model" title="Examples of courses and course &quot;Outline pages&quot; on WikEducator">Here's a list</a> of some of the currently available course outlines on WikiEducator with corresponding links to the "rendered" WordPress courses so you can see what it looks like.</p> </div> </div> </div> <section class="field field-node--field-blog-comments field-name-field-blog-comments field-type-comment field-label-above comment-wrapper"> <a name="comments"></a> <div class="comment-form-wrapper"> <h2 class="comment-form__title">Add new comment</h2> <drupal-render-placeholder callback="comment.lazy_builders:renderForm" arguments="0=node&amp;1=15&amp;2=field_blog_comments&amp;3=comment" token="Su39uEtmGb5TGwGIb3Eyr4NoVO4Ofw4MyngXZefYMqA"></drupal-render-placeholder> </div> </section> Thu, 15 Jun 2017 23:19:06 +0000 dave 15 at http://tech.oeru.org