let&#039;s encrypt http://tech.oeru.org/ en Creating your own OER Foundation-style Libre Self-hosting Infrastructure with Docker Compose and Ubuntu LTS http://tech.oeru.org/creating-your-own-oer-foundation-style-libre-self-hosting-infrastructure-docker-compose-and-ubuntu <span class="field field--name-title field--type-string field--label-hidden">Creating your own OER Foundation-style Libre Self-hosting Infrastructure with Docker Compose and Ubuntu LTS</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--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--nginx"> <span class="field__item-wrapper"><a href="/taxonomy/term/30" hreflang="en">nginx</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 class="field__item field__item--postfix"> <span class="field__item-wrapper"><a href="/taxonomy/term/66" hreflang="en">postfix</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> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><a title="View user profile." href="/user/1" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Thu 12/10/2023 - 13:12</span> <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"><ul class="table-of-contents"><li> <p><a href="#tips-for-this-tutorial">Tips for this tutorial</a></p> </li> <li> <p><a href="#create-a-virtual-private-server">Create a Virtual Private Server</a></p> <ul><li> <p><a href="#vps-properties">VPS Properties:</a></p> </li> </ul></li> <li> <p><a href="#key-variables-for-you-vps">Key variables for you VPS</a></p> <ul><li> <p><a href="#get-your-domain-lined-up">Get your Domain lined up</a></p> </li> </ul></li> <li> <p><a href="#editing-files">Editing files</a></p> <ul><li> <p><a href="#set-up-an-unprivileged-user-for-yourself">Set up an unprivileged user for yourself</a></p> </li> </ul></li> <li> <p><a href="#configure-the-vps">Configure the VPS</a></p> <ul><li> <p><a href="#configuring-your-firewall">Configuring your firewall</a></p> </li> <li> <p><a href="#install-the-nginx">Install the Nginx</a></p> </li> </ul></li> <li> <p><a href="#outgoing-vps-email-optional">Outgoing VPS Email (optional)</a></p> </li> <li> <p><a href="#installing-the-docker-engine-docker-compose-and-lets-encrypt">Installing the Docker Engine, Docker Compose, and Let's Encrypt</a></p> <ul><li> <p><a href="#backwards-compatibility-for-docker-compose">Backwards compatibility for Docker Compose</a></p> </li> <li> <p><a href="#docker-use-by-non-root-user">Docker use by non-root user</a></p> </li> </ul></li> <li> <p><a href="#setting-up-places-for-your-docker-configurations-and-persistent-data">Setting up places for your Docker configurations and persistent Data</a></p> </li> <li> <p><a href="#configuring-nginx-reverse-proxy-for-your-service">Configuring Nginx reverse proxy for your service</a></p> </li> <li> <p><a href="#lets-encrypt-setup">Let's Encrypt setup</a></p> <ul><li> <p><a href="#requesting-lets-encrypt-certificates">Requesting Let's Encrypt certificates</a></p> </li> </ul></li> </ul><h2><a id="user-content-tips-for-this-tutorial" href="#tips-for-this-tutorial" name="tips-for-this-tutorial" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Tips for this tutorial</h2> <p>This tutorial is aimed at adventuresome would-be system administrators. I try not to assume any specialised knowledge on your part, and try to provide useful tips and explanation along the way to help you build a valid mental model of what you're doing. At the same, this is not a trivial process. Luckily, if you try it out, and decide not to follow through, so long as you <em>delete your VPS</em>, you should not be out-of-pocket by more than a few cents.</p> <p>With this tutorial, I'm assuming you've got a computer with an Internet connection, that can run SSH (all modern systems should do that) and you can copy-and-paste stuff from this tutorial (in your browser) into either a terminal window (in which you're SSH'd into your VPS) or into a text editor. Note, if you find it difficult to paste into a terminal window, try using CTRL+SHIFT+V (CTRL+V is already used as a short-cut for something else in UNIX terminals since long before the Windows world started using CTRL+C and CTRL+V).</p> <p>When I provide files you need to copy, look for the 'tokens' or placeholders with values you need to substitute (search-and-replace) in square brackets like this, [token name], with your own values. Any text editor worth using should let you do that!</p> <h2><a id="user-content-create-a-virtual-private-server" href="#create-a-virtual-private-server" name="create-a-virtual-private-server" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Create a Virtual Private Server</h2> <p>The first step is to create a place to host instances of your own <a href="/node/58">LibreSoftware</a> services.</p> <p>You can use a local piece of computing hardware of sufficient capacity in your own home or organisation (e.g. a redundant depreciated desktop should be sufficiently powerful), recognising that it needs to be reliable because anyone using your solution will not be able to access it if it's <em>not running</em>. But, doing that is no joke - making reliable systems is <em>hard</em>.</p> <p>The more cost-effective self-hosting approach in our experience is to lease a low cost commodity Linux Virtual Private Server (aka a VPS) running Ubuntu Linux 22.04 (or the latest "Long Term Support" version - the next one, 24.04, will be released in April 2024). That's what we assume you're running for all our recent tutorials.</p> <p>We have used quite a few Linux VPSs commodity providers. Known good options include Digital Ocean, Linode, Vultr, Hetzner, and TurnkeyLinux. There are many (hundreds of) other credible options. We recommend you find one hosted in the network epicentre (which isn't necessarily the same as the 'geographic' epicentre) of your audience. In the past year, we shifted almost all of our hosting to Hetzner as they've got the benefit of not being US-owned (They're German, and are therefore less likely to expose us to the egregiously over-reaching US <a href="https://en.wikipedia.org/wiki/CLOUD_Act">Cloud</a> and <a href="https://proprivacy.com/guides/what-is-the-partiot-act">Patriot</a> Acts). Shifting to Hetzner also reduced our infrastructure costs by about 50% compared to Digital Ocean. It also represents a <strong>95% savings</strong> over Amazon AWS or Microsoft Azure hosting (can't imagine how anyone could justify hosting with either of them).</p> <p>If you have trouble getting a VPS, you might find <a href="https://vimeo.com/684028258">this video</a> I created for provisioning a VPS, using Digital Ocean as an example, useful. In my experience, the process for provisioning VPSs on other platforms is very similar. You'll find this process <em>much</em> easier than using either Microsoft Azure or Amazon AWS, which we strongly recommend against using.</p> <h3><a id="user-content-vps-properties" href="#vps-properties" name="vps-properties" class="heading-permalink" aria-hidden="true" title="Permalink"></a>VPS Properties:</h3> <p>We recommend that you provision a VPS with the following spec - these will suffice capacity-wise for all the tutorials we provide, although depending on your load, you might want to beef them up, which you can do as the need arises - ou should be able to upgrade those specs in real time if required, except for your disk space. You can, however, provision a secondary storage space (you can start small and increase it as you need to). I will cover setting this up, as it'll make your life far far easier in the medium-long term.</p> <ul><li>4-8 GB RAM - system volatile memory for running applications</li> <li>2-4 Virtual CPUs - processing capacity</li> <li>80-160 GB Disk space (NVMe storage is faster than SSD which is faster than spinning disk space) - long term storage for data</li> <li>running Ubuntu Linux 22.04 (the current Long Term Support version) - the operating system</li> <li>extra storage - 20-40GB extra space (can be expanded on fairly short notice) - a long term storage option separate to your operating system drive - this is very very useful in the event that your app inadvertently fills your hard drive. You'll thank yourself for doing this (I an assure you from bitter experience). It's always a good idea to learn from others' past mistakes rather than repeat them!</li> </ul><p>You'll need to create an account for yourself on your chosen hosting provider (it's a good idea 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 22.04 (or the most recent 'Long Term Support' (LTS) version) in the 'zone' nearest to you (or your primary audience, if that's different).</p> <p>If you don't already have an SSH key on your computer, I encourage you to <a href="https://helpdeskgeek.com/how-to/how-to-generate-ssh-keys-on-windows-mac-and-linux/">create one</a> and specify the <strong>public key</strong> in the process of creating your server - specifying the 'public key' of your SSH identity during the server creation process that should allow you to log in without needing a password!</p> <p>You'll need to note the server's <strong>IPv4</strong> 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 <strong>IPv6</strong> address, which will be a set of 8 four <em>hex character</em> 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 should be able to <a href="https://www.digitalocean.com/community/tutorials/how-to-use-ssh-to-connect-to-a-remote-server-in-ubuntu">log into your new server via SSH</a>. If you're on a UNIX command line (e.g. a Linux or MacOS desktop), do this in a terminal (On Windows, I understand people use a tool called Putty for SSH, in which case follow the app's instructions - or you'll find many tutorials on using it with a quick web search):</p> <p><code>ssh [your server IPv4 or IPv6]</code></p> <p>followed by the ENTER key (that'll be true for any line of commands I provide).</p> <p>In some cases, depending on your hosting provider, you'll have a password to enter, or if you've specified your pre-existing public SSH key, you shouldn't need to enter a password at all, you should be logged in. To check what user you care, you can type</p> <p><code>whoami</code></p> <p>If it returns <code>root</code> (there's also a convention of using a '#' as the command prompt), you're the root or super-admin of the server. If not, you're a normal user (some hosting providers have a convention of giving you a default user called "ubuntu" or perhaps "debian") with a prompt that is, by convention, a '$'.</p> <p>Now that you're logged in, it's worth doing an upgrade of your server's Ubuntu system! Do that as follows (this works regardless of whether your a root user or an unprivileged user with 'sudo' ability):</p> <p><code>sudo apt update &amp;&amp; sudo apt dist-upgrade</code></p> <p>Usually the user, even if it's not the root user, will have the ability to use the <code>sudo</code> command modifier - that means "<em>do</em> this action as the root (aka the 'Super User', thus '<em>su</em>' in 'sudo' for short) user" - if you're a non-root user, you'll likely be asked to enter your password as a security precaution the first time you run a command prefaced by <code>sudo</code>. Enter it, and it should run the command. Plus, the system shouldn't bother you for it again unless you leave your terminal unused for a while (usually 5 minutes) and come back to it.</p> <p>At this point, I also like to install a cool software package called 'etckeeper' which records configuration changes on your VPS for future reference (it can be life-saving if trying to recover from an administrative mess-up!):</p> <p><code>sudo apt install etckeeper git</code></p> <p>which will also install some dependencies, including the very important (and relevant later on) 'git' version control system.</p> <h2><a id="user-content-key-variables-for-you-vps" href="#key-variables-for-you-vps" name="key-variables-for-you-vps" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Key variables for you VPS</h2> <p>To set up your services, you'll need a few crucial bits of information related to your system's identity and external systems you'll need it to interact with. For example, as mentioned before, you'll need a domain name. For the rest of this tutorial, we'll use the convention of representing those variables as a name inside [], for example, the domain name you've picked, [domain name].</p> <p>Here's a list of variables you'll need to know to complete the rest of this tutorial:</p> <ul><li> <strong>[ipv4]</strong> and <strong>[ipv6]</strong> - your VPS' IPv4 and IPv6 addresses (the latter can be ignored if your cloud provider doesn't support IPv6 addresses) as described above.</li> <li> <strong>[domain name]</strong> - the fully qualified domain names or subdomains of a base <strong>[domain name]</strong> by which you want your services to be accessed. You must have full domain management ability on this domain. Example: nextcloud.oeru.org - that's the nextcloud subdomain of the oeru.org domain.</li> <li>Authenticating SMTP details - this is required so your services can send emails to users - crucial things like email address validation and password recovery emails... <ul><li> <strong>[smtp server]</strong> - the domain name or IPv4 or IPv6 address of an SMTP server</li> <li> <strong>[smtp port]</strong> - the port number on the server that is listening for your connection. By convention it's likely to be 465 or 587, or possibly 25.</li> <li> <strong>[smtp reply-to-email]</strong> - a monitored email to which people can send email related to this WordPress site, e.g. notifications@<strong>[domain name]</strong> </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> <strong>[your email]</strong> - an email address to which system-related emails can be sent to you, perhaps something like webmaster@[domain name].</li> <li> <strong>[vps username]</strong> - the username you use on your server (by convention, these are one word, and all lower case).</li> </ul><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>Once you have selected and registered your domain, you can 'manage your Zone' to set up (usually through a web interface provided by the registrar) an <strong>A Record</strong> which associates your website's name to the <strong>IPv4</strong> 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 the web service you want to set up.</p> <p>Nowadays, <em>if your Domain Name host offers it (some don't, meaning you might be better off with a different one),</em> it's also important to define an <strong>IPv6</strong> record, which is called an <strong>AAAA Record</strong>... 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 registrar's interface requests... but in most cases that'll be set to a default of an hour automatically.</p> <h2><a id="user-content-editing-files" href="#editing-files" name="editing-files" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Editing files</h2> <p>In the rest of this tutorial, we're going to be editing quite a few files via the command line. If you're new to this, I recommend using the 'nano' text editor which is installed by default on Ubuntu Linux systems. It's fairly simple, and all of its options are visible in the text-based interface. I tend to use a far more powerful but far less beginner-friendly editor called 'vim'. There're other editors people might choose, too. To use your preferred editor for the rest of the tutorial, enter the following to set an environment variable EDIT, specifying your preferred editor, e.g.:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>EDIT</span>=$<span>(</span><span>which</span> <span>nano</span><span>)</span></pre></div></div> <p>or, if you're like me</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>EDIT</span>=$<span>(</span><span>which</span> <span>vim</span><span>)</span></pre></div></div> <p>so that subsequent references to $EDIT will invoke your preferred editor. Note the command <code>$(which nano)</code> is a script which finds the full path to the named command, in this case 'nano'. Putting a command inside the $() means 'replace with the value the script returns', so it sets the value of EDIT to the path of the nano command in this case. On my current machine, the value it returns is <code>/usr/bin/nano</code>, which is pretty typical.</p> <p>To test (at any time) whether you session still knows your $EDIT command, run</p> <p><code>echo $EDIT</code></p> <p>if it returns the path to your preferred editor, you're good to go. If not, just reassert (run again) the EDIT= line from above!</p> <p><em>Note: if you log out and back in again, change users, or create a new terminal tab/session, you'll need to reassert the EDIT value.</em></p> <h3><a id="user-content-set-up-an-unprivileged-user-for-yourself" href="#set-up-an-unprivileged-user-for-yourself" name="set-up-an-unprivileged-user-for-yourself" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up an unprivileged user for yourself</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.</p> <p>This will log you into your server as it did the first time, either as 'root' or the default unprivileged user. It's not considered good practice to access your server as root (it's too easy to completely screw it up by accident). It's a good idea to create your own 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 [vps username] with your chosen username - in my case, I'd use <code>U=dave</code>):</p> <p><code>U=[vps username]</code><br /><code>adduser $U</code><br /><code>adduser $U ssh</code><br /><code>adduser $U admin</code><br /><code>adduser $U sudo</code></p> <p>You'll also want to a set a password for user [vps username] (we have a tutorial on <a href="/node/43">creating good passwords</a>):</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' - you can show them in a directory listing via <code>ls -a</code>) for the file into which to put your public SSH key:</p> <p><code>su $U</code></p> <p>after which you need to re-run your EDIT command: <code>EDIT=$(which nano)</code></p> <p>and then run <code>ssh-keygen -t rsa -b 2048</code><br /><code>$EDIT ~/.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>and then leave the 'su' state, back to the superuser:</p> <p><code>CTRL+D</code> or type <code>exit</code></p> <p>From that point, you should be able to SSH to your server via <code>ssh [vps 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' (the 'sudo' will be ignored as it's unnecessary).</p> <h2><a id="user-content-configure-the-vps" href="#configure-the-vps" name="configure-the-vps" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configure the VPS</h2> <p>First things first. Let's make sure you've got the time zone set appropriately for your instance. It'll probably default to 'UTC' (Greenwich Mean Time). For our servers, I tend to pick 'Pacific/Auckland' which is our time zone. Run this</p> <p><code>sudo dpkg-reconfigure tzdata</code></p> <p>and pick the appropriate timezone. You can just leave it running UTC, but you might find it tricky down the track if, for example, you're looking at logs and having to constantly convert the times into your timezone.</p> <h3><a id="user-content-configuring-your-firewall" href="#configuring-your-firewall" name="configuring-your-firewall" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring your firewall</h3> <p>In the name of safety from the get-go, let's configure our firewall. We work on the basis of explicitly allowing in <em>only</em> what we want to let in (i.e. a 'default deny' policy).</p> <p>First we'll enable the use of SSH through the firewall (<em>not doing this could lock us out of your machine!</em>)</p> <p><code>sudo ufw allow ssh</code><br /></p> <p>while we're here, we'll also enable data transfer from the internal (to the VPS) Docker virtual network and the IP range it uses for Docker containers:</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>Then we'll enable forwarding from internal network interfaces as required for Docker containers to be able to talk to the outside world:</p> <p><code>sudo $EDIT /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><span>#DEFAULT_FORWARD_POLICY="DROP"</span> <span>DEFAULT_FORWARD_POLICY</span>=<span>"ACCEPT"</span></pre></div></div> <p>and then save and exit the file (CTRL-X and then 'Y' if your editor is nano).</p> <p>You also have to edit <code>/etc/ufw/sysctl.conf</code></p> <p><code>sudo $EDIT /etc/ufw/sysctl.conf</code></p> <p>and remove the "#" at the start of the following lines, so they look like this:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Uncomment this to allow this host to route packets between interfaces</span> net<span>/</span>ipv4<span>/</span><span>ip_forward</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>default<span>/</span><span>forwarding</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>all<span>/</span><span>forwarding</span>=<span>1</span></pre></div></div> <p>Then we need to restart the network stack to apply that configuration change</p> <p><code>sudo systemctl restart systemd-networkd</code></p> <p>(on older Ubuntu systems this would have been done via <code>sudo service networking restart</code>...)</p> <p>Next we have to enable the (default on Ubuntu) UFW firewall to start at boot time to keep your machine relatively safe.</p> <p><code>sudo $EDIT /etc/ufw/ufw.conf</code></p> <p>And set the ENABLED variable near the top:</p> <p><code>ENABLED=yes</code></p> <p>Now you can formally start UFW now:</p> <p><code>sudo ufw enable</code></p> <h3><a id="user-content-install-the-nginx" href="#install-the-nginx" name="install-the-nginx" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install the Nginx</h3> <p>Next we need to install the Nginx web server and reverse-proxy, as well as the Let's Encrypt SSL certificate generator, both of which are crucial for any secure web services you might want to host. Nginx is a more efficient and flexible alternative to the older Apache web server you might've seen elsewhere (Nginx recently surpassed Apache as the most widely used web server on the Internet).</p> <p><code>sudo apt install nginx-full letsencrypt ssl-cert</code></p> <p>You'll get a couple pop-up windows in your terminal, just hit ENTER to accept the defaults. Having installed it, we need to create firewall rules to allow external services to see it:</p> <p><code>sudo ufw allow 'Nginx Full'</code></p> <p>You can check if the firewall rules you requested have been enabled:</p> <p><code>sudo ufw status</code></p> <h2><a id="user-content-outgoing-vps-email-optional" href="#outgoing-vps-email-optional" name="outgoing-vps-email-optional" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Outgoing VPS Email (optional)</h2> <p>Although it's not absolutely necessary (you can do this section later if you're in a big hurry), it's very useful for your server to be able to send out emails, like status emails to administrators (perhaps you) about things requiring their attention, e.g. the status of backups, pending security updates, expiring SSL certificates, etc.</p> <p>To do this, we'll set up the industrial strength Postfix SMTP server, which is pretty quick and easy. First we install Postfix and a command line mail client for testing purposes.</p> <p><code>sudo apt install postfix bsd-mailx</code></p> <p>During the install, you'll be asked to select a bunch of configuration parameters. Select the defaults except:</p> <ul><li>Select "Internet Site with Smarthost",</li> <li>fill in the domain name for your server [domain name],</li> <li>the [smtp server] name and [smtp port] (in the form [smtp server]:[smtp port], e.g. smtp.oeru.org:587 ) of your "smarthost" who'll be doing the authenticating SMTP for you, and</li> <li>the email address to which you want to receive system-related messages, [your email].</li> </ul><p>After that's done, we set a default address for the server to mail to, to [your email] selected above. First</p> <p><code>sudo $EDIT /etc/aliases</code></p> <p>We need to make sure the "root" user points to a real email address. Add a line at the bottom which says (replacing [your email] with your email :) )</p> <p><code>root: [your email]</code></p> <p>After which you'll need to convert the aliases file into a form that postfix can process, simply by running this:</p> <p><code>sudo newaliases</code></p> <p>Then we have to define the authentication credentials required to convince your mail server that you're you!</p> <p><code>sudo $EDIT /etc/postfix/relay_password</code></p> <p>and enter a single line in this format:</p> <p><code>[smtp server] [smtp user]:[smtp password]</code></p> <p>as an example, this is more or less what I've got for my system. Note that the [smtp user] in my case is an email address (this is common with many smtp system - the user is the same as the email address):</p> <p><code>smtp.oeru.org smtp-work@fossdle.org:YourObscurePassw0rd</code></p> <p>then save the file and, like the aliases file, run the conversion process (which uses a slightly different mechanism):</p> <p><code>sudo postmap /etc/postfix/relay_password</code></p> <p>Finally, we'll edit the main configuration file for Postfix to tell it about all this stuff:</p> <p>sudo $EDIT /etc/postfix/main.cf</p> <p>If your SMTP server uses port 25 (the default for unencrypted SMTP) you don't have to change anything, although most people nowadays prefer to use StartTLS or otherwise encrypted transport to at least ensure that your SMTP authentication details (at least) are transferred encrypted. That means using port 587 or 465. If you're using either of those ports, find the "relayhost = [your server name]" line... and add your port number after a colon, like this</p> <p><code>relayhost = [smtp server]:[smtp port]</code></p> <p>or, for example:</p> <p><code>relayhost = smtp.oerfoundation.org:465</code></p> <p>Then we have to update the configuration for Postfix to ensure that it knows about the details we've just defined (this command will automatically back up the original default configuration so you can start from scratch with the template below):</p> <p><code>sudo mv /etc/postfix/main.cf /etc/postfix/main.cf.orig &amp;&amp; sudo $EDIT /etc/postfix/main.cf</code></p> <p>You can just copy-and-paste the following into it, substituting your specific values for the [tokens]. Note: the IPv6 designations in the line <code>mynetworks = 127.0.0.0/8 [::ffff:127.0.0.0]/104 [::1]/128</code> are <em>not</em> tokens - you can leave those unchanged.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># See /usr/share/postfix/main.cf.dist for a commented, more complete version</span>   <span># Debian specific: Specifying a file name will cause the first</span> <span># line of that file to be used as the name. The Debian default</span> <span># is /etc/mailname.</span> <span>#myorigin = /etc/mailname</span>   smtpd_banner = <span>$myhostname</span> ESMTP <span>$mail_name</span> <span>(</span>Ubuntu<span>)</span> biff = no   <span># appending .domain is the MUA's job.</span> append_dot_mydomain = no   <span># Uncomment the next line to generate "delayed mail" warnings</span> <span>#delay_warning_time = 4h</span> readme_directory = no   <span># See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on</span> <span># fresh installs.</span> compatibility_level = <span>3.6</span>   <span># TLS parameters</span> <span>smtpd_tls_cert_file</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem <span>smtpd_tls_key_file</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key <span>smtpd_tls_security_level</span>=may   <span>smtp_tls_CApath</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>certs <span>#smtp_tls_security_level=may</span> smtp_tls_session_cache_database = btree:<span>${data_directory}</span><span>/</span>smtp_scache   smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination myhostname = <span>[</span>domain name<span>]</span> alias_maps = hash:<span>/</span>etc<span>/</span>aliases alias_database = hash:<span>/</span>etc<span>/</span>aliases myorigin = <span>/</span>etc<span>/</span>mailname mydestination = <span>$myhostname</span>, localhost relayhost = <span>[</span>smtp server<span>]</span>:<span>[</span>smtp port<span>]</span> mynetworks = 127.0.0.0<span>/</span><span>8</span> <span>[</span>::ffff:127.0.0.0<span>]</span><span>/</span><span>104</span> <span>[</span>::<span>1</span><span>]</span><span>/</span><span>128</span> mailbox_size_limit = <span>0</span> recipient_delimiter = + inet_interfaces = all inet_protocols = all   <span># added to configure accessing the relay host via authenticating SMTP</span> smtp_sasl_auth_enable = <span>yes</span> smtp_sasl_password_maps = hash:<span>/</span>etc<span>/</span>postfix<span>/</span>relay_password smtp_sasl_security_options = noanonymous smtp_tls_security_level = encrypt   <span># if you're using Ubuntu prior to 20.04, uncomment (remove the #) the</span> <span># earlier line smtp_tls_security_level = may to save errors in 'postfix check'</span> <span># and comment this line (by adding a # at the start)</span> smtp_tls_wrappermode = <span>yes</span></pre></div></div> <p>Once you've created that <code>main.cf</code> file, you can double check that your config is valid:</p> <p><code>sudo postfix check</code></p> <p>and if it's all ok, you can get Postfix to re-read its configuration:</p> <p><code>sudo postfix reload</code></p> <p>You can then try sending an email so see if it works!</p> <p>By default, a command line application called "mail" is installed as part of the bsd-mailx package we installed alongside postfix. You can use it to send test email from the command line on your host to verify you've got things working correctly! The stuff in &lt;&gt; are the keys to hit at the end of the line...</p> <p><code>mail you@email.domain&lt;ENTER&gt;</code></p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Subject: Testing from your.relay.server.domain<span>&lt;</span>ENTER<span>&gt;</span> Testing postfix remote host<span>&lt;</span>ENTER<span>&gt;</span> <span>&lt;</span>CTRL-D<span>&gt;</span> Cc:<span>&lt;</span>ENTER<span>&gt;</span></pre></div></div> <p>Typing (hold down the Control or Ctrl key on your keyboard and press the "d" key) will finish your message, showing you a "CC:" field, in which you can type in other email addresses if you want to test sending to multiple addresses. When you then hit , it will attempt to send this email. It might take a few minutes to work its way through to the receiving email system (having to run the gauntlet of spam and virus filters on the way).</p> <p>You can also always check the postfix system logs to see what postfix thinks about it using the command:</p> <p><code>sudo less +G /var/log/mail.log</code></p> <p>if your system doesn't have a <code>/var/log/mail.log</code>, never fear! Try this instead:</p> <p><code>sudo less +G /var/log/syslog</code></p> <p>In either case, hit to have the log update in real time.</p> <h2><a id="user-content-installing-the-docker-engine-docker-compose-and-lets-encrypt" href="#installing-the-docker-engine-docker-compose-and-lets-encrypt" name="installing-the-docker-engine-docker-compose-and-lets-encrypt" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Installing the Docker Engine, Docker Compose, and Let's Encrypt</h2> <p>First let's install the Docker Engine, which (these days) comes with Docker Compose and the <a href="http://letsencrypt.org/">Let's Encrypt</a> scripts that let you procure no-cost Secure Sockets Layer certificates to secure access to your server. You can follow the <a href="https://docs.docker.com/engine/install/ubuntu/">official Docker Engine install instructions</a>, but I've summarised them here (If the following doesn't work for you, go back to the official instructions, because something might've changed since I wrote this).</p> <p>First we want to make sure no old Docker engines are installed on this server (this probably won't do anything, but no harm in running it):</p> <p><code>for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do sudo apt-get remove $pkg; done</code></p> <p>Second, we want to set up Docker's 'APT' repository, so you can keep Docker up-to-date with their latest versions (usually more up-to-date than those shipped with Ubuntu):</p> <p>First we Add Docker's official GPG key (copy and paste all of this at your command line):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>sudo</span> <span>apt-get update</span> <span>sudo</span> <span>apt-get install</span> ca-certificates curl gnupg <span>sudo</span> <span>install</span> <span>-m</span> 0755 <span>-d</span> <span>/</span>etc<span>/</span>apt<span>/</span>keyrings curl <span>-fsSL</span> https:<span>//</span>download.docker.com<span>/</span>linux<span>/</span>ubuntu<span>/</span>gpg <span>|</span> <span>sudo</span> gpg <span>--dearmor</span> <span>-o</span> <span>/</span>etc<span>/</span>apt<span>/</span>keyrings<span>/</span>docker.gpg <span>sudo</span> <span>chmod</span> a+r <span>/</span>etc<span>/</span>apt<span>/</span>keyrings<span>/</span>docker.gpg</pre></div></div> <p>Then we have to add the repository to Apt our system's sources and install the Docker Engine:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>echo</span> \ <span>"deb [arch="</span>$<span>(</span><span>dpkg</span> --print-architecture<span>)</span><span>" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu <span>\ </span> "</span>$<span>(</span>. <span>/</span>etc<span>/</span>os-release <span>&amp;&amp;</span> <span>echo</span> <span>"<span>$VERSION_CODENAME</span>"</span><span>)</span><span>" stable"</span> <span>|</span> \ <span>sudo</span> <span>tee</span> <span>/</span>etc<span>/</span>apt<span>/</span>sources.list.d<span>/</span>docker.list <span>&gt;</span> <span>/</span>dev<span>/</span>null <span>sudo</span> <span>apt-get update</span> <span>sudo</span> <span>apt-get install</span> docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin</pre></div></div> <p>With this approach, any updates made by the Docker community will be installed as part of your regular server upgrades.</p> <p>Having installed the most recent release of the Docker engine, you should find that you've now got Docker Compose built in. Test that by running</p> <p><code>docker compose version</code></p> <p>which should return something like</p> <p><code>Docker Compose version v2.18.1</code></p> <p>If that's the case, superb, everything's great.</p> <h3><a id="user-content-backwards-compatibility-for-docker-compose" href="#backwards-compatibility-for-docker-compose" name="backwards-compatibility-for-docker-compose" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Backwards compatibility for Docker Compose</h3> <p>Since version 2 of the Docker Engine, the 'Docker Compose' capability has been incorporated into the base system. Prior to version 2, using Docker Compose required installing a separate app, and it was run by typing <code>docker-compose</code> rather than <code>docker compose</code>... so something I do now, to accommodate my muscle memory of typing <code>docker-compose</code> is to create a tiny script that lets me keep using that command, but have it call, instead, the new <code>docker compose</code> functionality. I do this via</p> <p><code>sudo $EDIT /usr/local/bin/docker-compose</code></p> <p>into which I put the following:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>#!/bin/bash</span> <span>D</span>=<span>`</span><span>which</span> docker<span>`</span> <span>$D</span> compose <span>"$@"</span></pre></div></div> <p>After saving that, we have to make the script 'executable' via</p> <p><code>sudo chmod a+x /usr/local/bin/docker-compose</code></p> <p>So you should now be able to run</p> <p><code>docker-compose version</code></p> <p>and get the same result that you did above for <code>docker compose version</code>...</p> <h3><a id="user-content-docker-use-by-non-root-user" href="#docker-use-by-non-root-user" name="docker-use-by-non-root-user" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Docker use by non-root user</h3> <p>If the above docker commands didn't work for you... and if you want to run Docker commands without being the root user or using <code>sudo</code>, as we usually do, you need to do a few more steps...</p> <ol><li> <p>create a 'docker' group on your system (this might already exist, but doing this again won't hurt): <code> sudo groupadd docker</code></p> </li> <li> <p>add your user to it: <code>sudo usermod -aG docker $USER</code></p> </li> <li> <p>refresh your shell so that it recognises your user's membership in the docker group: <code>newgrp docker</code></p> </li> </ol><p>You should now be to run a test as a non-root user <code>docker run hello-world</code></p> <h2><a id="user-content-setting-up-places-for-your-docker-configurations-and-persistent-data" href="#setting-up-places-for-your-docker-configurations-and-persistent-data" name="setting-up-places-for-your-docker-configurations-and-persistent-data" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Setting up places for your Docker configurations and persistent Data</h2> <p>The next step is to set up the file structure for holding your Docker configurations and the data your Docker containers will access. This is my convention, so you're welcome to do things different, but this is a 'known good' approach.</p> <p>Now we create the set of directories I use for holding Docker Compose configurations (<code>/home/docker</code>) and the persistent data the Docker containers create (<code>/home/data</code>)</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>D</span>=<span>[</span>domain<span>]</span> <span>sudo</span> <span>mkdir</span> <span>-p</span> <span>/</span>home<span>/</span>data<span>/</span><span>$D</span> <span>sudo</span> <span>mkdir</span> <span>-p</span> <span>/</span>home<span>/</span>docker<span>/</span><span>$D</span></pre></div></div> <p>It's helpful to make sure that your non-root user can also read and write files in these directories:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>U</span>=<span>[</span>vps username<span>]</span> <span>sudo</span> <span>chown</span> <span>-R</span> <span>$U</span> <span>/</span>home<span>/</span>docker <span>sudo</span> <span>chown</span> <span>-R</span> <span>$U</span> <span>/</span>home<span>/</span>data</pre></div></div> <h2><a id="user-content-configuring-nginx-reverse-proxy-for-your-service" href="#configuring-nginx-reverse-proxy-for-your-service" name="configuring-nginx-reverse-proxy-for-your-service" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring Nginx reverse proxy for your service</h2> <p>Above, we installed Nginx as well as the Let's Encrypt scripts. Now we'll configure them as it's useful to have them working <em>before</em> you set up your services.</p> <p>In order for you, outside of your server, to see your specific LibreSoftware services, you will need to set up a secure external 'reverse proxy' on your host VPS which will accept requests for each of those services from the Internet and pass those requests securely to the two sets of Docker containers providing the services. These will answer to <code>https://[domain name]</code>, noting that a given VPS can answer on behalf of more than one domain name or subdomain name, where each references a different instance of a service or altogether different services. For example, a given VPS could host multiple services, like, say, a password manager, a WordPress blog, a Mautic email automation system, an Authentik Single-Sign On service, and a Discourse forum, where each has a separate domain name (or sub domain name) and each has an Nginx reverse proxy configuration that directs requests to the appropriate domain to the appropriate set of Docker containers.</p> <p>We use Let's Encrypt to provide the SSL certificates (each is a file with a specially generated, very long string) which we use to limit access to our services to encrypted (secure) connections (protecting both our users and ourselves from external enemies), usually one for each service domain name and corresponding Nginx configuration file. Some Nginx configurations will have multiple domains names for which a given service accepts connections, but it consolidates them to the 'canonical' (official) domain name. For example, the OER Foundation's WordPress website will respond to any of the following:</p> <ul><li> <a href="http://www.oerfoundation.org">http://www.oerfoundation.org</a>,</li> <li> <a href="http://oerfoundation.org">http://oerfoundation.org</a>,</li> <li> <a href="https://www.oerfoundation.org">https://www.oerfoundation.org</a>, and</li> <li> <a href="https://oerfoundation.org">https://oerfoundation.org</a> </li> </ul><p>but all requests to any of those 4 options (note the difference between http:// and http<strong>s</strong>:// where 's' refers to 'Secure' or encrypted) will all be redirected - transparently by the Nginx configuration, which will be reflected in your browser's address bar - to the canonical web address (or URL), <a href="https://oerfoundation.org">https://oerfoundation.org</a>, that we've chosen because it's short and secure.</p> <p>Nginx will not run unless the SSL certificates you reference in your configurations are valid. Given that we need to request them with a working Nginx <em>prior</em> to them being created puts us in an awkward position. We use a trick to get around it: we <em>temporarily</em> reference the default 'self-signed' SSL certificates (sometimes called 'Snakeoil certs' because that's the placeholder name they're given) that every new Linux system generates when it's installed, that are <em>valid certificates</em> (and thus acceptable to Nginx) but *they won't work with our domains, as they're generic and not 'signed' by an external party, like Let's Encrypt, meaning that your browser won't like them. But that's ok, as you browser will never need to see them, and Let's Encrypt's systems won't look at them either. We'll swap the Snakeoil certs out as soon as we've successfully created the Let's Encrypt ones, and your browser will be happy, and all will be well with the world.</p> <p>This should happen automatically when you install the Let's Encrypt packages, but just to be sure, run this (it won't harm anything if you run it more than once):</p> <p><code>sudo make-ssl-cert generate-default-snakeoil</code></p> <p>which creates your default 'Snakeoil' certificates. They are technically <em>valid</em> certificates, but they aren't signed by any</p> <h2><a id="user-content-lets-encrypt-setup" href="#lets-encrypt-setup" name="lets-encrypt-setup" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Let's Encrypt setup</h2> <p>Step one of using Let's Encrypt is to make sure the Let's Encrypt scripts are installed (note: sometimes they're referred to as 'certbot' - it's the same code):</p> <p><code>sudo apt install letsencrypt</code></p> <p>Let's Encrypt and Nginx need to work together. Nginx stores all of its configuration in the directory <code>/etc/nginx</code>. The first thing we'll do is create a place for Let's Encrypt Nginx-specific configuration details:</p> <p><code>sudo mkdir /etc/nginx/includes</code></p> <p>Then we create that configuration file itself:</p> <p><code>sudo $EDIT /etc/nginx/includes/letsencrypt.conf</code></p> <p>into which we copy-and-paste the following (no [tokens] to replace in this one!)</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Rule for legitimate ACME Challenge requests</span> location ^~ <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> default_type <span>"text/plain"</span>; <span># this can be any directory, but this name keeps it clear</span> root <span>/</span>var<span>/</span>www<span>/</span>letsencrypt; <span>}</span>   <span># Hide /acme-challenge subdirectory and return 404 on all requests.</span> <span># It is somewhat more secure than letting Nginx return 403.</span> <span># Ending slash is important!</span> location = <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> <span>return</span> <span>404</span>; <span>}</span></pre></div></div> <p>As described in the file we've just created, Let's Encrypt will look for a secret code we create to verify that we own the domain we're requesting an SSL certificate for, so we have to make sure it exists:</p> <p><code>sudo mkdir /var/www/letsencrypt</code></p> <p>You will need to install an Nginx reverse proxy configuration file for each web-based service you run on your VPS, but those will usually be service-specific in their make up, so I won't discuss them here.</p> <p>But, for the record (to make your lives a bit easier), this is what you'll do <em>after</em> you've got a suitable Nginx reverse proxy configuration in place and ready to run to acquire a Let's Encrypt SSL certificate.</p> <h3><a id="user-content-requesting-lets-encrypt-certificates" href="#requesting-lets-encrypt-certificates" name="requesting-lets-encrypt-certificates" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Requesting Let's Encrypt certificates</h3> <p>To request Let's Encrypt SSL certificates for your service, run the following, replacing the [domain name] reference with the address you've selected for your service. Note that 'certbot' is the script provided by the Let's Encrypt package - historically, it could also be called via 'letsencrypt', although apparently the latter is now deprecated:</p> <p><code>sudo certbot --webroot -w /var/www/letsencrypt -d [domain name]</code></p> <p>Note - if you want to address your instance from multiple domains, use one (or more) <code>-d [another domain]</code> - just make sure that</p> <ul><li>all those domains already point to your VPS, and</li> <li>those domains are included in the Nginx proxy configuration above.</li> </ul><p>otherwise the Let's Encrypt certbot request will fail!</p> <p>Here's what you're likely to see as output from the first run of the letsencrypt script - note that it will ask you for an email address (so it can send you warnings if your certificate is going to expire, e.g. due to a problem with renewal (like if you make a configuration change that breaks the renewal process)).</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Saving debug log to <span>/</span>var<span>/</span>log<span>/</span>letsencrypt<span>/</span>letsencrypt.log Enter email address <span>(</span>used <span>for</span> urgent renewal and security notices<span>)</span> <span>(</span>Enter <span>'c'</span> to cancel<span>)</span>: webmaster<span>@</span>fossdle.org   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please <span>read</span> the Terms of Service at https:<span>//</span>letsencrypt.org<span>/</span>documents<span>/</span>LE-SA-v1.3-September-<span>21</span>-<span>2022</span>.pdf. You must agree <span>in</span> order to register with the ACME server. Do you agree? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <span>(</span>Y<span>)</span>es<span>/</span><span>(</span>N<span>)</span>o: y   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Would you be willing, once your first certificate is successfully issued, to share your email address with the Electronic Frontier Foundation, a founding partner of the Let<span>'s Encrypt project and the non-profit organization that develops Certbot? We'</span>d like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <span>(</span>Y<span>)</span>es<span>/</span><span>(</span>N<span>)</span>o: y Account registered. Requesting a certificate <span>for</span> <span>[</span>domain name<span>]</span>   Successfully received certificate. Certificate is saved at: <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>domain name<span>]</span><span>/</span>fullchain.pem Key is saved at: <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>domain name<span>]</span><span>/</span>privkey.pem This certificate expires on <span>(</span>some future <span>date</span><span>)</span>. These files will be updated when the certificate renews. Certbot has <span>set</span> up a scheduled task to automatically renew this certificate <span>in</span> the background.   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - If you like Certbot, please consider supporting our work by: <span>*</span> Donating to ISRG <span>/</span> Let<span>'s Encrypt: https://letsencrypt.org/donate * Donating to EFF: https://eff.org/donate-le - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span></pre></div></div> <p>Ideally, you'll see a message like the above. If not, and there's an error, the error messages they provide are usually very useful and accurate. Fix the problem and try again. Note, your SSL certificate will have the name of your [domain name], even if it also provide support for [second domain name] (or third, fourth, etc.).</p> <p>Once you have a Let's Encrypt certificate, you can update our Nginx configuration:</p> <p><code>sudo $EDIT /etc/nginx/sites-available/[domain name]</code></p> <p>and swap all occurrences of</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre> ssl_certificate <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key; <span># ssl_certificate /etc/letsencrypt/live/[domain name]/fullchain.pem;</span> <span># ssl_certificate_key /etc/letsencrypt/live/[domain name]/privkey.pem;</span></pre></div></div> <p>to</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;</span> <span># ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;</span> ssl_certificate <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>domain name<span>]</span><span>/</span>fullchain.pem; ssl_certificate_key <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>domain name<span>]</span><span>/</span>privkey.pem;</pre></div></div> <p>which enables your new domain-specific SSL certificate. Check that Nginx is happy with your change:</p> <p><code>sudo nginx -t</code></p> <p>and if so,</p> <p><code>sudo service nginx reload</code></p> <p>You domain should now be enabled for <code>https://</code> access. Note that going to <code>http://[domain name]</code> should automatically redirect you to <code>https://[domain name]</code> because you care about your user's security!</p> <p>And that's it - your server is now ready to receive specific configurations for a myriad of Libre web services! Have a look around this site and check out some of the other tutorials - for many of them, you'll now be able to skip to near the end, as you've done most of the groundwork already!</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=59&amp;2=field_blog_comments&amp;3=comment" token="_ja99mlVJRPvJ8nwyG98TWz55iVgOEcN5jO1aOJbqu0"></drupal-render-placeholder> </div> </section> Thu, 12 Oct 2023 00:12:25 +0000 dave 59 at http://tech.oeru.org Install NextCloud Hub and OnlyOffice on Ubuntu 22.04 with Docker Compose http://tech.oeru.org/install-nextcloud-hub-and-onlyoffice-ubuntu-2204-docker-compose <span class="field field--name-title field--type-string field--label-hidden">Install NextCloud Hub and OnlyOffice on Ubuntu 22.04 with 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--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--mariadb"> <span class="field__item-wrapper"><a href="/taxonomy/term/48" hreflang="en">mariadb</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--nextcloud"> <span class="field__item-wrapper"><a href="/taxonomy/term/51" hreflang="en">nextcloud</a></span> </div> <div class="field__item field__item--onlyoffice"> <span class="field__item-wrapper"><a href="/taxonomy/term/69" hreflang="en">onlyoffice</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--lets-encrypt"> <span class="field__item-wrapper"><a href="/taxonomy/term/17" hreflang="en">let&#039;s encrypt</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--productivity"> <span class="field__item-wrapper"><a href="/taxonomy/term/52" hreflang="en">productivity</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" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Mon 29/05/2023 - 11:50</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/2023-06/Screenshot%202023-05-31%20at%2016-23-11%20Dashboard%20-%20FOSSDLE%20NextCloud.png?itok=f5dB1mPK" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The user &#039;dashboard&#039; view of NextCloud with user-specific content designed to indicate recent, timely, or important content for the user looking at it. &quot;}" role="button" title="The user &#039;dashboard&#039; view of NextCloud with user-specific content designed to indicate recent, timely, or important content for the user looking at it. " data-colorbox-gallery="gallery-field_image-6QjKsnuVoeQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The user &#039;dashboard&#039; view of NextCloud with user-specific content designed to indicate recent, timely, or important content for the user looking at it. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-06/Screenshot%202023-05-31%20at%2016-23-11%20Dashboard%20-%20FOSSDLE%20NextCloud.png?itok=lBbW70zi" width="220" height="182" alt="The user &#039;dashboard&#039; view of NextCloud with user-specific content designed to indicate recent, timely, or important content for the user looking at it. " loading="lazy" 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/2023-06/Screenshot%202023-05-31%20at%2016-21-55%20ONLYOFFICE%20Docs%20Community%20Edition.png?itok=CsEu3-io" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Default page for the OnlyOffice service, providing the commands to create the &#039;secret&#039; key required for external services wanting to integrate with it, like our NextCloud instance. &quot;}" role="button" title="Default page for the OnlyOffice service, providing the commands to create the &#039;secret&#039; key required for external services wanting to integrate with it, like our NextCloud instance. " data-colorbox-gallery="gallery-field_image-6QjKsnuVoeQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Default page for the OnlyOffice service, providing the commands to create the &#039;secret&#039; key required for external services wanting to integrate with it, like our NextCloud instance. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-06/Screenshot%202023-05-31%20at%2016-21-55%20ONLYOFFICE%20Docs%20Community%20Edition.png?itok=sWLT0Lju" width="164" height="220" alt="Default page for the OnlyOffice service, providing the commands to create the &#039;secret&#039; key required for external services wanting to integrate with it, like our NextCloud instance. " loading="lazy" 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/2023-06/Screenshot%202023-05-31%20at%2016-30-39%20ONLYOFFICE%20-%20Administration%20settings%20-%20FOSSDLE%20NextCloud.png?itok=xGW97X5D" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;NextCloud&#039;s admin settings page, only visible to users with &#039;admin&#039; privileges. &quot;}" role="button" title="NextCloud&#039;s admin settings page, only visible to users with &#039;admin&#039; privileges. " data-colorbox-gallery="gallery-field_image-6QjKsnuVoeQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;NextCloud&#039;s admin settings page, only visible to users with &#039;admin&#039; privileges. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-06/Screenshot%202023-05-31%20at%2016-30-39%20ONLYOFFICE%20-%20Administration%20settings%20-%20FOSSDLE%20NextCloud.png?itok=F6Bvm1Lb" width="146" height="220" alt="NextCloud&#039;s admin settings page, only visible to users with &#039;admin&#039; privileges. " loading="lazy" 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/2023-06/Screenshot%202023-05-31%20at%2016-29-48%20ONLYOFFICE%20-%20Administration%20settings%20-%20FOSSDLE%20NextCloud.png?itok=bV7eXn4L" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The OnlyOffice configuration page, into which we put the URL of our OnlyOffice service, and our secret key. &quot;}" role="button" title="The OnlyOffice configuration page, into which we put the URL of our OnlyOffice service, and our secret key. " data-colorbox-gallery="gallery-field_image-6QjKsnuVoeQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The OnlyOffice configuration page, into which we put the URL of our OnlyOffice service, and our secret key. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-06/Screenshot%202023-05-31%20at%2016-29-48%20ONLYOFFICE%20-%20Administration%20settings%20-%20FOSSDLE%20NextCloud.png?itok=RnF69S-5" width="220" height="182" alt="The OnlyOffice configuration page, into which we put the URL of our OnlyOffice service, and our secret key. " loading="lazy" 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/2023-06/Screenshot%202023-05-31%20at%2016-28-04%205763-Article%20Text-37303-1-18-20211004-cgoode%20dlane-edit.docx%20-%20OERu%20NextCloud.png?itok=BwJ_Rg2W" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A complex document being collaboratively edited (with track-changes turned on) on OnlyOffice. This was a peer reviewed journal article we wrote about using Free and Open Source Software tools for learners&#039; benefit. &quot;}" role="button" title="A complex document being collaboratively edited (with track-changes turned on) on OnlyOffice. This was a peer reviewed journal article we wrote about using Free and Open Source Software tools for learners&#039; benefit. " data-colorbox-gallery="gallery-field_image-6QjKsnuVoeQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A complex document being collaboratively edited (with track-changes turned on) on OnlyOffice. This was a peer reviewed journal article we wrote about using Free and Open Source Software tools for learners&#039; benefit. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-06/Screenshot%202023-05-31%20at%2016-28-04%205763-Article%20Text-37303-1-18-20211004-cgoode%20dlane-edit.docx%20-%20OERu%20NextCloud.png?itok=XBn0matl" width="220" height="182" alt="A complex document being collaboratively edited (with track-changes turned on) on OnlyOffice. This was a peer reviewed journal article we wrote about using Free and Open Source Software tools for learners&#039; benefit. " loading="lazy" 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>This is another update of my previous posts (installing <a href="/node/17">NextCloud with Collabora Office Online on Ubuntu 16.04</a> and then <a href="/node/29">NextCloud with OnlyOffice on Ubuntu 18.04</a>). I'm updating it thanks to my colleague in edtech, Stephen Downes' heroic videos showing how he went through this process using my 18.04 instructions on 22.04, running into a few minor issues... this update seeks to remedy the problems he encountered with the older tutorial. Remember, all these Free and Open Source Software projects are progressing and improving relentlessly, which means the they way they behaved a few years ago is not likely to be <em>exactly</em> the way they behave now. In particular, <a href="https://nextcloud.com">NextCloud</a> is leaping from strength to strength, benefiting from the <a href="https://nextcloud.com/blog/european-governments-work-with-nextcloud-to-build-digitally-sovereign-office/">well-founded concern held by many in the EU</a> about data sovereignty and the market domination (and exploitation) of US-based multinationals like Amazon, Google, Microsoft, Dropbox, and others.</p> <p>There're a few productivity packages that can be used in conjunction with NextCloud to provide comparable functionality to, for example, GoogleDocs + GoogleDrive or Microsoft Office 365 + Microsoft OneDrive, including Collabora Office (which we've used in the past). But the best companion productivity suite for NextCloud, in my opinion, is <a href="https://onlyoffice.com">OnlyOffice</a>. The application itself (for the tech focused reader, they've built an entirely <a href="https://github.com/ONLYOFFICE/">new application ecosystem</a> primarily using modern Javascript frameworks) is impressive in both capabilities and polish. The only real caveat I've come across is that it uses, by default, the <a href="https://openstandards.nz/case-study-microsofts-ooxml-standard">'fauxpen' standard formats</a> developed by Microsoft rather than the true open standard formats of <a href="https://en.wikipedia.org/wiki/OpenDocument">OpenDocumentFormat</a>. But in a world where, sadly, most people don't even know what a file format is, any software that doesn't read and write the incumbent monopolist's format with great fidelity is dead in the water. On that count, OnlyOffice is impressive. NextCloud + OnlyOffice - even better together (<em>without</em> a single US multinational tech giant involved. NextCloud development is led from Germany, OnlyOffice's development is led by a team in <a href="https://www.onlyoffice.com/about.aspx">Latvia</a>)!</p> <p>The beauty of the open source software model is that we can connect complementary applications, like NextCloud and OnlyOffice - developed by completely separate communities - to create a tightly integrated, highly functional, diverse computing platform. This combination, along with a bunch of other NextCloud "apps", is the equal of something like Google Apps (which includes Google Docs and Google Drive), but is under your control, not Google's. To me, that's a crucial difference.</p> <p>With the release of NextCloud 26.0 (current as of this writing), NextCloud has the option of installing <a href="https://nextcloud.com/office/">NextCloud Office</a> (which is just a bundled OnlyOffice installation) alongside it, creating something called "NextCloud Hub". It's pretty impressive, but I've found that managing the OnlyOffice install is problematic and somewhat inflexible, so I've opted to stay with having an independent (rather than bundled) OnlyOffice instance, but it's on the same server but managed as a different service. That's what we'll be setting up here!</p> <ul class="table-of-contents"><li> <p><a href="#tips-for-this-tutorial">Tips for this tutorial</a></p> </li> <li> <p><a href="#create-a-virtual-private-server">Create a Virtual Private Server</a></p> <ul><li> <p><a href="#vps-properties">VPS Properties:</a></p> </li> </ul></li> <li> <p><a href="#key-variables-for-you-nextcloud-and-onlyoffice-instances">Key variables for you NextCloud and OnlyOffice instances</a></p> <ul><li> <p><a href="#get-your-domain-lined-up">Get your Domain lined up</a></p> </li> </ul></li> <li> <p><a href="#editing-files">Editing files</a></p> <ul><li> <p><a href="#set-up-an-unprivileged-user-for-yourself">Set up an unprivileged user for yourself</a></p> </li> </ul></li> <li> <p><a href="#configure-the-vps">Configure the VPS</a></p> <ul><li> <p><a href="#configuring-your-firewall">Configuring your firewall</a></p> </li> <li> <p><a href="#install-the-nginx">Install the Nginx</a></p> </li> </ul></li> <li> <p><a href="#outgoing-vps-email-optional">Outgoing VPS Email (optional)</a></p> </li> <li> <p><a href="#installing-docker-compose-and-lets-encrypt">Installing Docker Compose and Let's Encrypt</a></p> </li> <li> <p><a href="#installing-mariadb">Installing MariaDB</a></p> </li> <li> <p><a href="#configuring-nginx-reverse-proxy-for-nextcloud-and-onlyoffice">Configuring Nginx reverse proxy for NextCloud and OnlyOffice</a></p> </li> <li> <p><a href="#lets-encrypt-setup">Let's Encrypt setup</a></p> <ul><li> <p><a href="#nextcloud-proxy-configuration">NextCloud Proxy Configuration</a></p> </li> <li> <p><a href="#onlyoffice-proxy-configuration">OnlyOffice Proxy Configuration</a></p> </li> <li> <p><a href="#requesting-lets-encrypt-certificates">Requesting Let's Encrypt certificates</a></p> </li> </ul></li> <li> <p><a href="#prepare-your-docker-compose-host">Prepare your Docker Compose host</a></p> </li> <li> <p><a href="#nextcloud-install">NextCloud Install</a></p> <ul><li> <p><a href="#install-the-nextcloud-docker-recipe">Install the NextCloud Docker recipe</a></p> </li> <li> <p><a href="#the-nextcloud-nginx-configuration">The NextCloud Nginx configuration</a></p> </li> <li> <p><a href="#the-onlyoffice-docker-configuration">The OnlyOffice Docker configuration</a></p> </li> </ul></li> <li> <p><a href="#firing-up-your-nextcloud">Firing up your NextCloud!</a></p> <ul><li> <p><a href="#the-nextcloud-source-code-if-necessary">The NextCloud source code (if necessary)</a></p> </li> </ul></li> <li> <p><a href="#configuring-database-access">Configuring database access</a></p> </li> <li> <p><a href="#configuring-the-admin-user">Configuring the Admin user</a></p> </li> <li> <p><a href="#configuring-outgoing-email">Configuring Outgoing Email</a></p> </li> <li> <p><a href="#setting-up-onlyoffice">Setting up OnlyOffice</a></p> </li> <li> <p><a href="#configuring-onlyoffice-integration-with-nextcloud">Configuring OnlyOffice Integration with NextCloud</a></p> </li> <li> <p><a href="#keeping-the-whole-thing-up-to-date">Keeping the whole thing up-to-date</a></p> </li> <li> <p><a href="#backing-up-nextcloud">Backing up NextCloud</a></p> </li> <li> <p><a href="#backup-onlyoffice">Backup OnlyOffice</a></p> </li> </ul><h2><a id="user-content-tips-for-this-tutorial" href="#tips-for-this-tutorial" name="tips-for-this-tutorial" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Tips for this tutorial</h2> <p>This tutorial is aimed at adventuresome would-be system administrators. I endeavour not to assume any specialised knowledge on your part, and try to provide useful tips and exposition along the way to help you build a valid mental model of what you're doing. At the same, this is not a trivial process. Luckily, if you try it out, and decide not to follow through, so long as you <em>delete your VPS</em>, you should not be out-of-pocket by more than a few cents.</p> <p>If this is your first attempt at 'self-hosting', and you <em>do</em> follow through, this could be the start of a new era in your technical status - you could realised that self-hosting 'agency' you always wanted. People with that skill set are in hot demand among most organisations, especially in the NGO/charitable spaces. Plus, I'll be very impressed by your <a href="https://www.merriam-webster.com/dictionary/moxie">moxie</a>!</p> <p>With this tutorial, I'm assuming you've got a computer with an Internet connection, that can run SSH (all modern systems should do that) and you can copy-and-paste stuff from this tutorial (in your browser) into either a terminal window (in which you're SSH'd into your VPS) or into a text editor. Note, if you find it difficult to paste into a terminal window, try using CTRL+SHIFT+V (CTRL+V is already used as a short-cut for something else in UNIX terminals since long before the Windows world started using CTRL+C and CTRL+V).</p> <p>When I provide files you need to copy, look for the placeholders with values you need to substitute (search-and-replace) in square brackets - [] - with your own values. I assume you'll be able to do that in a text editor on your desktop.</p> <h2><a id="user-content-create-a-virtual-private-server" href="#create-a-virtual-private-server" name="create-a-virtual-private-server" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Create a Virtual Private Server</h2> <p>The first step is to create a place to host the NextCloud and OnlyOffice instances. You can run them on a local piece of hardware of sufficient capacity, but make sure you've got a <em>fast</em> and symmetrical (as fast to upload as to download!) connection. If (as with most residential Internet services) your upload is much slower than your download (often 1:10 ratio) your server is going to be very slow for external people, especially if streaming video. Also, don't undertake this unless you have a flat-rate data connection.</p> <p>The more cost-effective approach in our experience, is to secure a low cost commodity Linux Virtual Private Server running Ubuntu Linux 22.04 (the latest "Long Term Support" version). That's what we'll assume you're running for this tutorial. We have used quite a few Linux VPSs commodity providers. Known good options are Digital Ocean (who recently raised their prices significantly), Linode, Vultr, Hetzner, and TurnkeyLinux. There are many (hundreds) of other credible options. We recommend you find one hosted in the network epicentre (which isn't necessarily the same as the 'geographic' epicentre) of your audience. For the record, we've just shifted our hosting to Hetzner as they've got the benefit of not being US-owned (They're German, and therefore don't expose us to the egregiously over-reaching US <a href="https://en.wikipedia.org/wiki/CLOUD_Act">Cloud</a> and <a href="https://proprivacy.com/guides/what-is-the-partiot-act">Patriot</a> Acts) and their pricing is pretty unbeatable.</p> <p>If you have trouble getting a VPS, you might find <a href="https://vimeo.com/684028258">this video</a> I created for provisioning a VPS, using Digital Ocean as an example. In my experience, the process for provisioning VPSs on other platforms is very similar. You'll find this process <em>much</em> easier than using either Microsoft Azure or Amazon AWS, which we do not recommend. Their systems are unnecessarily complex, proprietary (they will lock you in), and 10-20 times more expensive than commodity hosting options already listed.</p> <h3><a id="user-content-vps-properties" href="#vps-properties" name="vps-properties" class="heading-permalink" aria-hidden="true" title="Permalink"></a>VPS Properties:</h3> <p>We recommend that, for a NextCloud instance of modest size (say up to 50 users) you provision a VPS with the following spec. You should be able to upgrade those specs in realtime if required, except for your disk space. You can, however, provision a secondary storage space (you can start small and increase it as you need to). I will cover setting this up, as it'll make your life far far easier in the medium-long term.</p> <ul><li>4-8 GB RAM</li> <li>2-4 Virtual CPUs</li> <li>80-160 GB Disk space (NVME disk is faster than SSD which is faster than spinning disk space)</li> <li>running Ubuntu Linux 22.04 (the current Long Term Support version)</li> <li>extra storage - 20-40GB extra space (can be expanded on fairly short notice)</li> </ul><p>You'll need to create an account for yourself on your chosen hosting provider (it's a good idea 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 @2.04 (or the most recent 'Long Term Support' (LTS) version) - 24.04 is likely to come out in April 2024) in the 'zone' nearest to you (or your primary audience, if that's different).</p> <p>If you don't already have an SSH key on your computer, I encourage you to <a href="https://helpdeskgeek.com/how-to/how-to-generate-ssh-keys-on-windows-mac-and-linux/">create one</a> and specify the <strong>public key</strong> in the process of creating your server - specifying the 'public key' of your SSH identity during the server creation process that should allow you to log in without needing a password!</p> <p>You'll need to note the server's <strong>IPv4</strong> 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 <strong>IPv6</strong> address, which will be a set of 8 four <em>hex character</em> 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 should be able to <a href="https://www.digitalocean.com/community/tutorials/how-to-use-ssh-to-connect-to-a-remote-server-in-ubuntu">log into your new server via SSH</a>. If you're on a UNIX command line (e.g. a Linux or MacOS desktop), do this in a terminal. On Windows, I understand people use a tool called Putty for SSH, in which case follow the app's instructions.</p> <p><code>ssh [your server IPv4 or IPv6]</code></p> <p>followed by the ENTER key (that'll be true for any line of commands I provide).</p> <p>In some cases, depending on your hosting provider, you'll have a password to enter, or if you've specified your pre-existing public SSH key, you shouldn't need to enter a password at all, you should be logged in. To check what user you care, you can type</p> <p><code>whoami</code></p> <p>If it returns <code>root</code> (there's also a convention of using a '#' as the command prompt), you're the root or super-admin of the server. If not, you're a normal user (some hosting providers have a convention of giving you a default user called "ubuntu" or perhaps "debian") with a prompt that is, by convention, a '$'.</p> <p>Now that you're logged in, it's worth doing an upgrade of your server's Ubuntu system! Do that as follows (this works regardless of whether your a root user or an unprivileged user with 'sudo' ability):</p> <p><code>sudo apt update &amp;&amp; sudo apt dist-upgrade</code></p> <p>Usually the user, even if it's not the root user, will have the ability to use the <code>sudo</code> command modifier - that means "<em>do</em> this action as the root (aka the 'Super User', thus '<em>su</em>' in 'sudo' for short) user" - if you're a non-root user, you'll likely be asked to enter your password as a security precaution the first time you run a command prefaced by <code>sudo</code>. Enter it, and it should run the command. Plus, the system shouldn't bother you for it again unless you leave your terminal unused for a while (usually 5 minutes) and come back to it.</p> <p>At this point, I also like to install a cool software package called 'etckeeper' which records configuration changes on your VPS for future reference (it can be life-saving if trying to recover from an administrative mess-up!):</p> <p><code>sudo apt install etckeeper</code></p> <p>which will also install some dependencies, including the very important (and relevant later on) 'git' version control system.</p> <h2><a id="user-content-key-variables-for-you-nextcloud-and-onlyoffice-instances" href="#key-variables-for-you-nextcloud-and-onlyoffice-instances" name="key-variables-for-you-nextcloud-and-onlyoffice-instances" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Key variables for you NextCloud and OnlyOffice instances</h2> <p>To set up your services, you'll need a few crucial bits of information related to your system's identity and external systems you'll need it to interact with. For example, as mentioned before, you'll need a domain name. For the rest of this tutorial, we'll use the convention of representing those variables as a name inside [], or, for the domain name you've picked, [domain name].</p> <p>Here's a list of variables you'll need to know to complete the rest of this tutorial:</p> <ul><li> <strong>[ipv4]</strong> and <strong>[ipv6]</strong> - your VPS' IPv4 and IPv6 addresses (the latter can be ignored if your cloud provider doesn't support IPv6 addresses) as described above.</li> <li> <strong>[nextcloud domain]</strong> and <strong>[onlyoffice domain]</strong> - the fully qualified domain names or subdomains of a base <strong>[domain name]</strong> by which you want your services to be accessed. You must have full domain management ability on this domain. Example: nextcloud.oeru.org - that's the nextcloud subdomain of the oeru.org domain. *Authenticating SMTP details - this is required so your services can send emails to users - crucial things like email address validation and password recovery emails... <ul><li> <strong>[smtp server]</strong> - the domain name or IPv4 or IPv6 address of an SMTP server</li> <li> <strong>[smtp port]</strong> - the port number on the server that is listening for your connection. By convention it's likely to be 465 or 587, or possibly 25.</li> <li> <strong>[smtp reply-to-email]</strong> - a monitored email to which people can send email related to this WordPress site, e.g. notifications@<strong>[domain name]</strong> </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> <strong>[your email]</strong> - an email address to which system-related emails can be sent to you, perhaps something like webmaster@[domain name].</li> <li> <strong>[vps username]</strong> - the username you use on your server (by convention, these are one word, and all lower case).</li> <li> <strong>[redis password]</strong> - this is a random secret that secure access to your webserver's cached data - I use a <a href="/node/43">randomly generated alphanumeric password</a>.</li> <li> <strong>[onlyoffice secret]</strong> - this comes from your actual install, and you can get it when the time comes.</li> <li>The MariaDB credentials for your NextCloud system (which stores a lot of stuff in MariaDB or MySQL by default) <ul><li> <strong>[db name]</strong> - the name of your MariaDB database for NextCloud - usually 'nextcloud'.</li> <li> <strong>[db user]</strong> - the user who can manage your database.</li> <li> <strong>[db password]</strong> - the user's password.</li> </ul></li> </ul><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>Once you have selected and registered your domain, you can 'manage your Zone' to set up (usually through a web interface provided by the registrar) an <strong>A Record</strong> which associates your website's name to the <strong>IPv4</strong> 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 the web service you want to set up.</p> <p>Nowadays, <em>if your Domain Name host offers it (some don't, meaning you might be better off with a different one),</em> it's also important to define an <strong>IPv6</strong> record, which is called an <strong>AAAA Record</strong>... 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 registrar's interface requests... but in most cases that'll be set to a default of an hour automatically.</p> <h2><a id="user-content-editing-files" href="#editing-files" name="editing-files" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Editing files</h2> <p>In the rest of this tutorial, we're going to be editing quite a few files via the command line. If you're new to this, I recommend using the 'nano' text editor which is installed by default on Ubuntu Linux systems. It's fairly simple, and all of its options are visible in the text-based interface. I tend to use a far more powerful but far less beginner-friendly editor called 'vim'. There're other editors people might choose, too. To use your preferred editor for the rest of the tutorial, enter the following to set an environment variable EDIT, specifying your preferred editor, e.g.:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>EDIT</span>=$<span>(</span><span>which</span> <span>nano</span><span>)</span></pre></div></div> <p>or, if you're like me</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>EDIT</span>=$<span>(</span><span>which</span> <span>vim</span><span>)</span></pre></div></div> <p>so that subsequent references to $EDIT will invoke your preferred editor. Note the command <code>$(which nano)</code> is a script which finds the full path to the named command, in this case 'nano'. Putting a command inside the $() means 'replace with the value the script returns', so it sets the value of EDIT to the path of the nano command in this case.</p> <p>To test (at any time) whether you session still knows your $EDIT command, run</p> <p><code>echo $EDIT</code></p> <p>if it returns the path to your preferred editor, you're good to go. If not, just reassert the EDIT= line from above!</p> <p><em>Note: if you log out and back in again, change users, or create a new terminal tab/session, you'll need to reassert the EDIT value.</em></p> <h3><a id="user-content-set-up-an-unprivileged-user-for-yourself" href="#set-up-an-unprivileged-user-for-yourself" name="set-up-an-unprivileged-user-for-yourself" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up an unprivileged user for yourself</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.</p> <p>This will log you into your server as it did the first time, either as 'root' or the default unprivileged user. It's not considered good practice to access your server as root (it's too easy to completely screw it up by accident). It's a good idea to create your own 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 [vps username] with your chosen username - in my case, I'd use <code>U=dave</code>):</p> <p><code>U=[vps username]</code><br /><code>adduser $U</code><br /><code>adduser $U ssh</code><br /><code>adduser $U admin</code><br /><code>adduser $U sudo</code></p> <p>You'll also want to a set a password for user [vps username] (we have a tutorial on <a href="/node/43">creating good passwords</a>):</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' - you can show them in a directory listing via <code>ls -a</code>) for the file into which to put your public SSH key:</p> <p><code>su $U</code></p> <p>after which you need to re-run your EDIT command: <code>EDIT=$(which nano)</code></p> <p>and then run <code>ssh-keygen -t rsa -b 2048</code><br /><code>$EDIT ~/.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>and then leave the 'su' state, back to the superuser:</p> <p><code>CTRL+D</code> or type <code>exit</code></p> <p>From that point, you should be able to SSH to your server via <code>ssh [vps 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' (the 'sudo' will be ignored as it's unnecessary).</p> <h2><a id="user-content-configure-the-vps" href="#configure-the-vps" name="configure-the-vps" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configure the VPS</h2> <p>First things first. Let's make sure you've got the time zone set appropriately for your instance. It'll probably default to 'UTC' (Greenwich Mean Time). For our servers, I tend to pick 'Pacific/Auckland' which is our time zone. Run this</p> <p><code>sudo dpkg-reconfigure tzdata</code></p> <p>and pick the appropriate timezone. You can just leave it running UTC, but you might find it tricky down the track if, for example, you're looking at logs and having to constantly convert the times into your timezone.</p> <h3><a id="user-content-configuring-your-firewall" href="#configuring-your-firewall" name="configuring-your-firewall" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring your firewall</h3> <p>In the name of safety from the get-go, let's configure our firewall. We work on the basis of explicitly allowing in <em>only</em> what we want to let in (i.e. a 'default deny' policy).</p> <p>First we'll enable the use of SSH through the firewall (<em>not doing this could lock us out of your machine!</em>)</p> <p><code>sudo ufw allow ssh</code><br /></p> <p>while we're here, we'll also enable data transfer from the internal (to the VPS) Docker virtual network and the IP range it uses for Docker containers:</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>Then we'll enable forwarding from internal network interfaces as required for Docker containers to be able to talk to the outside world:</p> <p><code>sudo $EDIT /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><span>#DEFAULT_FORWARD_POLICY="DROP"</span> <span>DEFAULT_FORWARD_POLICY</span>=<span>"ACCEPT"</span></pre></div></div> <p>and then save and exit the file (CTRL-X and then 'Y' if your editor is nano).</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 $EDIT /etc/ufw/sysctl.conf</code></p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Uncomment this to allow this host to route packets between interfaces</span> net<span>/</span>ipv4<span>/</span><span>ip_forward</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>default<span>/</span><span>forwarding</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>all<span>/</span><span>forwarding</span>=<span>1</span></pre></div></div> <p>Then we need to restart the network stack to apply that configuration change</p> <p><code>sudo systemctl restart systemd-networkd</code></p> <p>(on older Ubuntu systems this would have been done via <code>sudo service networking restart</code>...)</p> <p>Next we have to enable the UFW firewall to start at boot time.</p> <p><code>sudo $EDIT /etc/ufw/ufw.conf</code></p> <p>And set the ENABLED variable near the top:</p> <p><code>ENABLED=yes</code></p> <p>Now you can formally start UFW now:</p> <p><code>sudo ufw enable</code></p> <h3><a id="user-content-install-the-nginx" href="#install-the-nginx" name="install-the-nginx" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install the Nginx</h3> <p>Next we need to install the Nginx web server and reverse-proxy, as well as the Let's Encrypt SSL certificate generator, both of which are crucial for any secure web services you might want to host. Nginx is a more efficient and flexible alternative to the older Apache web server you might've seen elsewhere (Nginx recently surpassed Apache as the most widely used web server on the Internet).</p> <p><code>sudo apt install nginx-full letsencrypt ssl-cert</code></p> <p>You'll get a couple pop-up windows in your terminal, just hit ENTER to accept the defaults. Having installed it, we need to create firewall rules to allow external services to see it:</p> <p><code>sudo ufw allow 'Nginx Full'</code></p> <p>You can check if the firewall rules you requested have been enabled:</p> <p><code>sudo ufw status</code></p> <h2><a id="user-content-outgoing-vps-email-optional" href="#outgoing-vps-email-optional" name="outgoing-vps-email-optional" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Outgoing VPS Email (optional)</h2> <p>Although it's not absolutely necessary (you can do this section later if you're in a big hurry), it's very useful for your server to be able to send out emails, like status emails to administrators (perhaps you) about things requiring their attention, e.g. the status of backups, pending security updates, expiring SSL certificates, etc. To do this, we'll set up the industrial strength Postfix SMTP server, which is pretty quick and easy. First we install Postfix.</p> <p><code>sudo apt install postfix bsd-mailx</code></p> <p>During the install, you'll be asked to select a bunch of configuration parameters. Select the defaults except:</p> <ul><li>Select "Internet Site with Smarthost",</li> <li>fill in the domain name for your server [domain name],</li> <li>the [smtp server] name and [smtp port] (in the form [smtp server]:[smtp port], e.g. smtp.oeru.org:587 ) of your "smarthost" who'll be doing the authenticating SMTP for you, and</li> <li>the email address to which you want to receive system-related messages, [your email].</li> </ul><p>After that's done, we set a default address for the server to mail to, to [your email] selected above. First</p> <p><code>sudo $EDIT /etc/aliases</code></p> <p>We need to make sure the "root" user points to a real email address. Add a line at the bottom which says (replacing [your email] with your email :) )</p> <p><code>root: [your email]</code></p> <p>After which you'll need to convert the aliases file into a form that postfix can process, simply by running this:</p> <p><code>sudo newaliases</code></p> <p>Then we have to define the authentication credentials required to convince your mail server that you're you!</p> <p><code>sudo $EDIT /etc/postfix/relay_password</code></p> <p>and enter a single line in this format:</p> <p><code>[smtp server] [smtp user]:[smtp password]</code></p> <p>as an example, this is more or less what I've got for my system. Note that the [smtp user] in my case is an email address (this is common with many smtp system - the user is the same as the email address):</p> <p><code>smtp.oerfoundation.org smtp-work@fossdle.org:SomeObscurePassw0rd</code></p> <p>then save the file and, like the aliases file, run the conversion process (which uses a slightly different mechanism):</p> <p><code>sudo postmap /etc/postfix/relay_password</code></p> <p>Finally, we'll edit the main configuration file for Postfix to tell it about all this stuff:</p> <p>sudo $EDIT /etc/postfix/main.cf</p> <p>If your SMTP server uses port 25 (the default for unencrypted SMTP) you don't have to change anything, although most people nowadays prefer to use StartTLS or otherwise encrypted transport to at least ensure that your SMTP authentication details (at least) are transferred encrypted. That means using port 587 or 465. If you're using either of those ports, find the "relayhost = [your server name]" line... and add your port number after a colon, like this</p> <p><code>relayhost = [smtp server]:[smtp port]</code></p> <p>or, for example:</p> <p><code>relayhost = smtp.oerfoundation.org:465</code></p> <p>Then we have to update the configuration for Postfix to ensure that it knows about the details we've just defined (this command will automatically back up the original default configuration so you can start from scratch with the template below):</p> <p><code>sudo mv /etc/postfix/main.cf /etc/postfix/main.cf.orig &amp;&amp; sudo $EDIT /etc/postfix/main.cf</code></p> <p>You can just copy-and-paste the following into it, substituting your specific values for the [tokens].</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># See /usr/share/postfix/main.cf.dist for a commented, more complete version</span>   <span># Debian specific: Specifying a file name will cause the first</span> <span># line of that file to be used as the name. The Debian default</span> <span># is /etc/mailname.</span> <span>#myorigin = /etc/mailname</span>   smtpd_banner = <span>$myhostname</span> ESMTP <span>$mail_name</span> <span>(</span>Ubuntu<span>)</span> biff = no   <span># appending .domain is the MUA's job.</span> append_dot_mydomain = no   <span># Uncomment the next line to generate "delayed mail" warnings</span> <span>#delay_warning_time = 4h</span> readme_directory = no   <span># See http://www.postfix.org/COMPATIBILITY_README.html -- default to 3.6 on</span> <span># fresh installs.</span> compatibility_level = <span>3.6</span>   <span># TLS parameters</span> <span>smtpd_tls_cert_file</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem <span>smtpd_tls_key_file</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key <span>smtpd_tls_security_level</span>=may   <span>smtp_tls_CApath</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>certs <span>#smtp_tls_security_level=may</span> smtp_tls_session_cache_database = btree:<span>${data_directory}</span><span>/</span>smtp_scache   smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination myhostname = <span>[</span>domain name<span>]</span> alias_maps = hash:<span>/</span>etc<span>/</span>aliases alias_database = hash:<span>/</span>etc<span>/</span>aliases myorigin = <span>/</span>etc<span>/</span>mailname mydestination = <span>$myhostname</span>, localhost relayhost = <span>[</span>smtp server<span>]</span>:<span>[</span>smtp port<span>]</span> mynetworks = 127.0.0.0<span>/</span><span>8</span> <span>[</span>::ffff:127.0.0.0<span>]</span><span>/</span><span>104</span> <span>[</span>::<span>1</span><span>]</span><span>/</span><span>128</span> mailbox_size_limit = <span>0</span> recipient_delimiter = + inet_interfaces = all inet_protocols = all   <span># added to configure accessing the relay host via authenticating SMTP</span> smtp_sasl_auth_enable = <span>yes</span> smtp_sasl_password_maps = hash:<span>/</span>etc<span>/</span>postfix<span>/</span>relay_password smtp_sasl_security_options = noanonymous smtp_tls_security_level = encrypt   <span># if you're using Ubuntu prior to 20.04, uncomment (remove the #) the</span> <span># earlier line smtp_tls_security_level = may to save errors in 'postfix check'</span> <span># and comment this line (by adding a # at the start)</span> smtp_tls_wrappermode = <span>yes</span></pre></div></div> <p>Once you've created that <code>main.cf</code> file, you can double check that your config is valid:</p> <p><code>sudo postfix check</code></p> <p>and if it's all ok, you can get Postfix to re-read its configuration:</p> <p><code>sudo postfix reload</code></p> <p>You can then try sending an email so see if it works!</p> <p>By default, a command line application called "mail" is installed as part of the bsd-mailx package we installed alongside postfix. You can use it to send test email from the command line on your host to verify you've got things working correctly! The stuff in &lt;&gt; are the keys to hit at the end of the line...</p> <p><code>mail you@email.domain&lt;ENTER&gt;</code></p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Subject: Testing from your.relay.server.domain<span>&lt;</span>ENTER<span>&gt;</span> Testing postfix remote host<span>&lt;</span>ENTER<span>&gt;</span> <span>&lt;</span>CTRL-D<span>&gt;</span> Cc:<span>&lt;</span>ENTER<span>&gt;</span></pre></div></div> <p>Typing (hold down the Control or Ctrl key on your keyboard and press the "d" key) will finish your message, showing you a "CC:" field, in which you can type in other email addresses if you want to test sending to multiple addresses. When you then hit , it will attempt to send this email. It might take a few minutes to work its way through to the receiving email system (having to run the gauntlet of spam and virus filters on the way).</p> <p>You can also always check the postfix system logs to see what postfix thinks about it using the command:</p> <p><code>sudo less +G /var/log/mail.log</code></p> <p>if your system doesn't have a <code>/var/log/mail.log</code>, never fear! Try this instead:</p> <p><code>sudo less +G /var/log/syslog</code></p> <p>In either case, hit to have the log update in real time.</p> <h2><a id="user-content-installing-docker-compose-and-lets-encrypt" href="#installing-docker-compose-and-lets-encrypt" name="installing-docker-compose-and-lets-encrypt" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Installing Docker Compose and Let's Encrypt</h2> <p>The next step is to set up the file structure for holding your Docker configurations and the data your Docker containers will access. This is my convention, so you're welcome to do things different, but this is a 'known good' approach.</p> <p>First let's install Docker Compose (and its dependencies, like the whole Docker subsystem) and the <a href="http://letsencrypt.org/">Let's Encrypt</a> scripts that let you procure no-cost Secure Sockets Layer certificates to secure access to your server.</p> <p><code>sudo apt install docker-compose letsencrypt</code></p> <p>Now we create the set of directories I use for holding Docker Compose configurations (<code>/home/docker</code>) and the persistent data the Docker containers create (<code>/home/data</code>)</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>D</span>=<span>[</span>nextcloud domain<span>]</span> <span>sudo</span> <span>mkdir</span> <span>-p</span> <span>/</span>home<span>/</span>data<span>/</span><span>$D</span> <span>sudo</span> <span>mkdir</span> <span>-p</span> <span>/</span>home<span>/</span>docker<span>/</span><span>$D</span></pre></div></div> <p>followed by</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>D</span>=<span>[</span>onlyoffice domain<span>]</span> <span>sudo</span> <span>mkdir</span> <span>-p</span> <span>/</span>home<span>/</span>data<span>/</span><span>$D</span> <span>sudo</span> <span>mkdir</span> <span>-p</span> <span>/</span>home<span>/</span>docker<span>/</span><span>$D</span></pre></div></div> <p>It's helpful to make sure that your non-root user can also read and write files in these directories:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>U</span>=<span>[</span>vps username<span>]</span> <span>sudo</span> <span>chown</span> <span>-R</span> <span>$U</span> <span>/</span>home<span>/</span>docker <span>sudo</span> <span>chown</span> <span>-R</span> <span>$U</span> <span>/</span>home<span>/</span>data</pre></div></div> <h2><a id="user-content-installing-mariadb" href="#installing-mariadb" name="installing-mariadb" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Installing MariaDB</h2> <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 server and the client like this.</p> <p><code>sudo apt install mariadb-server mariadb-client</code></p> <p>You should now be able to type <code>sudo mysql</code> at the command prompt, and it'll log you into the MariaDB console (to get out type <code>\q</code> or <code>exit</code>)</p> <p>Tweak the configuration so that it's listening on</p> <p><code>sudo vim /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 (127.0.0.1)...</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Instead of skip-networking the default is now to listen only on</span> <span># localhost which is more compatible and is not less secure.</span> <span>#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 port 3306 on all interfaces, i.e. 0.0.0.0. Your instance will be protected from anyone outside of your VPS connecting to it by the fact that external access to port 3306 isn't allowed by your ufw firewall.</p> <p>To check it's running, you can run</p> <p><code>sudo netstat -punta | grep 3306</code></p> <p>and you should see something like</p> <p><code>tcp 0 0 0.0.0.0:3306 0.0.0.0:* LISTEN 8459/mysqld</code></p> <p>which is the 'mysqld' (the MySQL-compatible database daemon provided by MariaDB).</p> <p>Now set up the database which will hold NextCloud's data. Log into the MySQL client on the host:</p> <p><code>sudo mysql</code></p> <p>You'll need to gin up a password for your "nextcloud" database user. I usually use <code>pwgen</code> (<code>sudo apt install pwgen</code>) - for example running this command will give you a single 19 character password without special characters (just numbers and letters):</p> <p><code>pwgen -s 19 1</code></p> <p>Giving you something like this (but if it's truly random, almost certainly not exactly this):</p> <p>bYIOSrvR9aGwL5FRGFU</p> <p>At the prompt (which will look something like MariaDB [(none)]&gt;) enter the following lines (putting your password in place of [passwd]):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER <span>"nextcloud"</span><span>@</span><span>"%"</span> IDENTIFIED BY <span>"[passwd]"</span>; GRANT ALL ON nextcloud.<span>*</span> to <span>"nextcloud"</span><span>@</span><span>"%"</span>; FLUSH PRIVILEGES;</pre></div></div> <p>Then enter <code>\q</code> to exit.</p> <h2><a id="user-content-configuring-nginx-reverse-proxy-for-nextcloud-and-onlyoffice" href="#configuring-nginx-reverse-proxy-for-nextcloud-and-onlyoffice" name="configuring-nginx-reverse-proxy-for-nextcloud-and-onlyoffice" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring Nginx reverse proxy for NextCloud and OnlyOffice</h2> <p>Above, we installed Nginx as well as the Let's Encrypt scripts. Now we'll configure them as it's useful to have them working <em>before</em> you set up your services.</p> <p>In order for you, outside of your server, to see the NextCloud and OnlyOffice services, you will need to set up a secure external 'reverse proxy' on your host VPS which will accept requests for those two services from the Internet and pass those requests securely to the two sets of Docker containers providing the services. These will answer to <code>https://[nextcloud domain]</code> (for NextCloud) and <code>https://[onlyoffice domain]</code> for OnlyOffice.</p> <p>Let's Encrypt will provide the SSL certificates (each is a file with a specially generated, very long string) which we use to limit access to our services to encrypted (secure) connections (protecting both our users and ourselves from external enemies).</p> <p>Nginx will not run unless the SSL certificates you reference in your configurations are valid. Given that we need to request them with a working Nginx <em>prior</em> to them being created puts us in an awkward position. We use a trick to get around it: we <em>temporarily</em> reference the default 'self-signed' SSL certificates (sometimes called 'Snakeoil certs' because that's the placeholder name they're given) that every new Linux system generates when it's installed, that are <em>valid certificates</em> (and thus acceptable to Nginx) but *they won't work with our domains, as they're generic and not 'signed' by an external party, like Let's Encrypt, meaning that your browser won't like them. But that's ok, as you browser will never need to see them, and Let's Encrypt's systems won't look at them either. We'll swap the Snakeoil certs out as soon as we've successfully created the Let's Encrypt ones, and your browser will be happy, and all will be well with the world.</p> <p>Note: many thanks to Stephen Harlow (who crash-tested this tutorial!) for pointing out that you <em>might</em> need to run the following if you're not finding the 'Snakeoil certs' on your system (running them just to be safe shouldn't cause any issues):</p> <p><code>sudo make-ssl-cert generate-default-snakeoil</code></p> <h2><a id="user-content-lets-encrypt-setup" href="#lets-encrypt-setup" name="lets-encrypt-setup" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Let's Encrypt setup</h2> <p>Let's Encrypt and Nginx need to work together. Nginx stores all of its configuration in the directory <code>/etc/nginx</code>. The first thing we'll do is create a place for Let's Encrypt Nginx-specific configuration details:</p> <p><code>sudo mkdir /etc/nginx/includes</code></p> <p>Then we create that configuration file itself:</p> <p><code>sudo $EDIT /etc/nginx/includes/letsencrypt.conf</code></p> <p>into which we copy-and-paste the following (no [tokens] to replace in this one!)</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Rule for legitimate ACME Challenge requests</span> location ^~ <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> default_type <span>"text/plain"</span>; <span># this can be any directory, but this name keeps it clear</span> root <span>/</span>var<span>/</span>www<span>/</span>letsencrypt; <span>}</span>   <span># Hide /acme-challenge subdirectory and return 404 on all requests.</span> <span># It is somewhat more secure than letting Nginx return 403.</span> <span># Ending slash is important!</span> location = <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> <span>return</span> <span>404</span>; <span>}</span></pre></div></div> <p>As described in the file we've just created, Let's Encrypt will look for a secret code we create to verify that we own the domain we're requesting an SSL certificate for, so we have to make sure it exists:</p> <p><code>sudo mkdir /var/www/letsencrypt</code></p> <h3><a id="user-content-nextcloud-proxy-configuration" href="#nextcloud-proxy-configuration" name="nextcloud-proxy-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>NextCloud Proxy Configuration</h3> <p>To configure the NextCloud proxy, you need to create this configuration file in your <code>/etc/nginx/sites-available/</code> directory.</p> <p>Create a file with a meaningful name for your NextCloud Proxy, something like "nextcloud" (I use the domain name I've chosen, e.g. for docs.oeru.org I call the proxy file "docs.oeru.org" - keeps everything clear, and I can have <em>multiple instances on the same server if I want</em>...). Let's go with in this instance (change it if you prefer)</p> <p><code>sudo $EDIT /etc/nginx/sites-available/nextcloud </code></p> <p>with the following contents, replacing [nextcloud domain] with your selected domain name, but leave off the [ ] (those are just there to make sure nginx errors if you've missed replacing any) - and the port number 8080 if you've opted to change to a different one!:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>server <span>{</span> listen <span>80</span>; listen <span>[</span>::<span>]</span>:<span>80</span>;   <span># note, you can add additional domain names, separated by a space, to which this config will answer.</span> server_name <span>[</span>nextcloud domain<span>]</span>;   include includes<span>/</span>letsencrypt.conf;   <span># enforce https</span> location <span>/</span> <span>{</span> <span>return</span> <span>302</span> https:<span>//</span><span>$server_name</span><span>$request_uri</span>; <span>}</span> <span>}</span>   server <span>{</span> listen <span>443</span> ssl; listen <span>[</span>::<span>]</span>:<span>443</span> ssl;   <span># note, you can add additional domain names, separated by a space, to which this config will answer.</span> server_name <span>[</span>nextcloud domain<span>]</span>;   <span>## Access and error logs.</span> access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>nextcloud domain<span>]</span>_access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>nextcloud domain<span>]</span>_error.log;   <span># these are temporary certificates, used only long enough to secure Let's Encrypt certs as below.</span> ssl_certificate <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key;   <span># these need to be commented out until after the Let's Encrypt</span> <span># certificates have been acquired</span> <span>#ssl_certificate /etc/letsencrypt/live/[nextcloud domain]/fullchain.pem;</span> <span>#ssl_certificate_key /etc/letsencrypt/live/[nextcloud domain]/privkey.pem;</span>   <span># from http://axiacore.com/blog/enable-perfect-forward-secrecy-nginx/</span> ssl_session_cache shared:SSL:10m; ssl_session_timeout 10m; <span># limit_req_zone $binary_remote_addr zone=one:10m rate=1r/s;</span> <span># forward secrecy settings</span> ssl_protocols TLSv1 TLSv1.1 TLSv1.2; ssl_prefer_server_ciphers on; ssl_ciphers <span>"EECDH+ECDSA+AESGCM EECDH+aRSA+AESGCM EECDH+ECDSA+SHA384 EECDH+ECDSA+SHA256 EECDH+aRSA+SHA384 EECDH+aRSA+SHA256 EECDH+aRSA+RC4 EECDH EDH+aRSA RC4 !aNULL !eNULL !LOW !3DES !MD5 !EXP !PSK !SRP !DSS !RC4"</span>; ssl_dhparam <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem;   <span># The following 2 rules are only needed for the user_webfinger app.</span> <span># Uncomment it if you're planning to use this app.</span> rewrite ^<span>/</span>.well-known<span>/</span>host-meta <span>/</span>public.php?<span>service</span>=host-meta <span>last</span>; rewrite ^<span>/</span>.well-known<span>/</span>host-meta.json <span>/</span>public.php?<span>service</span>=host-meta-json <span>last</span>;   <span># The following rule is only needed for the Social app.</span> <span># Uncomment it if you're planning to use this app.</span> rewrite ^<span>/</span>.well-known<span>/</span>webfinger <span>/</span>public.php?<span>service</span>=webfinger <span>last</span>;   location ^~ <span>/</span> <span>{</span> proxy_pass http:<span>//</span>127.0.0.1:<span>8080</span>; proxy_set_header Upgrade <span>$http_upgrade</span>; proxy_set_header Connection <span>"Upgrade"</span>; proxy_set_header Host <span>$http_host</span>; proxy_read_timeout 36000s; proxy_buffering off; proxy_max_temp_file_size 15000m; <span>}</span> client_max_body_size 1G; fastcgi_buffers <span>64</span> 4K; add_header Strict-Transport-Security <span>"max-age=31536000; includeSubdomains;"</span>; <span># Remove X-Powered-By, which is an information leak</span> fastcgi_hide_header X-Powered-By; <span>}</span></pre></div></div> <p>Note: you'll need to create the file cited in the proxy configuration: <code>/etc/ssl/certs/dhparam.pem</code></p> <p>You can do this as follows (install the necessary software, backup any possible existing version as a matter of prudence, and create a new one):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>sudo</span> apt update <span>&amp;&amp;</span> <span>sudo</span> apt <span>install</span> openssl <span>sudo</span> <span>[</span> <span>-f</span> <span>"/etc/ssl/certs/dhparam.pem"</span> <span>]</span> <span>&amp;&amp;</span> <span>sudo</span> <span>mv</span> <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem.bak <span>sudo</span> openssl dhparam <span>-out</span> <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem <span>2048</span></pre></div></div> <p>Once those are created, you have to make sure that they're "enabled" (replacing with your file names, of course):</p> <p><code>cd /etc/nginx/sites-enabled</code> <code>sudo ln -sf ../sites-available/nextcloud .</code></p> <p>To confirm that there aren't any typos or issues that might make nginx unhappy, run</p> <p><code>sudo nginx -t</code></p> <p>If all's well, get nginx to reread its configuration with the new files (if not, it might be because you missed replacing one of the [tokens]):</p> <p><code>sudo service nginx reload</code></p> <h3><a id="user-content-onlyoffice-proxy-configuration" href="#onlyoffice-proxy-configuration" name="onlyoffice-proxy-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>OnlyOffice Proxy Configuration</h3> <p>The OnlyOffice proxy configuration uses a very similar process to the one above. You just need to create another configuration file:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>upstream docservice <span>{</span> server 127.0.0.1:<span>9880</span>; <span>}</span>   map <span>$http_host</span> <span>$this_host</span> <span>{</span> <span>""</span> <span>$host</span>; default <span>$http_host</span>; <span>}</span>   map <span>$http_x_forwarded_proto</span> <span>$the_scheme</span> <span>{</span> default <span>$http_x_forwarded_proto</span>; <span>""</span> <span>$scheme</span>; <span>}</span>   map <span>$http_x_forwarded_host</span> <span>$the_host</span> <span>{</span> default <span>$http_x_forwarded_host</span>; <span>""</span> <span>$this_host</span>; <span>}</span>   map <span>$http_upgrade</span> <span>$proxy_connection</span> <span>{</span> default upgrade; <span>""</span> close; <span>}</span>   proxy_set_header Upgrade <span>$http_upgrade</span>; proxy_set_header Connection <span>$proxy_connection</span>; proxy_set_header X-Forwarded-Host <span>$the_host</span>; proxy_set_header X-Forwarded-Proto <span>$the_scheme</span>; proxy_set_header X-Forwarded-For <span>$proxy_add_x_forwarded_for</span>;   server <span>{</span> listen <span>80</span>; listen <span>[</span>::<span>]</span>:<span>80</span>;   server_name <span>[</span>onlyoffice domain<span>]</span>;   <span># for let's encrypt renewals!</span> include <span>/</span>etc<span>/</span>nginx<span>/</span>includes<span>/</span>letsencrypt.conf;   <span>## Access and error logs.</span> access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>onlyoffice domain<span>]</span>_access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>onlyoffice domain<span>]</span>_error.log;   <span># redirect all HTTP traffic to HTTPS.</span> location <span>/</span> <span>{</span> <span>return</span> <span>302</span> https:<span>//</span><span>$server_name</span><span>$request_uri</span>; <span>}</span> <span>}</span>   <span># This configuration assumes that there's an nginx container talking to the mautic PHP-fpm container,</span> <span># and this is a reverse proxy for that Mautic instance.</span> server <span>{</span> listen <span>443</span> ssl; listen <span>[</span>::<span>]</span>:<span>443</span> ssl;   server_name <span>[</span>onlyoffice domain<span>]</span>;   ssl_certificate <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key; <span>#ssl_certificate /etc/letsencrypt/live/[onlyoffice domain]/fullchain.pem;</span> <span>#ssl_certificate_key /etc/letsencrypt/live/[onlyoffice domain]/privkey.pem;</span> ssl_protocols TLSv1 TLSv1.1 TLSv1.2; <span># to create this, see https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html</span> ssl_dhparam <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem; keepalive_timeout 20s; <span># for let's encrypt renewals!</span> include <span>/</span>etc<span>/</span>nginx<span>/</span>includes<span>/</span>letsencrypt.conf;   proxy_http_version <span>1.1</span>; proxy_buffering off;   <span>## Access and error logs.</span> access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>onlyoffice domain<span>]</span>_access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>onlyoffice domain<span>]</span>_error.log;   add_header Strict-Transport-Security max-age=<span>31536000</span>; <span># add_header X-Frame-Options SAMEORIGIN;</span> add_header X-Content-Type-Options nosniff;   <span># see https://github.com/ONLYOFFICE/document-server-proxy/blob/master/nginx/proxy-https-to-http.conf</span> location <span>/</span> <span>{</span> proxy_pass http:<span>//</span>docservice; proxy_http_version <span>1.1</span>; <span>}</span> <span>}</span></pre></div></div> <p>After that's done, we'll repeat what we did for the NextCloud config:</p> <p><code>sudo cd /etc/nginx/sites-enabled</code> <code>sudo ln -sf ../sites-available/onlyoffice .</code> <code>sudo nginx -t</code></p> <p>and, if there're no errors, run</p> <p><code>sudo service nginx reload</code></p> <p>Now we're ready to request Let's Encrypt certificates.</p> <h3><a id="user-content-requesting-lets-encrypt-certificates" href="#requesting-lets-encrypt-certificates" name="requesting-lets-encrypt-certificates" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Requesting Let's Encrypt certificates</h3> <p>To request Let's Encrypt SSL certificates for your NextCloud and OnlyOffice services, run the following, replacing the [token], of course (note that 'certbot' is the script provided by the Let's Encrypt package - historically, it could also be called via 'letsencrypt', although apparently the latter is now deprecated):</p> <p><code>sudo certbot --webroot -w /var/www/letsencrypt -d [nextcloud domain]</code></p> <p>Note - if you want to address your instance from multiple domains, use one (or more) <code>-d [another domain]</code> - just make sure that</p> <ul><li>all those domains already point to your VPS, and</li> <li>those domains are included in the Nginx proxy configuration above.</li> </ul><p>otherwise the Let's Encrypt certbot request will fail!</p> <p>Here's what you're likely to see as output from the first run of the letsencrypt script - note that it will ask you for an email address (so it can send you warnings if your certificate is going to expire, e.g. due to a problem with renewal (like if you make a configuration change that breaks the renewal process)).</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Saving debug log to <span>/</span>var<span>/</span>log<span>/</span>letsencrypt<span>/</span>letsencrypt.log Enter email address <span>(</span>used <span>for</span> urgent renewal and security notices<span>)</span> <span>(</span>Enter <span>'c'</span> to cancel<span>)</span>: webmaster<span>@</span>fossdle.org   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please <span>read</span> the Terms of Service at https:<span>//</span>letsencrypt.org<span>/</span>documents<span>/</span>LE-SA-v1.3-September-<span>21</span>-<span>2022</span>.pdf. You must agree <span>in</span> order to register with the ACME server. Do you agree? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <span>(</span>Y<span>)</span>es<span>/</span><span>(</span>N<span>)</span>o: y   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Would you be willing, once your first certificate is successfully issued, to share your email address with the Electronic Frontier Foundation, a founding partner of the Let<span>'s Encrypt project and the non-profit organization that develops Certbot? We'</span>d like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <span>(</span>Y<span>)</span>es<span>/</span><span>(</span>N<span>)</span>o: y Account registered. Requesting a certificate <span>for</span> <span>[</span>nextcloud domain<span>]</span>   Successfully received certificate. Certificate is saved at: <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>nextcloud domain<span>]</span><span>/</span>fullchain.pem Key is saved at: <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>nextcloud domain<span>]</span><span>/</span>privkey.pem This certificate expires on <span>(</span>some future <span>date</span><span>)</span>. These files will be updated when the certificate renews. Certbot has <span>set</span> up a scheduled task to automatically renew this certificate <span>in</span> the background.   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - If you like Certbot, please consider supporting our work by: <span>*</span> Donating to ISRG <span>/</span> Let<span>'s Encrypt: https://letsencrypt.org/donate * Donating to EFF: https://eff.org/donate-le - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span></pre></div></div> <p>Ideally, you'll see a message like the above. If not, and there's an error, the error messages they provide are usually very useful and accurate. Fix the problem and try again. Note, your SSL certificate will have the name of your [nextcloud domain], even if it also provide support for [second domain name] (or third, fourth, etc.).</p> <p>Once you have a Let's Encrypt certificate, you can update our NGINX configuration:</p> <p><code>sudo $EDIT /etc/nginx/sites-available/[nextcloud domain]</code></p> <p>and swap all occurrences of</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre> ssl_certificate <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key; <span># ssl_certificate /etc/letsencrypt/live/[nextcloud domain]/fullchain.pem;</span> <span># ssl_certificate_key /etc/letsencrypt/live/[nextcloud domain]/privkey.pem;</span></pre></div></div> <p>to</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;</span> <span># ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;</span> ssl_certificate <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>nextcloud domain<span>]</span><span>/</span>fullchain.pem; ssl_certificate_key <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>nextcloud domain<span>]</span><span>/</span>privkey.pem;</pre></div></div> <p>which enables your new domain-specific SSL certificate. Check that NGINX is happy with your change:</p> <p><code>sudo nginx -t</code></p> <p>and if so,</p> <p><code>sudo service nginx reload</code></p> <p>You domain should now be enabled for <code>https://</code> access. Note that going to <code>http://[nextcloud domain]</code> should automatically redirect you to <code>https://[nextcloud domain]</code> because you care about your user's security! :grin:</p> <p>Now you'll have to repeat the same process for the [onlyoffice domain]. When that's done... Onward!</p> <h2><a id="user-content-prepare-your-docker-compose-host" href="#prepare-your-docker-compose-host" name="prepare-your-docker-compose-host" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Prepare your Docker Compose host</h2> <p>We make use of the NextCloud community's stable Docker container which they keep (more or less) up-to-date. Similarly, the OnlyOffice developers maintain a Docker container, too. We will run them both on this same server as separate services via Docker Compose. The two sets of Docker containers will look like this:</p> <ol><li>a suite of NextCloud containers:</li> </ol><ul><li>the main PHP-FPM container (which provides most of the functionality for NextCloud using the PHP scripting engine,</li> <li>an identical container to the PHP one which runs the cron service (which does periodic administrative tasks relevant to NextCloud)</li> <li>a Redis container (which provides performance improving caching for NextCloud), and</li> <li>an Nginx webserver container which makes it easier to manage the configuration and paths of the NextCloud instance. It means that on the hosting server, we only need to run a proxying web server, which is easy.</li> </ul><ol start="2"><li>the single OnlyOffice container which, despite the Docker convention of each container running only a single services, runs the whole OnlyOffice stack, which includes PostgreSQL, Nginx, Rabbit-MQ, Python, and NodeJS.</li> </ol><p>The way I prefer to implement this set of containers is to use:</p> <p><code>sudo apt install docker-compose</code></p> <p>to set up the entire Docker and Docker Compose system on your server. If you desire, you can also set up a <a href="https://docs.docker.com/compose/install/">newer version of Docker Compose</a>, namely the 2.x series. The command line interface and feedback on actions is nicer with this newer version, but otherwise, there's little difference (a few other services I run require 2.x, like <a href="https://github.com/mailcow/mailcow-dockerized">Mailcow</a>, so I've had experience with both).</p> <p>Then set up a place for your Docker containers and the associated persistent data (your Docker containers should hold no important data - you should be able to delete and recreate them entirely without losing any important data or configuration):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>sudo</span> <span>mkdir</span> <span>/</span>home<span>/</span>docker <span>sudo</span> <span>mkdir</span> <span>/</span>home<span>/</span>docker<span>/</span>nextcloud <span>sudo</span> <span>mkdir</span> <span>/</span>home<span>/</span>docker<span>/</span>onlyoffice <span>sudo</span> <span>mkdir</span> <span>/</span>home<span>/</span>data <span>sudo</span> <span>mkdir</span> <span>/</span>home<span>/</span>data<span>/</span>nextcloud <span>sudo</span> <span>mkdir</span> <span>/</span>home<span>/</span>data<span>/</span>nextcloud<span>/</span>nginx <span>sudo</span> <span>mkdir</span> <span>/</span>home<span>/</span>data<span>/</span>onlyofficesudo <span>chown</span> <span>-R</span> <span>${USER}</span>:<span>${USER}</span> <span>/</span>home<span>/</span>docker <span>/</span>home<span>/</span>data</pre></div></div> <p>My personal convention is to name both docker and data directories after the specific domain name of the service to which they apply - makes it easier when, for example, I have multiple instances of NextCloud on a single server. The above is intended to be straight forward for folks only running one of each - but feel free to modify for your requirements. If you do so, remember to ripple that through the rest of these instructions!</p> <h2><a id="user-content-nextcloud-install" href="#nextcloud-install" name="nextcloud-install" class="heading-permalink" aria-hidden="true" title="Permalink"></a>NextCloud Install</h2> <h3><a id="user-content-install-the-nextcloud-docker-recipe" href="#install-the-nextcloud-docker-recipe" name="install-the-nextcloud-docker-recipe" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install the NextCloud Docker recipe</h3> <p>Now we have a place to put the really key bit - the code for running NextCloud and OnlyOffice via Docker Compose. First, let's set up NextCloud (this also installs the OnlyOffice server):</p> <p><code>cd /home/docker/nextcloud</code></p> <p>You'll have to create a file, e.g via</p> <p><code>$EDIT docker-compose.yml</code></p> <p>and fill it with this (substituting the values in [] to suit your details - and changing the paths in /home/data if you've used something different than the default above!):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>version: <span>'3'</span> services: nginx: container_name: nginx-server image: nginx ports: - 127.0.0.1:<span>8080</span>:<span>80</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>nextcloud<span>/</span>nginx<span>/</span>nginx.conf:<span>/</span>etc<span>/</span>nginx<span>/</span>nginx.conf:ro - <span>/</span>home<span>/</span>data<span>/</span>nextcloud<span>/</span>nextcloud:<span>/</span>var<span>/</span>www<span>/</span>html links: - app environment: - VIRTUAL_HOST restart: unless-stopped app: container_name: app-server image: nextcloud:fpm stdin_open: <span>true</span> tty: <span>true</span> links: - redis expose: - <span>'80'</span> - <span>'9000'</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>nextcloud<span>/</span>nextcloud:<span>/</span>var<span>/</span>www<span>/</span>html environment: - <span>REDIS_HOST</span>=redis - <span>REDIS_HOST_PASSWORD</span>=<span>[</span>redis password<span>]</span> extra_hosts: - <span>'[onlyoffice domain]:[ipv4]'</span> restart: unless-stopped cron: image: nextcloud:fpm volumes: - <span>/</span>home<span>/</span>data<span>/</span>nextcloud<span>/</span>nextcloud:<span>/</span>var<span>/</span>www<span>/</span>html user: www-data entrypoint: <span>|</span> <span>bash</span> <span>-c</span> <span>'bash -s &lt;&lt;EOF trap "break;exit" SIGHUP SIGINT SIGTERM while /bin/true; do /usr/local/bin/php /var/www/html/cron.php sleep 900 done EOF'</span> restart: unless-stopped redis: image: redis:alpine command: redis-server <span>--requirepass</span> <span>[</span>redis password<span>]</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>nextcloud<span>/</span>redis:<span>/</span>data restart: unless-stopped</pre></div></div> <p>The "port" specified above, 8080, for nginx is arbitrary - I picked it to ensure it doesn't don't conflict with ports being used by other containers on my server - you can use this value if you want, or use <code>sudo netstat -punta</code> (you might need to install the package that provides netstat first, <code>sudo apt install net-tools</code>) to see what ports are currently claimed by other services on your server (if there are any) and pick one that doesn't clash! If it scroll past too fast, you can pipe it into less to allow you to scroll and search like this: <code>sudo netstat -punta | less</code> - hit "q" to exit or "/" to initiate a text search. Or, if you want verify that a specific port is not already being used, you can do this (in this case for port 8080) via <code>sudo netstat -punta | grep 8080</code> - if it returns any results, something is already listening on that port. If not, it's available.</p> <h3><a id="user-content-the-nextcloud-nginx-configuration" href="#the-nextcloud-nginx-configuration" name="the-nextcloud-nginx-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>The NextCloud Nginx configuration</h3> <p>You will also need to provide the "nginx.conf" file referenced in the nginx section of the Docker Compose configuration. Do that via</p> <p><code>$EDIT /home/data/nextcloud/nginx/nginx.conf</code></p> <p>and copy-and-paste the following incantation (you shouldn't need to change anything in this one) - there're notes in it offering some explanations:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>worker_processes auto;   error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span>error.log warn; pid <span>/</span>var<span>/</span>run<span>/</span>nginx.pid;     events <span>{</span> worker_connections <span>1024</span>; <span>}</span>     http <span>{</span> include <span>/</span>etc<span>/</span>nginx<span>/</span>mime.types; default_type application<span>/</span>octet-stream;   log_format main <span>'$remote_addr - $remote_user [$time_local] "$request" '</span> <span>'$status $body_bytes_sent "$http_referer" '</span> <span>'"$http_user_agent" "$http_x_forwarded_for"'</span>;   access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span>access.log main;   sendfile on; <span>#tcp_nopush on;</span>   keepalive_timeout <span>65</span>;   set_real_ip_from 10.0.0.0<span>/</span><span>8</span>; set_real_ip_from 172.16.0.0<span>/</span><span>12</span>; set_real_ip_from 192.168.0.0<span>/</span><span>16</span>; real_ip_header X-Real-IP;   <span>#gzip on;</span>   map <span>$http_host</span> <span>$this_host</span> <span>{</span> <span>""</span> <span>$host</span>; default <span>$http_host</span>; <span>}</span>   map <span>$http_x_forwarded_proto</span> <span>$the_scheme</span> <span>{</span> default <span>$http_x_forwarded_proto</span>; <span>""</span> <span>$scheme</span>; <span>}</span>   map <span>$http_x_forwarded_host</span> <span>$the_host</span> <span>{</span> default <span>$http_x_forwarded_host</span>; <span>""</span> <span>$this_host</span>; <span>}</span>   upstream php-handler <span>{</span> server app-server:<span>9000</span>; <span>}</span>   server <span>{</span> listen <span>80</span>;   <span># Add headers to serve security related headers</span> <span># Before enabling Strict-Transport-Security headers please read into this</span> <span># topic first.</span> <span>#add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;</span> <span>#</span> <span># WARNING: Only add the preload option once you read about</span> <span># the consequences in https://hstspreload.org/. This option</span> <span># will add the domain to a hardcoded list that is shipped</span> <span># in all major browsers and getting removed from this list</span> <span># could take several months.</span> add_header Referrer-Policy <span>"no-referrer"</span> always; add_header X-Content-Type-Options <span>"nosniff"</span> always; add_header X-Download-Options <span>"noopen"</span> always; add_header X-Frame-Options <span>"SAMEORIGIN"</span> always; add_header X-Permitted-Cross-Domain-Policies <span>"none"</span> always; add_header X-Robots-Tag <span>"noindex, nofollow"</span> always; add_header X-XSS-Protection <span>"1; mode=block"</span> always;   <span># Remove X-Powered-By, which is an information leak</span> fastcgi_hide_header X-Powered-By;   <span># Path to the root of your installation</span> root <span>/</span>var<span>/</span>www<span>/</span>html;   location = <span>/</span>robots.txt <span>{</span> allow all; log_not_found off; access_log off; <span>}</span>   <span># The following 2 rules are only needed for the user_webfinger app.</span> <span># Uncomment it if you're planning to use this app.</span> <span>#rewrite ^/.well-known/host-meta /public.php?service=host-meta last;</span> <span>#rewrite ^/.well-known/host-meta.json /public.php?service=host-meta-json last;</span>   <span># The following rule is only needed for the Social app.</span> <span># Uncomment it if you're planning to use this app.</span> <span>#rewrite ^/.well-known/webfinger /public.php?service=webfinger last;</span>   location = <span>/</span>.well-known<span>/</span>carddav <span>{</span> <span>return</span> <span>301</span> <span>$scheme</span>:<span>//</span><span>$host</span>:<span>$server_port</span><span>/</span>remote.php<span>/</span>dav; <span>}</span>   location = <span>/</span>.well-known<span>/</span>caldav <span>{</span> <span>return</span> <span>301</span> <span>$scheme</span>:<span>//</span><span>$host</span>:<span>$server_port</span><span>/</span>remote.php<span>/</span>dav; <span>}</span>   <span># set max upload size</span> client_max_body_size 10G; fastcgi_buffers <span>64</span> 4K;   <span># Enable gzip but do not remove ETag headers</span> <span>gzip</span> on; gzip_vary on; gzip_comp_level <span>4</span>; gzip_min_length <span>256</span>; gzip_proxied expired no-cache no-store private no_last_modified no_etag auth; gzip_types application<span>/</span>atom+xml application<span>/</span>javascript application<span>/</span>json application<span>/</span><span>ld</span>+json application<span>/</span>manifest+json application<span>/</span>rss+xml application<span>/</span>vnd.geo+json application<span>/</span>vnd.ms-fontobject application<span>/</span>x-font-ttf application<span>/</span>x-web-app-manifest+json application<span>/</span>xhtml+xml application<span>/</span>xml font<span>/</span>opentype image<span>/</span>bmp image<span>/</span>svg+xml image<span>/</span>x-icon text<span>/</span>cache-manifest text<span>/</span>css text<span>/</span>plain text<span>/</span>vcard text<span>/</span>vnd.rim.location.xloc text<span>/</span>vtt text<span>/</span>x-component text<span>/</span>x-cross-domain-policy;   <span># Uncomment if your server is build with the ngx_pagespeed module</span> <span># This module is currently not supported.</span> <span>#pagespeed off;</span>   location <span>/</span> <span>{</span> rewrite ^ <span>/</span>index.php; <span>}</span>   location ~ ^\<span>/</span><span>(</span>?:build<span>|</span>tests<span>|</span>config<span>|</span>lib<span>|</span>3rdparty<span>|</span>templates<span>|</span>data<span>)</span>\<span>/</span> <span>{</span> deny all; <span>}</span> location ~ ^\<span>/</span><span>(</span>?:\.<span>|</span>autotest<span>|</span>occ<span>|</span>issue<span>|</span>indie<span>|</span>db_<span>|</span>console<span>)</span> <span>{</span> deny all; <span>}</span>   location ~ ^\<span>/</span><span>(</span>?:index<span>|</span>remote<span>|</span>public<span>|</span>cron<span>|</span>core\<span>/</span>ajax\<span>/</span>update<span>|</span>status<span>|</span>ocs\<span>/</span>v<span>[</span><span>12</span><span>]</span><span>|</span>updater\<span>/</span>.+<span>|</span>oc<span>[</span>ms<span>]</span>-provider\<span>/</span>.+<span>)</span>\.php<span>(</span>?:$<span>|</span>\<span>/</span><span>)</span> <span>{</span> fastcgi_split_path_info ^<span>(</span>.+?\.php<span>)</span><span>(</span>\<span>/</span>.<span>*|</span><span>)</span>$; <span>set</span> <span>$path_info</span> <span>$fastcgi_path_info</span>; try_files <span>$fastcgi_script_name</span> =<span>404</span>; include fastcgi_params; fastcgi_param SCRIPT_FILENAME <span>$document_root</span><span>$fastcgi_script_name</span>; fastcgi_param PATH_INFO <span>$path_info</span>; <span># fastcgi_param HTTPS on;</span>   <span># Avoid sending the security headers twice</span> fastcgi_param modHeadersAvailable <span>true</span>;   <span># Enable pretty urls</span> fastcgi_param front_controller_active <span>true</span>; fastcgi_pass php-handler; fastcgi_intercept_errors on; fastcgi_request_buffering off; <span>}</span>   location ~ ^\<span>/</span><span>(</span>?:updater<span>|</span>oc<span>[</span>ms<span>]</span>-provider<span>)</span><span>(</span>?:$<span>|</span>\<span>/</span><span>)</span> <span>{</span> try_files <span>$uri</span><span>/</span> =<span>404</span>; index index.php; <span>}</span>   <span># Adding the cache control header for js, css and map files</span> <span># Make sure it is BELOW the PHP block</span> location ~ \.<span>(</span>?:css<span>|</span>js<span>|</span>woff2?<span>|</span>svg<span>|</span>gif<span>|</span>map<span>)</span>$ <span>{</span> try_files <span>$uri</span> <span>/</span>index.php<span>$request_uri</span>; add_header Cache-Control <span>"public, max-age=15778463"</span>; <span># Add headers to serve security related headers (It is intended to</span> <span># have those duplicated to the ones above)</span> <span># Before enabling Strict-Transport-Security headers please read into</span> <span># this topic first.</span> <span>#add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;" always;</span> <span>#</span> <span># WARNING: Only add the preload option once you read about</span> <span># the consequences in https://hstspreload.org/. This option</span> <span># will add the domain to a hardcoded list that is shipped</span> <span># in all major browsers and getting removed from this list</span> <span># could take several months.</span> add_header Referrer-Policy <span>"no-referrer"</span> always; add_header X-Content-Type-Options <span>"nosniff"</span> always; add_header X-Download-Options <span>"noopen"</span> always; add_header X-Frame-Options <span>"SAMEORIGIN"</span> always; add_header X-Permitted-Cross-Domain-Policies <span>"none"</span> always; add_header X-Robots-Tag <span>"noindex, nofollow"</span> always; add_header X-XSS-Protection <span>"1; mode=block"</span> always;   <span># Optional: Don't log access to assets</span> access_log off; <span>}</span>   location ~ \.<span>(</span>?:png<span>|</span>html<span>|</span>ttf<span>|</span>ico<span>|</span>jpg<span>|</span>jpeg<span>|</span>bcmap<span>|</span>mp4<span>|</span>webm<span>)</span>$ <span>{</span> try_files <span>$uri</span> <span>/</span>index.php<span>$request_uri</span>; <span># Optional: Don't log access to other assets</span> access_log off; <span>}</span> add_header Strict-Transport-Security <span>"max-age=31536000; includeSubdomains;"</span>; <span>}</span> <span>}</span></pre></div></div> <p>That should be all the configuration you need to make the NextCloud Docker containers go.</p> <h3><a id="user-content-the-onlyoffice-docker-configuration" href="#the-onlyoffice-docker-configuration" name="the-onlyoffice-docker-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>The OnlyOffice Docker configuration</h3> <p>We also need to something similar (but easier) for OnlyOffice. This is the docker-compose.yml that I use:</p> <p><code>cd /home/docker/onlyoffice</code> <code>$EDIT docker-compose.yml</code></p> <p>and copy-and-paste this into it (replacing the []):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>version: <span>'3'</span> services: onlyoffice: image: onlyoffice<span>/</span>documentserver:latest restart: unless-stopped ports: - 127.0.0.1:<span>9880</span>:<span>80</span> environment: <span># - JWT_SECRET=[onlyoffice secret]</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>onlyoffice<span>/</span>data:<span>/</span>var<span>/</span>www<span>/</span>onlyoffice<span>/</span>Data - <span>/</span>home<span>/</span>data<span>/</span>onlyoffice<span>/</span>logs:<span>/</span>var<span>/</span>log<span>/</span>onlyoffice - <span>/</span>home<span>/</span>data<span>/</span>onlyoffice<span>/</span>lib:<span>/</span>var<span>/</span>lib<span>/</span>onlyoffice - <span>/</span>home<span>/</span>data<span>/</span>onlyoffice<span>/</span>db:<span>/</span>var<span>/</span>lib<span>/</span>postgresql extra_hosts: - <span>"[nextcloud domain]:[ipv4]"</span></pre></div></div> <p>Now, we can fire it up provisionally:</p> <p><code>docker-compose up -d &amp;&amp; docker-compose logs -f</code></p> <p>We'll find the value of [onlyoffice secret] a bit later, below.</p> <p>To get back to a command prompt without killing the running Docker container execute a <code>CTRL-C</code>.</p> <h2><a id="user-content-firing-up-your-nextcloud" href="#firing-up-your-nextcloud" name="firing-up-your-nextcloud" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Firing up your NextCloud!</h2> <p>Phew - congratulations on getting here! We've reached the moment of truth where we need to see if this whole thing will work!</p> <p>We need to make sure we're back in the NextCloud Docker directory we set up:</p> <p><code>cd /home/docker/nextcloud</code></p> <p>Then you can run:</p> <p><code>docker-compose up -d &amp;&amp; docker-compose logs -f</code></p> <p>This will trigger the initial download of the docker container images you've specified in your <code>docker-compose.yml</code> file. All going well, after a few minutes (longer or shorter depending on the speed of your server's connection) you should have download the Nginx, Redis, and NextCloud Docker images, and then the script will attempt to start them (bringing them "up" in daemon mode with the -d, meaning they'll keep running even if you log out) and then, if successful, the <code>logs -f</code> command will run and show you a stream of log messages from the containers, each preceded by the container name to which it corresponds. This should help you debug any problems that occur during the process (ideally, none).</p> <p>Once you see log messages streaming past, and no obvious "container exited" or other error messages (which will usually contain the word "error" a lot), you should be able to point your browser at your selected domain name and have your fist visit to your NextCloud in your browser! Just point your web browser at https://[nextcloud domain] (replacing with your domain, of course. You should also try going to http://[nextcloud domain] (note the missing 's' from http) which should automatically <em>redirect</em> you to https://[nextcloud domain] as the reverse proxy file instructs.</p> <p>Again, to get back to a command prompt without killing the running Docker containers execute a <code>CTRL-C</code>.</p> <h3><a id="user-content-the-nextcloud-source-code-if-necessary" href="#the-nextcloud-source-code-if-necessary" name="the-nextcloud-source-code-if-necessary" class="heading-permalink" aria-hidden="true" title="Permalink"></a>The NextCloud source code (if necessary)</h3> <p><strong>Normally the source code for NextCloud's current stable version is transparently downloaded and installed by the NextCloud Docker container the first time it's instantiated</strong>. If it is, you'll see a bunch of files and directories in your <code>/home/data/nextcloud/nextcloud</code> folder. If so, you're fine and you can move on to the next step.</p> <p>If not, you can always find the most recent stable release's <a href="https://nextcloud.com/install/#instructions-server">source code here</a>. I tend to prefer the .tar.bz2 archive format, so I get it from this link: <a href="https://download.nextcloud.com/server/releases/latest.tar.bz2">https://download.nextcloud.com/server/releases/latest.tar.bz2</a> (which, fingers crossed, should remain valid indefinitely - if not, check the previous link or look for 'Download' on the <a href="https://nextcloud.com">NextCloud website</a>.).</p> <p>We need to get that file and extract it in <code>/home/data/nextcloud</code>, so do the following (if <code>wget</code> isn't already installed, get it via <code>sudo apt install wget</code>):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>cd</span> <span>/</span>home<span>/</span>data<span>/</span>nextcloud <span>wget</span> https:<span>//</span>download.nextcloud.com<span>/</span>server<span>/</span>releases<span>/</span>latest.tar.bz2 <span>tar</span> xvfj latest.tar.bz2</pre></div></div> <p>which will create a directory 'nextcloud' with the latest (stable) version of the NextCloud source code in that directory.</p> <p>Then reassert the file permissions just to be sure</p> <p><code>sudo chown -R www-data nextcloud</code></p> <p>After that, you should be able to point your browser at your domain (the containers are already running) and see if it starts the install process as it should. [/code]</p> <p>You can figure out what version that is by running:</p> <p><code>cat nextcloud/version.php | grep VersionString</code></p> <p>With my latest install, I get the result:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>$</span>OC_VersionString = <span>'26.0.2'</span>;</pre></div></div> <p>Note, I tend to hold on to install archives for safety's sake, so I generally do the following to tidy up (still in the nextcloud data directory), replacing [version] with the advertised most recent stable version of NextCloud (26.0.2 in my case):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>mkdir</span> attic <span>mv</span> latest.tar.bz2 attic<span>/</span>nextcloud_<span>[</span>version<span>]</span>.tar.bz2</pre></div></div> <p>Now you've got the source code for NextCloud where you containers are configured to look for it!</p> <h2><a id="user-content-configuring-database-access" href="#configuring-database-access" name="configuring-database-access" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring database access</h2> <p>On doing so, if all is well, you should be directed through the database set up process for your NextCloud instance. You'll be asked for your database details, which should be:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>database IP: 172.17.0.1 - this is the default IP of the Docker host server. database name: <span>[</span>db name<span>]</span> database user: <span>[</span>db user<span>]</span> database password: <span>[</span>db password<span>]</span></pre></div></div> <h2><a id="user-content-configuring-the-admin-user" href="#configuring-the-admin-user" name="configuring-the-admin-user" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring the Admin user</h2> <p>Once that's set and working, NextCloud will install all the relevant database tables and initial data. You'll be asked to set up an admin user account, which can be "admin" and some strong password you create (you can use the <code>pwgen</code> utility you used earlier) - I'd recommend recording it somewhere. <em>I would not recommend making your own account, in your name, the main admin account</em>. Instead, I recommend creating a second account, <em>with administrator privileges</em>, for yourself, but leave the admin account purely for administrative activities.</p> <h2><a id="user-content-configuring-outgoing-email" href="#configuring-outgoing-email" name="configuring-outgoing-email" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring Outgoing Email</h2> <p>To allow your NextCloud instance to send outgoing email, so that your site can alert you to security updates that need to be applied, or so that any of your NextCloud users can request a password reset if they've forgot theirs. For this, you'll need the authenticating SMTP account details from the start of this process. You'll need:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>SMTP server : an IP address or a domain name SMTP username: a username or an email address SMTP password: a strong password already configured <span>for</span> the username on that server SMTP <span>login</span> security: whether <span>login</span> is via TLS, SSL, or unsecure <span>(</span><span>!!</span><span>)</span>, and SMTP <span>login</span> method: plain, encrypted, <span>"login"</span> or some other value.</pre></div></div> <p>You should be able to test your email settings to make sure the details you've entered are valid. If you need to adjust these settings later, you can go to the admin menu (top right of the web browser interface) and go to Admin-&gt;Basic Settings - should have a path of <code>https://[nextcloud domain]/settings/admin</code>.</p> <h2><a id="user-content-setting-up-onlyoffice" href="#setting-up-onlyoffice" name="setting-up-onlyoffice" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Setting up OnlyOffice</h2> <p>The OnlyOffice server should already be running - if you point your browser at <code>https://[onlyoffice domain]</code> you should see a page like the attached screenshot with the OnlyOffice Logo and a title of "OnlyOffice Docs Community Edition".</p> <h2><a id="user-content-configuring-onlyoffice-integration-with-nextcloud" href="#configuring-onlyoffice-integration-with-nextcloud" name="configuring-onlyoffice-integration-with-nextcloud" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring OnlyOffice Integration with NextCloud</h2> <p>To configure your NextCloud to use your OnlyOffice, the OnlyOffice will require that NextCloud knows its "secret". To generate the secret, run this in <code>/home/docker/onlyoffice</code>:</p> <p><code>docker-compose exec onlyoffice /var/www/onlyoffice/documentserver/npm/json -f /etc/onlyoffice/documentserver/local.json 'services.CoAuthoring.secret.session.string'</code></p> <p>The resulting <em>secret</em> string, which will look something like <code>QC7QmEqUpXmmnwXZcvBQ</code> needs to be added to your OnlyOffice docker-compose.yml file to ensure that the same code is used everytime you start OnlyOffice (if it isn't set, it'll be generated each time you restart OnlyOffice and your NextCloud will need a different 'secret' each time - a major inconvenience).</p> <p><code>$EDIT /home/docker/onlyoffice/docker-compose.yml</code></p> <p>Add it in place of [onlyoffice secret] <em>and also remove the '#' that's commenting out the line</em> - again, thanks to Stephen Harlow for pointing out that this is required! Then restart OnlyOffice via</p> <p><code>docker-compose up -d</code></p> <p>Docker will see that the container's configuration has changed and will restart the container.</p> <p>Next you need to be logged into your NextCloud as an administartive user (your own user is fine if you've given it admin privileges).</p> <p>You should have an "admin" menu (assuming you've created your user with Administrator privileges) at the top right of the web interface. If you go to Apps, you can install the new "Hub bundle" available under the "App bundles" option (see attached image). If you don't want the whole bundle you can just use the search box to search for "OnlyOffice" or go to the "Office &amp; text" App category and enable the OnlyOffice "official" app, at which point it will automatically download the latest version of the connector app and install it (it should appear in your /home/data/nextcloud/apps directory)</p> <p>Once you've done that, go to your top right menu again, selecting Admin, and you should see "OnlyOffice" as an option in the left column (which starts with "Basic settings"). Selecting that, you'll need to enter the following:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>"Document Editing Service address"</span>: https:<span>//</span><span>[</span>onlyoffice domain<span>]</span> <span>"Secret key"</span>: <span>[</span>onlyoffice secret<span>]</span></pre></div></div> <p>You don't need the 'advanced settings'.</p> <p>When you're done, click "Save".</p> <p>You can also select formats you'd like OnlyOffice to open and edit files of those types are clicked or created. I've selected the following: doc, docx, odp, ods, odt, ppt, pptx, xls, xlsx, and in the second section: csv and txt.</p> <p>You can also make other editor customisations as you desire. The only Editor customisation setting I haven't selected is "Display Chat menu button" because NextCloud Hub provides an integrated Chat service, making this one within OnlyOffice an unnecessary distraction.</p> <p>Once finished configuring, you should have the ability to go back to the home of your NextCloud install, which should show you your top-level folders. If you click the "+" next to the home icon (top left of the folder pane) you should now have the option to create (in addition to "Upload file", "New folder", "New text file") a "New Document", "New Spreadsheet", and "New Presentation". Clicking those should give you the OnlyOffice interface for the designated content type.</p> <p>Similarly, you can use the "Upload file" to upload a document in a format that is supported by OnlyOffice. Once uploaded, clicking on the filename should open it for editing in the appropriate OnlyOffice interface.</p> <p>It is saved as it is changed, so you shouldn't need to save it explicitly.</p> <h2><a id="user-content-keeping-the-whole-thing-up-to-date" href="#keeping-the-whole-thing-up-to-date" name="keeping-the-whole-thing-up-to-date" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Keeping the whole thing up-to-date</h2> <p>So, as you're no doubt aware, both NextCloud and OnlyOffice are always being improved and updated. I certainly encourage you to keep your installations up-to-date.</p> <p>While you'll periodically be alerted that NextCloud apps have available updates (these can be upgraded through the browser interface) updates to the NextCloud and OnlyOffice systems themselves need to be undertaken by upgrading their containers. Luckily it's easy to do although I strongly urge you to ensure you have a very recent backup of both database and uploaded files - they're the files in <code>/home/data/nextcloud/data</code> and <code>/home/data/onlyoffice/</code> (note, backups of OnlyOffice are complicated somewhat by the fact that you can't reliably back up running PostgreSQL instance simply by backing up its files - see a solution below). Prior to updating my containers, I normally create an archive of my NextCloud code in the event the upgrade goes wrong and I need to recover quickly. I do it like this. In my <code>/home/data/nextcloud</code> directory, I create a new 'attic' directory:</p> <p><code>cd /home/data/nextcloud &amp;&amp; sudo mkdir attic</code></p> <p>then I create an archive of my entire NextCloud source directory in the attic directory:</p> <p><code>DATE=</code>date +%Y%m%d<code> &amp;&amp; sudo tar cvfz attic/nextcloud-${DATE}.tgz --exclude "data" nextcloud</code></p> <p>If you need to recover files from it, you can untar it (assuming you have sufficient disk space! Best to be mindful of that!) via</p> <p><code>sudo mkdir tmp &amp;&amp; cd tmp &amp;&amp; tar xvfz ../attic/nextcloud-${DATE}.tgz</code></p> <p>although if you're doing it in a different SSH session (e.g. after the fact) you might have to manually enter the DATE part of the filename.</p> <p>Once you've got a backup of your NextCloud source code, updating the container should be as easy as either doing another</p> <p><code>docker-compose pull</code></p> <p>and then restarting the service with the new containers via</p> <p><code>docker-compose up -d</code></p> <p>which will remove any old containers (this won't remove any data you want to save if you followed the directions above! But remember to do it in the right directory!) and start up the new versions you've just pulled.</p> <p>Use <code>docker-compose logs -f</code> to watch the logs - you'll likely see useful debugging information in the unlikely event that something goes wrong in the upgrade process.</p> <h2><a id="user-content-backing-up-nextcloud" href="#backing-up-nextcloud" name="backing-up-nextcloud" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Backing up NextCloud</h2> <p>To back up your instance on your server, you need two things: a file system backup of your <code>/home/data/nextcloud</code> directory, and database dumps of your database.</p> <p>There're lots of ways to back up your files (I've recently updated to using a system called Restic to make off-server incremental encrypted backups - I plan to document this in a future howto! - although there're other documented approaches - leave a comment below if you'd like to learn more about my approach!).</p> <p>Backing up your MariaDB databases is as easy installing automysqlbackups:</p> <p><code>sudo apt install automysqlbackups</code></p> <p>You'll find daily versioned dumps of your MariaDB database(s) in /var/lib/automysqlbackups on your VM host's filesystem. To run an ad hoc backup (which will replace the previous backup from that day, if there is one) just run</p> <p><code>sudo automysqlbackups</code></p> <h2><a id="user-content-backup-onlyoffice" href="#backup-onlyoffice" name="backup-onlyoffice" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Backup OnlyOffice</h2> <p>Along with backing up the files in your <code>/home/data/onlyoffice</code> directory, you'll also want a proper "dump" of your PostgreSQL backup (you can write simple bash scripts to do this regularly, automatically), particularly prior to doing an upgrade (to allow for recovery if something goes badly wrong, which is always possible). You can achieve this by going to</p> <p><code>cd /home/docker/onlyoffice</code></p> <p>and running this</p> <p><code>DATE=$(date +%Y%m%d) &amp;&amp; FILE=/home/data/onlyoffice/backup/fullbackup-${DATE}.sql &amp;&amp; docker-compose exec onlyoffice sudo -u postgres pg_dumpall &gt; ${FILE} &amp;&amp; gzip ${FILE}</code></p> <p>which will assign the current date to DATE, the relevant filename to FILE, and then put the backup SQL into a dated file called $FILE and compress the result with gzip :)</p> <p>At some point, I'll modify my normal versioned PostgreSQL-in-a-Docker-Container dated database backup scripts to cater for this solution and make the result available on <a href="https://git.oeru.org">https://git.oeru.org</a> - it'll probably be a small modification to this script: <a href="https://git.oeru.org/oeru/docker-compose-dbbackup">https://git.oeru.org/oeru/docker-compose-dbbackup</a> in case someone wants to beat me to it!</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=56&amp;2=field_blog_comments&amp;3=comment" token="TVFnCS25aCQNEoXgVru9brw8N6CW8SDeKyXkrmV0rJI"></drupal-render-placeholder> </div> </section> Sun, 28 May 2023 23:50:43 +0000 dave 56 at http://tech.oeru.org Installing and Upgrading Moodle with Docker Compose on Ubuntu 22.04 http://tech.oeru.org/installing-and-upgrading-moodle-docker-compose-ubuntu-2204 <span class="field field--name-title field--type-string field--label-hidden">Installing and Upgrading Moodle with Docker Compose on Ubuntu 22.04</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--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--_204"> <span class="field__item-wrapper"><a href="/taxonomy/term/85" hreflang="en">22.04</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--docker"> <span class="field__item-wrapper"><a href="/taxonomy/term/16" hreflang="en">docker</a></span> </div> <div class="field__item field__item--mysql"> <span class="field__item-wrapper"><a href="/taxonomy/term/83" hreflang="en">mysql</a></span> </div> <div class="field__item field__item--mariadb"> <span class="field__item-wrapper"><a href="/taxonomy/term/48" hreflang="en">mariadb</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--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" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Mon 23/05/2022 - 11:13</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/2022-05/Moodle-DNS_config-cropped-highlighted.png?itok=hZ5bW5gV" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Showing the specific &#039;CNAME&#039; line pointing moodletest.milll.ws to the server called sandbox.milll.ws.&quot;}" role="button" title="Showing the specific &#039;CNAME&#039; line pointing moodletest.milll.ws to the server called sandbox.milll.ws." data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Showing the specific &#039;CNAME&#039; line pointing moodletest.milll.ws to the server called sandbox.milll.ws.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moodle-DNS_config-cropped-highlighted.png?itok=9bxNzleE" width="220" height="87" alt="Showing the specific &#039;CNAME&#039; line pointing moodletest.milll.ws to the server called sandbox.milll.ws." loading="lazy" 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/2022-05/Moodle-DNS_config-2022-05-23-milll.ws%20-%20Metaname.png?itok=v3q9RKz5" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Configuring a Domain Name (moodletest.milll.ws) for this Moodle instance.&quot;}" role="button" title="Configuring a Domain Name (moodletest.milll.ws) for this Moodle instance." data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Configuring a Domain Name (moodletest.milll.ws) for this Moodle instance.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moodle-DNS_config-2022-05-23-milll.ws%20-%20Metaname.png?itok=HHzZlKDm" width="220" height="165" alt="Configuring a Domain Name (moodletest.milll.ws) for this Moodle instance." loading="lazy" 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/2022-05/Moode-Install-page.png?itok=8Iq6c32v" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Initiating the Moodle install process (no CSS). &quot;}" role="button" title="Initiating the Moodle install process (no CSS). " data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Initiating the Moodle install process (no CSS). &quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moode-Install-page.png?itok=fLzRGa4w" width="220" height="82" alt="Initiating the Moodle install process (no CSS). " loading="lazy" 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/2022-05/Moode-Install-MariaDB_driver-page.png?itok=Aex-biDT" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Moodle database driver selection page (no CSS).&quot;}" role="button" title="The Moodle database driver selection page (no CSS)." data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Moodle database driver selection page (no CSS).&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moode-Install-MariaDB_driver-page.png?itok=yV9Eu1aR" width="220" height="82" alt="The Moodle database driver selection page (no CSS)." loading="lazy" 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/2022-05/Moode-Install-MariaDB_install-page.png?itok=jJbDC0wz" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Moodle install database configuration page (no CSS).&quot;}" role="button" title="The Moodle install database configuration page (no CSS)." data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Moodle install database configuration page (no CSS).&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moode-Install-MariaDB_install-page.png?itok=MFiSpaGO" width="220" height="82" alt="The Moodle install database configuration page (no CSS)." loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-6"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2022-05/Moode-Installing-page.png?itok=DRZOwdri" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Moodle install page, missing CSS. &quot;}" role="button" title="The Moodle install page, missing CSS. " data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Moodle install page, missing CSS. &quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moode-Installing-page.png?itok=NbWTqjli" width="220" height="113" alt="The Moodle install page, missing CSS. " loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-7"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2022-05/Moode-Install-admin-page-withCSS.png?itok=M2wy0fSS" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The completed Moodle install confirmation page, with CSS enabled. &quot;}" role="button" title="The completed Moodle install confirmation page, with CSS enabled. " data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The completed Moodle install confirmation page, with CSS enabled. &quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moode-Install-admin-page-withCSS.png?itok=zvN4fPDG" width="179" height="220" alt="The completed Moodle install confirmation page, with CSS enabled. " loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-8"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2022-05/Moode-Install-site_registration.png?itok=_n0ff61n" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Once installed, Moodle asks you to register your site so the Moodle community can count installs and you can get updates about new Moodle releases.&quot;}" role="button" title="Once installed, Moodle asks you to register your site so the Moodle community can count installs and you can get updates about new Moodle releases." data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Once installed, Moodle asks you to register your site so the Moodle community can count installs and you can get updates about new Moodle releases.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moode-Install-site_registration.png?itok=5VtSCN6p" width="220" height="168" alt="Once installed, Moodle asks you to register your site so the Moodle community can count installs and you can get updates about new Moodle releases." loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-9"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2022-05/Moode-Installed-dashboard.png?itok=pyXdyK4T" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The first look at the Moodle admin dashboard after installation. &quot;}" role="button" title="The first look at the Moodle admin dashboard after installation. " data-colorbox-gallery="gallery-field_image-Xqjii2Glang" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The first look at the Moodle admin dashboard after installation. &quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Moode-Installed-dashboard.png?itok=ERLiwXLf" width="220" height="168" alt="The first look at the Moodle admin dashboard after installation. " loading="lazy" 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><strong>Note: work in progress</strong></p> <p><a href="https://moodle.org">Moodle</a> is probably (there's little agreement among the pundits) the <a href="https://webtechsurvey.com/technology-type/learning-management-system">market-leading</a> Free and Open Source Software (FOSS) Learning Management System (LMS). It's pretty much everywhere. Here's how you can set up and maintain your own Moodle instance(s).</p> <ul class="table-of-contents"><li> <p><a href="#prerequisites">Prerequisites</a></p> </li> <li> <p><a href="#installing-moodle-on-ubuntu-2204-with-docker-compose">Installing Moodle on Ubuntu 22.04 with Docker Compose</a></p> <ul><li> <p><a href="#configure-a-domain-name">Configure a domain name</a></p> </li> <li> <p><a href="#install-the-moodle-source-code">Install the Moodle source code</a></p> </li> <li> <p><a href="#configure-docker-compose-to-host-moodle">Configure Docker Compose to host Moodle</a></p> </li> <li> <p><a href="#create-a-secure-reverse-proxy-configuration">Create a secure reverse-proxy configuration</a></p> </li> <li> <p><a href="#installing-moodle">Installing Moodle</a></p> </li> </ul></li> <li> <p><a href="#backing-up-your-data">Backing up your data</a></p> <ul><li> <p><a href="#mariadb-database-dumps">MariaDB database dumps</a></p> </li> </ul></li> <li> <p><a href="#upgrading-moodle-with-git">Upgrading Moodle with Git</a></p> </li> </ul><h2><a id="user-content-prerequisites" href="#prerequisites" name="prerequisites" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Prerequisites</h2> <p>If you would like to host your own (and we certainly encourage it!) using the same approach we use at the <a href="https://oerfoundation.org">OER Foundation</a> here is how you get started:</p> <ol><li>Set up a Virtual Private Server (VPS) and <a href="/node/42">configure it to run Ubuntu Linux and Docker Compose</a>.</li> <li>Make sure any <a href="/node/49">administrative users can log into the VPS securely</a> via Secure Shell (SSH).</li> </ol><p>From that point, use the following process.</p> <h2><a id="user-content-installing-moodle-on-ubuntu-2204-with-docker-compose" href="#installing-moodle-on-ubuntu-2204-with-docker-compose" name="installing-moodle-on-ubuntu-2204-with-docker-compose" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Installing Moodle on Ubuntu 22.04 with Docker Compose</h2> <p>The process for completing the install involves</p> <ol><li> <p>setting up a domain name (or subdomain) to point at the server so people can easily find the Moodle site,</p> </li> <li> <p>installing all the relevant code via '<a href="https://en.wikipedia.org/wiki/Git">Git</a>' on the VPS' file system,</p> </li> <li> <p>configuring the Docker Compose file, defining the set of Docker containers comprising this site's moving parts, and 'pulling' the component containers (to download the current versions of each to our VPS),</p> </li> <li> <p>adding the 'default configuration' for the Dockerised NGINX instance, so it knows how to server Moodle from the adjacent containers,</p> </li> <li> <p>configuring the NGINX webserver running on the VPS to act as a '<a href="https://en.wikipedia.org/wiki/Reverse_proxy">reverse proxy</a>' as well as using to generate a Let's Encrypt Secure Sockets Layer (SSL) certificate to encrypt and otherwise secure users' interactions with the Moodle instance, and finally</p> </li> <li> <p>configuring the Moodle instance itself.</p> </li> </ol><h3><a id="user-content-configure-a-domain-name" href="#configure-a-domain-name" name="configure-a-domain-name" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configure a domain name</h3> <p>Configuring a domain name requires us to register a domain with a 'domain registrar' - we use one called <a href="https://metaname.net">Metaname</a>, based here in Christchurch, New Zealand - and using the registrar's browser based configuration system to create a 'zone file' which defines the server to which the domain points. Each registrar is likely to have their own bespoke zone file configuration tools, so consider this to be just one example.</p> <p>Our convention at the OERF is to allocate a default domain name to each VPS we create. Each one hosts one or more Free and Open Source Software services. Each of those services has its own domain name. Usually we designate "A" and "AAAA" records for the default domain name pointing to the IPv4 and IPv6 addresses given to the VPS by the cloud services provider.</p> <p>For the purposes of this tutorial, we'll create a site called <strong>moodletest.milll.ws</strong> which is the <code>moodletest</code> subdomain of the <code>milll.ws</code> (Ministry for Education, Sport, and Culture's 'innovative Lifelong Learning Lab', aka MiLLL, in (western) Samoa). We use a "CNAME" to point the subdomain at the VPS' official name, <code>sandbox.milll.ws</code>, which is how its A and AAAA records identify it (via its IPv4 and IPv6 addresses).</p> <p>So our "Fully Qualified Domain Name" (FQDN) is <code>moodletest.milll.ws</code>.</p> <p>To prepare for the rest of this tutorial, we're going to set a variable for your selected domain name. Do that by typing this at your command prompt. It will set this variable ''for this session'':</p> <p><code>FQDN=your domain name</code></p> <p>in our case, it'd be</p> <p><code>FQDN=moodletest.milll.ws</code></p> <p>The following will assuming that this value is set correctly.</p> <h3><a id="user-content-install-the-moodle-source-code" href="#install-the-moodle-source-code" name="install-the-moodle-source-code" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install the Moodle source code</h3> <p>Before we can install the Moodle source code, we have to prepare a sensible place for it to live. We do that by creating suitable directories for both the Docker configurations and the Moodle instance data and configuration as follows:</p> <p><code>sudo mkdir -p /home/docker/${FQDN} /home/data/${FQDN}</code></p> <p>That creates two directories as you can see. We need to go into the data directory:</p> <p><code>cd /home/data/${FQDN}</code></p> <p>and in there, we'll execute this git command to download the entire Moodle codebase:</p> <p><code>sudo git clone https://github.com/moodle/moodle.git src</code></p> <p>When that finishes, go into the source directory:</p> <p><code>cd src</code></p> <p>Here, we'll have to execute this new command, which we use to declare this a 'safe' git directory, which helps protect us from nefarious parties running git commands in dangerous places if they can get access to our server via some other security vulnerability...</p> <p><code>sudo git config --global --add safe.directory /home/data/${FQDN}</code></p> <p>Then we can run this comman:</p> <p><code>git branch -a</code></p> <p>This should give you a list of all the Moodle repository version 'tags'.</p> <p>In this case, we're going to install a stable (but not the latest) version of Moodle - to do that we tell git what tag we're going to track:</p> <p><code>sudo git branch --track MOODLE_310_STABLE origin/MOODLE_310_STABLE</code></p> <p>And then, to make sure that our Docker container's nginx webserver can read these files (they need to be owned by the <code>www-data</code> users, and I (user 'dave') can run git commands without needing to use <code>sudo</code>. Replace with your own user name here!</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>sudo</span> <span>chown</span> <span>-R</span> www-data:dave ..<span>/</span>src <span>sudo</span> <span>chmod</span> <span>-R</span> g+<span>w</span> ..<span>/</span>src <span>ls</span> <span>-l</span> <span>git status</span> <span>git checkout</span> MOODLE_310_STABLE</pre></div></div> <h3><a id="user-content-configure-docker-compose-to-host-moodle" href="#configure-docker-compose-to-host-moodle" name="configure-docker-compose-to-host-moodle" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configure Docker Compose to host Moodle</h3> <p>The first step to running anything with Docker Compose - which helps you combine a set of Docker containers together into a useful, maintainable service - is to create <code>docker-compose.yml</code> file. It usually contains the full 'recipe' for the service.</p> <p>Go to the relevant docker directory and create and edit the file:</p> <p><code>cd /home/docker/${FQDN}</code></p> <p><code>sudo nano docker-compose.yml</code></p> <p>And copy and past in this content (with <strong>FQDN</strong> occurrences and <strong>[secret password]</strong> placeholders to be replaced appropriately, of course):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>version: <span>'3'</span>   services: mariadb: image: mariadb:<span>10</span> container_name: db environment: MYSQL_RANDOM_ROOT_PASSWORD: <span>1</span> MYSQL_DATABASE: moodle MYSQL_USER: moodle MYSQL_PASSWORD: <span>[</span>secret MariaDB password<span>]</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>FQDN<span>/</span>mariadb:<span>/</span>var<span>/</span>lib<span>/</span>mysql moodle: image: oeru<span>/</span>moodle:php74-fpm environment: MOODLE_DOMAIN: FQDN MOODLE_DB_HOST: mariadb MYSQL_PORT_3306_TCP: <span>3306</span> MOODLE_DB_NAME: moodle MOODLE_DB_USER: moodle MOODLE_DB_PASSWORD: <span>[</span>secret MariaDB password<span>]</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>FQDN<span>/</span>src:<span>/</span>var<span>/</span>www<span>/</span>html - <span>/</span>home<span>/</span>data<span>/</span>FQDN<span>/</span>data:<span>/</span>var<span>/</span>www<span>/</span>moodledata restart: unless-stopped depends_on: - mariadb cron: image: oeru<span>/</span>moodle-cron:php74-fpm environment: MOODLE_DOMAIN: FQDN MOODLE_DB_HOST: mariadb MYSQL_PORT_3306_TCP: <span>3306</span> MOODLE_DB_NAME: moodle MOODLE_DB_USER: moodle MOODLE_DB_PASSWORD: <span>[</span>secret MariaDB password<span>]</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>FQDN<span>/</span>src:<span>/</span>var<span>/</span>www<span>/</span>html - <span>/</span>home<span>/</span>data<span>/</span>FQDN<span>/</span>data:<span>/</span>var<span>/</span>www<span>/</span>moodledata restart: unless-stopped depends_on: - mariadb nginx: image: nginx depends_on: - moodle ports: - 127.0.0.1:<span>8080</span>:<span>80</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span>FQDN<span>/</span>nginx:<span>/</span>etc<span>/</span>nginx<span>/</span>conf.d - <span>/</span>home<span>/</span>data<span>/</span>FQDN<span>/</span>src:<span>/</span>var<span>/</span>www<span>/</span>html restart: unless-stopped</pre></div></div> <p>And, naturally, save the file.</p> <p>Note, the last stanza of the <code>docker-compose.yml</code> file, defining the NGINX component, specifies a port number on the host machine (i.e. 'localhost' or 127.0.0.1) of 8080, which in turn points to port 80 on the NGINX container. If this is the only Docker Compose configuration on your server, then 8080 is probably an avaiable port to use, but if you server has lots of other Docker Compose (or other sorts of services on it) port 8080 might not be available. You can check by running</p> <p><code>sudo netstat -punta</code></p> <p>If <code>netstat</code> isn't installed, you can install it via <code>sudo apt install net-tools</code>.</p> <p>Running this command will give output that looks something like this:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Active Internet connections <span>(</span>servers and established<span>)</span> Proto Recv-Q Send-Q Local Address Foreign Address State PID<span>/</span>Program name tcp <span>0</span> <span>0</span> 0.0.0.0:<span>80</span> 0.0.0.0:<span>*</span> LISTEN <span>11199</span><span>/</span>nginx: master tcp <span>0</span> <span>0</span> 0.0.0.0:<span>25</span> 0.0.0.0:<span>*</span> LISTEN <span>60021</span><span>/</span>master tcp <span>0</span> <span>0</span> 0.0.0.0:<span>22</span> 0.0.0.0:<span>*</span> LISTEN <span>59473</span><span>/</span>sshd: <span>/</span>usr<span>/</span>sb tcp <span>0</span> <span>0</span> 127.0.0.53:<span>53</span> 0.0.0.0:<span>*</span> LISTEN <span>11263</span><span>/</span>systemd-resol tcp <span>0</span> <span>0</span> 127.0.0.1:<span>8080</span> 0.0.0.0:<span>*</span> LISTEN <span>94497</span><span>/</span>docker-proxy tcp <span>0</span> <span>0</span> 127.0.0.1:<span>6010</span> 0.0.0.0:<span>*</span> LISTEN <span>13691</span><span>/</span>sshd: dave<span>@</span>pt tcp <span>0</span> <span>0</span> 127.0.0.1:<span>6011</span> 0.0.0.0:<span>*</span> LISTEN <span>13097</span><span>/</span>sshd: dave<span>@</span>pt tcp <span>0</span> <span>0</span> 147.182.232.12:<span>22</span> 203.118.159.63:<span>39248</span> ESTABLISHED <span>13044</span><span>/</span>sshd: dave <span>[</span>p tcp <span>0</span> <span>0</span> 147.182.232.12:<span>22</span> 202.4.38.248:<span>54655</span> ESTABLISHED <span>94900</span><span>/</span>sshd: william tcp <span>0</span> <span>0</span> 147.182.232.12:<span>22</span> 202.4.38.248:<span>53108</span> ESTABLISHED <span>91961</span><span>/</span>sshd: sialofi tcp <span>0</span> <span>52</span> 147.182.232.12:<span>22</span> 203.118.159.63:<span>39650</span> ESTABLISHED <span>13638</span><span>/</span>sshd: dave <span>[</span>p tcp <span>0</span> <span>0</span> 147.182.232.12:<span>22</span> 202.4.38.248:<span>62921</span> ESTABLISHED <span>92704</span><span>/</span>sshd: eleanor tcp6 <span>0</span> <span>0</span> :::<span>80</span> :::<span>*</span> LISTEN <span>11199</span><span>/</span>nginx: master tcp6 <span>0</span> <span>0</span> :::<span>25</span> :::<span>*</span> LISTEN <span>60021</span><span>/</span>master tcp6 <span>0</span> <span>0</span> :::<span>22</span> :::<span>*</span> LISTEN <span>59473</span><span>/</span>sshd: <span>/</span>usr<span>/</span>sb tcp6 <span>0</span> <span>0</span> ::<span>1</span>:<span>6010</span> :::<span>*</span> LISTEN <span>13691</span><span>/</span>sshd: dave<span>@</span>pt tcp6 <span>0</span> <span>0</span> ::<span>1</span>:<span>6011</span> :::<span>*</span> LISTEN <span>13097</span><span>/</span>sshd: dave<span>@</span>pt udp <span>0</span> <span>0</span> 127.0.0.53:<span>53</span> 0.0.0.0:<span>*</span> <span>11263</span><span>/</span>systemd-resol</pre></div></div> <p>You'll notice that in this case, the 7th line says</p> <p><code>tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN 94497/docker-proxy</code></p> <p>which means that port 8080 is already in use with 127.0.0.1 on the server and trying to start my containers with that port defined would result in a 'port unavailable' error.</p> <p>Here's a quick one-liner allowing you to check whether any particular port is in use right now (replace '8080' with whatever port number you're checking):</p> <p><code>sudo netstat -punta | grep '8080'</code></p> <p>if it returns nothing, that port is ''currently'' unused. If it returns a line like</p> <p><code>tcp 0 0 127.0.0.1:8080 0.0.0.0:* LISTEN 94497/docker-proxy</code></p> <p>it's in use and you'll need to choose another port, e.g. 8081 or 8070 or something else. Ports over 8000 are unlikely to be used by most normal software so should be fair game. You can verify that your selected port is available before launching your containers.</p> <h4><a id="user-content-download-your-containers-from-docker-hub" href="#download-your-containers-from-docker-hub" name="download-your-containers-from-docker-hub" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Download your containers from Docker Hub</h4> <p>You should then be able to 'pull' the relevant Docker images (might take a while depending on your server's location and available bandwidth):</p> <p><code>sudo docker-compose pull</code></p> <p>Once that's done, we need to make sure that our container running NGINX webserver has a valid Moodle-specific configuration.</p> <h4><a id="user-content-configure-nginx-container-for-moodle" href="#configure-nginx-container-for-moodle" name="configure-nginx-container-for-moodle" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configure NGINX container for Moodle</h4> <p>To do that, we're going to create an NGINX default configuration for the Moodle containers by creating and editing the file:</p> <p><code>sudo nano /home/data/${FQDN}/nginx/default.conf</code></p> <p>copying and pasting in the following content. It shouldn't require any changes.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># This configuration is used by a Docker container running nginx, and is not intended to be</span> <span># directly connected to the Internet (for that we recommend using SSL on port 443).</span> <span># You should reverse proxy this container on the Docker host using whatever webserver</span> <span># you have running there.</span> <span>#</span> server <span>{</span> listen <span>80</span> default_server;   <span># default path to Mautic - from the point of view of the docker container</span> root <span>/</span>var<span>/</span>www<span>/</span>html; index index.php index.html index.htm; fastcgi_keep_conn on; <span># keep alive to the FCGI upstream</span> location <span>/</span> <span>{</span> <span># First attempt to serve request as file</span> try_files <span>$uri</span> <span>$uri</span><span>/</span>index.php;   <span># moodle rewrite rules</span> rewrite ^<span>/</span><span>(</span>.<span>*</span>.php<span>)</span><span>(</span><span>/</span><span>)</span><span>(</span>.<span>*</span><span>)</span>$ <span>/</span><span>$1</span>?<span>file</span>=<span>/</span><span>$3</span> <span>last</span>; <span>}</span> <span># php parsing</span>   location ~ ^<span>(</span>.+\.php<span>)</span><span>(</span>.<span>*</span><span>)</span>$ <span>{</span> fastcgi_split_path_info ^<span>(</span>.+\.php<span>)</span><span>(</span>.<span>*</span><span>)</span>$; fastcgi_index index.php; fastcgi_pass moodle:<span>9000</span>; include <span>/</span>etc<span>/</span>nginx<span>/</span>mime.types; include fastcgi_params; fastcgi_param PATH_INFO <span>$fastcgi_path_info</span>; fastcgi_param SCRIPT_FILENAME <span>$document_root</span><span>$fastcgi_script_name</span>; fastcgi_buffer_size 128k; fastcgi_buffers <span>256</span> 4k; fastcgi_busy_buffers_size 256k; fastcgi_temp_file_write_size 256k; client_max_body_size 100M; <span>}</span> add_header <span>'Access-Control-Allow-Origin'</span> <span>"*"</span>; <span>}</span></pre></div></div> <p>Once this is in place, you can start up the Docker containers like this (as your own user!):</p> <p><code>docker-compose up -d &amp;&amp; docker-compose logs -f</code></p> <p>which will start the containers (the <code>up -d</code>) in 'daemon mode', where they continue to run even if you log out of the server, and then show you the logging from the containers (the <code>logs -f</code>) part. To exit the log view (which will update automatically as more log messages are created), hit the <code>CTRL-C</code> key combination. If there're any errors from your individual containers, they should visible here.</p> <p>That's it, your Docker containers should be running. Once you're out of the log view, you can always check on your containers by going into the <code>/home/docker/FQDN</code> directory and executing:</p> <p><code>docker-compose ps</code></p> <p>which should show you a list of the currently running containers and their states. Here's an example:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre> Name Command State Ports <span>--------------------------------------------------------------------------------------------</span> db docker-entrypoint.sh mariadbd Up <span>3306</span><span>/</span>tcp moodletestmilllws_cron_1 <span>/</span>entrypoint.sh <span>/</span>bin<span>/</span><span>sh</span> <span>-c</span> ... Up <span>9000</span><span>/</span>tcp moodletestmilllws_moodle_1 <span>/</span>entrypoint.sh php-fpm Up <span>9000</span><span>/</span>tcp moodletestmilllws_nginx_1 <span>/</span>docker-entrypoint.sh ngin ... Up 127.0.0.1:<span>8080</span>-<span>&gt;</span><span>80</span><span>/</span>tcp</pre></div></div> <p>or, to get a bit more info, you can run</p> <p><code>docker-compose top</code></p> <p>which should show you a bit more information about the different running containers and their active processes. Here's an example:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>db UID PID PPID C STIME TTY TIME CMD <span>-----------------------------------------------------------</span> lxd <span>12056</span> <span>12035</span> <span>0</span> May23 ? 00:04:06 mariadbd   moodletestmilllws_cron_1 UID PID PPID C STIME TTY TIME CMD <span>------------------------------------------------------------------------------------------------</span> root <span>12244</span> <span>12201</span> <span>0</span> May23 ? 00:00:00 <span>/</span>bin<span>/</span><span>sh</span> <span>-c</span> cron <span>&amp;&amp;</span> <span>tail</span> <span>-f</span> <span>/</span>var<span>/</span>log<span>/</span>cron.log root <span>12651</span> <span>12244</span> <span>0</span> May23 ? 00:00:02 cron root <span>12652</span> <span>12244</span> <span>0</span> May23 ? 00:00:<span>13</span> <span>tail</span> <span>-f</span> <span>/</span>var<span>/</span>log<span>/</span>cron.log   moodletestmilllws_moodle_1 UID PID PPID C STIME TTY TIME CMD <span>-------------------------------------------------------------------------------------------------------------</span> root <span>12250</span> <span>12173</span> <span>0</span> May23 ? 00:00:<span>12</span> php-fpm: master process <span>(</span><span>/</span>usr<span>/</span>local<span>/</span>etc<span>/</span>php-fpm.conf<span>)</span> www-data <span>37755</span> <span>12250</span> <span>2</span> 01:<span>46</span> ? 00:00:00 php-fpm: pool www www-data <span>37757</span> <span>12250</span> <span>1</span> 01:<span>46</span> ? 00:00:00 php-fpm: pool www www-data <span>37758</span> <span>12250</span> <span>1</span> 01:<span>46</span> ? 00:00:00 php-fpm: pool www www-data <span>37759</span> <span>12250</span> <span>0</span> 01:<span>46</span> ? 00:00:00 php-fpm: pool www   moodletestmilllws_nginx_1 UID PID PPID C STIME TTY TIME CMD <span>--------------------------------------------------------------------------------------------------</span> root <span>13207</span> <span>13179</span> <span>0</span> May23 ? 00:00:00 nginx: master process nginx <span>-g</span> daemon off; systemd+ <span>13333</span> <span>13207</span> <span>0</span> May23 ? 00:00:01 nginx: worker process systemd+ <span>13334</span> <span>13207</span> <span>0</span> May23 ? 00:00:00 nginx: worker process </pre></div></div> <p>So, now we should have all the relevant containers for your Moodle instance running... Al that's left to do is hook up the reverse proxy configuration creating that last vital link: between your browser's request and the Moodle service (secured by Let's Encrypt)!</p> <h3><a id="user-content-create-a-secure-reverse-proxy-configuration" href="#create-a-secure-reverse-proxy-configuration" name="create-a-secure-reverse-proxy-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Create a secure reverse-proxy configuration</h3> <p>The first step to doing that is to create a reverse proxy configuration:</p> <p><code>sudo nano /etc/nginx/sites-available/${FQDN}</code></p> <p>Copy and paste the following into the file. Of course, you will want to replace every occurrence of 'FQDN' with your own domain name.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>server <span>{</span> <span># add [IP-Address:]80 in the next line if you want to limit this to a single interface</span> listen <span>80</span>; listen <span>[</span>::<span>]</span>:<span>80</span>;   server_name FQDN;   root <span>/</span>var<span>/</span>www<span>/</span>html;   <span># for let's encrypt renewals!</span> include includes<span>/</span>letsencrypt.conf;   access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span>FQDN_access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span>FQDN_error.log;   <span># redirect all HTTP traffic to HTTPS.</span> location <span>/</span> <span>{</span> <span>return</span> <span>302</span> https:<span>//</span><span>$server_name</span><span>$request_uri</span>; <span>}</span> <span>}</span>   <span># This configuration assumes that there's an nginx container talking to the moodle PHP-fpm container,</span> <span># and this is a reverse proxy for that Moodle instance.</span> fastcgi_cache_path <span>/</span>etc<span>/</span>nginx<span>/</span>cache <span>levels</span>=<span>1</span>:<span>2</span> <span>keys_zone</span>=moodle:100m <span>inactive</span>=1d <span>max_size</span>=100m; fastcgi_cache_key <span>"<span>$scheme</span><span>$request_method</span><span>$host</span><span>$request_uri</span>"</span>;   server <span>{</span> <span># add [IP-Address:]443 in the next line if you want to limit this to a single interface</span> listen <span>443</span> ssl; listen <span>[</span>::<span>]</span>:<span>443</span> ssl;   ssl_certificate <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key; <span># ssl_certificate /etc/letsencrypt/live/FQDN/fullchain.pem;</span> <span># ssl_certificate_key /etc/letsencrypt/live/FQDN/privkey.pem;</span> ssl_protocols TLSv1 TLSv1.1 TLSv1.2;   <span># A strong ssl_dhparam will help secure your server from certain kinds of attacks. </span> <span># It is commented out unless you've created it (if it's uncommented but doesn't exist</span> <span># nginx won't start). To create one, see </span> <span># https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html</span> <span># note: it can take 5-30 minutes to create one.</span> <span>#ssl_dhparam /etc/ssl/certs/dhparam.pem;</span> <span>#ssl_stapling on;</span> <span>#ssl_stapling_verify on;</span>   ssl_session_tickets off; resolver_timeout 5s; keepalive_timeout 20s;   server_name FQDN;   <span># for let's encrypt renewals!</span> include includes<span>/</span>letsencrypt.conf;   access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span>FQDN_access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span>FQDN_error.log;   client_max_body_size 100M;   location <span>/</span> <span>{</span> proxy_pass http:<span>//</span>127.0.0.1:<span>8080</span>; proxy_set_header Upgrade <span>$http_upgrade</span>; proxy_set_header Connection <span>"upgrade"</span>; proxy_set_header Host <span>$http_host</span>; <span>}</span> <span>}</span></pre></div></div> <p>Once you've saved that file, we need to make sure NGINX can see the configuration - we do that by creating a 'symbolic link' between our configuration file in the <code>sites-available</code> directory and the <code>sites-enabled</code> directory as follows:</p> <p><code>sudo ln -sf /etc/nginx/sites-available/${FQDN} /etc/nginx/sites-enabled/</code></p> <p>Before we can restart NGINX with this new configuration, we have to make sure that the 'letsencrypt.conf' cited in our configuration file exists. We do that by creating a new directory:</p> <p><code>sudo mkdir /etc/nginx/includes</code></p> <p>and creating the <code>letsencrypt.conf</code> file:</p> <p><code>sudo nano /etc/nginx/includes/letsencrypt.conf</code></p> <p>into which we copy and paste the following:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>#############################################################################</span> <span># Configuration file for Let's Encrypt ACME Challenge location</span> <span># This file is already included in listen_xxx.conf files.</span> <span># Do NOT include it separately!</span> <span>#############################################################################</span> <span>#</span> <span># This config enables to access /.well-known/acme-challenge/xxxxxxxxxxx</span> <span># on all our sites (HTTP), including all subdomains.</span> <span># This is required by ACME Challenge (webroot authentication).</span> <span># You can check that this location is working by placing ping.txt here:</span> <span># /var/www/letsencrypt/.well-known/acme-challenge/ping.txt</span> <span># And pointing your browser to:</span> <span># http://xxx.domain.tld/.well-known/acme-challenge/ping.txt</span> <span>#</span> <span># Sources:</span> <span># https://community.letsencrypt.org/t/howto-easy-cert-generation-and-renewal-with-nginx/3491</span> <span>#</span> <span># Rule for legitimate ACME Challenge requests</span> location ^~ <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> default_type <span>"text/plain"</span>; <span># this can be any directory, but this name keeps it clear</span> root <span>/</span>var<span>/</span>www<span>/</span>letsencrypt; <span>}</span> <span># Hide /acme-challenge subdirectory and return 404 on all requests.</span> <span># It is somewhat more secure than letting Nginx return 403.</span> <span># Ending slash is important!</span> location = <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> <span>return</span> <span>404</span>; <span>}</span></pre></div></div> <p>In turn, we also have to make sure that the directory stipulated in the letsencrypt.conf file exists - we'll attempt to create the directory, which won't hurt anything if it already exists:</p> <p><code>sudo mkdir /var/www/letsencrypt</code></p> <p>With that done, we can test NGINX's configuration:</p> <p><code>sudo nginx -t</code></p> <p>If it's free of errors (there might be a warning related to the <code>snakeoil</code> ''temporary'' certificates we've specified. That's ok - we just specified them so that we can get NGINX restarted in a way that let's us request an SSL certificate from Let's Encrypt!), we can get NGINX to reload its configuration, so that it'll know how to respond to requests for the FQDN domain!</p> <p><code>sudo service nginx reload</code></p> <p>If that returns without errors, we're in business! It's time to make our Let's Encrypt certificate. Creating one is easy:</p> <p><code>sudo letsencrypt certonly --webroot -w /var/www/letsencrypt -d ${FQDN}</code></p> <p>In our case, it's:</p> <p><code>sudo letsencrypt certonly --webroot -w /var/www/letsencrypt -d moodletest.milll.ws</code></p> <p>Running that should trigger output like this:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Saving debug log to <span>/</span>var<span>/</span>log<span>/</span>letsencrypt<span>/</span>letsencrypt.log Enter email address <span>(</span>used <span>for</span> urgent renewal and security notices<span>)</span> <span>(</span>Enter <span>'c'</span> to cancel<span>)</span>: webmaster<span>@</span>milll.ws   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Please <span>read</span> the Terms of Service at https:<span>//</span>letsencrypt.org<span>/</span>documents<span>/</span>LE-SA-v1.2-November-<span>15</span>-<span>2017</span>.pdf. You must agree <span>in</span> order to register with the ACME server. Do you agree? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <span>(</span>Y<span>)</span>es<span>/</span><span>(</span>N<span>)</span>o: y   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - Would you be willing, once your first certificate is successfully issued, to share your email address with the Electronic Frontier Foundation, a founding partner of the Let<span>'s Encrypt project and the non-profit organization that develops Certbot? We'</span>d like to send you email about our work encrypting the web, EFF news, campaigns, and ways to support digital freedom. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - <span>(</span>Y<span>)</span>es<span>/</span><span>(</span>N<span>)</span>o: y Account registered. Requesting a certificate <span>for</span> moodletest.milll.ws   Successfully received certificate. Certificate is saved at: <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span>moodletest.milll.ws<span>/</span>fullchain.pem Key is saved at: <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span>moodletest.milll.ws<span>/</span>privkey.pem This certificate expires on <span>2022</span>-08-<span>21</span>. These files will be updated when the certificate renews. Certbot has <span>set</span> up a scheduled task to automatically renew this certificate <span>in</span> the background. We were unable to subscribe you the EFF mailing list because your e-mail address appears to be invalid. You can try again later by visiting https:<span>//</span>act.eff.org.   - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - If you like Certbot, please consider supporting our work by: <span>*</span> Donating to ISRG <span>/</span> Let<span>'s Encrypt: https://letsencrypt.org/donate * Donating to EFF: https://eff.org/donate-le - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -</span></pre></div></div> <p>... which means you've successfully received a new certificate! Now you can update your NGINX reverse proxy configuration:</p> <p><code>sudo nano /etc/nginx/sites-available/${FQDN}</code></p> <p>to change it as follows (again, make sure you've replaced FQDN with your actual domain name!)</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;</span> <span># ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;</span> ssl_certificate <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span>FQDN<span>/</span>fullchain.pem; ssl_certificate_key <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span>FQDN<span>/</span>privkey.pem;</pre></div></div> <p>Once you've saved the file you can test your NGINX configuration:</p> <p><code>sudo nginx -t</code></p> <p>and if it doesn't return any errors (nor should it return warnings), you can reload NGINX's config to make your new secure configuration live!</p> <p><code>sudo service nginx reload</code></p> <h3><a id="user-content-installing-moodle" href="#installing-moodle" name="installing-moodle" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Installing Moodle</h3> <p>Now that we've filled in all the parts of the tool-chain, your server is ready to receive requests for your Moodle site! You should be able to go to your web browser on your local computer and enter the URL, replacing FQDN with your domain name of course:</p> <p><code>https://FQDN</code></p> <p>(and entering <code>http://FQDN</code> should automatically redirect to <code>https://FQDN</code> - feel free to test it!)</p> <p>And if all is well, your Docker Compose stack running Moodle should respond by serving you up a page that looks like the attached "Install page" screenshot.</p> <p>You'll probably notice it looks a bit rough, as, for the moment at least, the Cascading Style Sheets (aka CSS, the code that determines how web content looks) won't be loading correctly. We'll address that shortly.</p> <p>We need to get through the installation of Moodle first, during which time, the Moodle system creates your <code>config.php</code> file in the <code>/home/data/${FQDN}/src/</code> directory. Once that's created, you'll be able to edit the file and make the tweaks to fix this problem.</p> <p>But first you'll need to proceed with the installation process - you'll be asked to select your language. And then you can click 'next' to continue. You'll be asked to specify your database (pick 'MariaDB' in this case) and then you'll be asked for the database details, as showing in the attached screenshot.</p> <p>Your host will be <code>db</code>, your database name and user will both be <code>moodle</code> (case sensitive!) and you'll have to enter the database password you put into your <code>docker-compose.yml</code> file.</p> <p>Then you should get a page showing you that your setup meets Moodle's requirement for installation (see attached screenshot), and finally you'll be asked to proceed with the install. Do so!</p> <p>The installation will create the relevant database tables in the <code>moodle</code> database via the MariaDB container, and Moodle will create the aforementioned <code>config.php</code> file. The next thing to do is edit that file:</p> <p><code>sudo nano /home/data/${FQDN}/src/config.php</code></p> <p>where you'll tweak the configuration file so that the line</p> <p><code>$CFG-&gt;wwwroot = 'http://moodletest.milll.ws';</code></p> <p>looks like (note the shift from ''http'' to ''https''):</p> <p><code>$CFG-&gt;wwwroot = 'https://moodletest.milll.ws';</code></p> <p>And add this line above the final comment (the lines starting with '//')</p> <p><code>$CFG-&gt;sslproxy=true;</code></p> <p>which informs the Moodle system that it's sitting behind an SSL reverse proxy so it can adjust its behaviour accordingly.</p> <p>So it should look something like this:</p> <div class="codeblock geshifilter"><code><span><br /><span>&lt;?php  </span><span>// Moodle configuration file<br /><br /></span><span>unset(</span><span>$CFG</span><span>);<br />global </span><span>$CFG</span><span>;<br /></span><span>$CFG </span><span>= new </span><span>stdClass</span><span>();<br /><br /></span><span>$CFG</span><span>-&gt;</span><span>dbtype    </span><span>= </span><span>'mariadb'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>dblibrary </span><span>= </span><span>'native'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>dbhost    </span><span>= </span><span>'db'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>dbname    </span><span>= </span><span>'moodle'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>dbuser    </span><span>= </span><span>'moodle'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>dbpass    </span><span>= </span><span>'[your super-secret db password]'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>prefix    </span><span>= </span><span>'mdl_'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>dboptions </span><span>= array (<br />  </span><span>'dbpersist' </span><span>=&gt; </span><span>0</span><span>,<br />  </span><span>'dbport' </span><span>=&gt; </span><span>3306</span><span>,<br />  </span><span>'dbsocket' </span><span>=&gt; </span><span>''</span><span>,<br />  </span><span>'dbcollation' </span><span>=&gt; </span><span>'utf8mb4_general_ci'</span><span>,<br />);<br /><br /></span><span>$CFG</span><span>-&gt;</span><span>wwwroot   </span><span>= </span><span>'https://moodletest.milll.ws'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>dataroot  </span><span>= </span><span>'/var/www/moodledata'</span><span>;<br /></span><span>$CFG</span><span>-&gt;</span><span>admin     </span><span>= </span><span>'admin'</span><span>;<br /><br /></span><span>$CFG</span><span>-&gt;</span><span>directorypermissions </span><span>= </span><span>0777</span><span>;<br /><br /></span><span>$CFG</span><span>-&gt;</span><span>sslproxy</span><span>=</span><span>true</span><span>;<br /><br />require_once(</span><span>__DIR__ </span><span>. </span><span>'/lib/setup.php'</span><span>);<br /><br /></span><span>// There is no php closing tag in this file,<br />// it is intentional because it prevents trailing whitespace problems!<br /></span><span>?&gt;</span></span></code></div> <p>Save your updated config.php file, and return to the installation process - after the next button press, when the Moodle installer page loads, you should suddenly see the interface look all beautiful and tidy as the CSS suddenly loads.</p> <p>You'll be asked set some properties for your site, like giving it a name, and finally, you'll have to provide the details of an 'admin' user - I recommend creating a default admin user, 'admin', with a <a href="/node/43">very strong password</a>. You'll log in with that admin user (and provide the details to your organisation so that there's a bit of succession planning, if you're not available at some point) just this once, and immediately create a new user for ''yourself'' with your preferred details, which you'll then elevate to 'administrator' privileges, and then do all your administration of the site via your own user.</p> <p>Once you've successfully completed defining an admin user, you'll end up at the admin 'dashboard', where you can create the personal administrator user for yourself. And then you can log out as 'admin' and log in as yourself... at which point, you're done! Moodle is installed. Hazzah!</p> <h2><a id="user-content-backing-up-your-data" href="#backing-up-your-data" name="backing-up-your-data" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Backing up your data</h2> <p>The way we run Docker Compose, all the files we want to preserve are stored on the VPS's own filesystem. The Docker containers only store 'volatile' data. That means we can literally remove the Docker containers at any time, and it won't (under normal circumstances) affect our precious data. The only time it ''might'' is when data, e.g. in the database, is held in system memory rather than being written to disk.</p> <p>That means that making backups of our data can be achieve simply by copying files on the hard disk - and if we're really interested in preserving that data, we'll send it somewhere else - on a different computer, in a different geographic region, and ideally managed by a different company - to achieve suitable diversity.</p> <h3><a id="user-content-mariadb-database-dumps" href="#mariadb-database-dumps" name="mariadb-database-dumps" class="heading-permalink" aria-hidden="true" title="Permalink"></a>MariaDB database dumps</h3> <p>The only element of our stack which isn't amenable to this file copying is the data managed by MariaDB. It is ''not'' generally safe to copy the files of a running database and to expect that restarting the database with those copied files will result in an uncorrupted data set. To safely backup a database, you either have to shut it down (which is disruptive and inelegant) or you have to get it do a database 'dump' while it's running (which it can usually do very quickly, without noticable impact on the performance of the database.</p> <p>A database 'dump' is a frozen snapshot of the data in a generic form, usually SQL. It allows us to restore the database tables, the data they contain, and the relationships (indices, foreign keys, etc.) between the tables. It usually even lets us take data from an older version of MariaDB and import it into a newer version (which is very handy!).</p> <p>So we need a method for periodically - and automatically - creating database dumps of our MariaDB Moodle database. Luckily, it's not too hard. I've created this script to do it.</p> <h2><a id="user-content-upgrading-moodle-with-git" href="#upgrading-moodle-with-git" name="upgrading-moodle-with-git" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Upgrading Moodle with Git</h2></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=52&amp;2=field_blog_comments&amp;3=comment" token="wfi0uCDurjkXOaaTefSDHhAt_udfRED6cv3itRTTiKg"></drupal-render-placeholder> </div> </section> Sun, 22 May 2022 23:13:24 +0000 dave 52 at http://tech.oeru.org Installing BigBlueButton on an OERu Docker Server http://tech.oeru.org/installing-bigbluebutton-oeru-docker-server <span class="field field--name-title field--type-string field--label-hidden">Installing BigBlueButton on an OERu Docker Server</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--bigbluebutton"> <span class="field__item-wrapper"><a href="/taxonomy/term/84" hreflang="en">BigBlueButton</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--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--postgresql"> <span class="field__item-wrapper"><a href="/taxonomy/term/20" hreflang="en">postgresql</a></span> </div> <div class="field__item field__item--postfix"> <span class="field__item-wrapper"><a href="/taxonomy/term/66" hreflang="en">postfix</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--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" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Wed 10/11/2021 - 15:42</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-11/Screenshot%202021-11-26%20at%2015-03-34%20The%20OERu%20BigBlueButton%20-%20Home%20Room-withModeratorMenu.png?itok=wkyM4MSu" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;BigBlueButton conversation window in a browser, with the moderator tools open, showing link to breakout rooms and other moderation functions.&quot;}" role="button" title="BigBlueButton conversation window in a browser, with the moderator tools open, showing link to breakout rooms and other moderation functions." data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;BigBlueButton conversation window in a browser, with the moderator tools open, showing link to breakout rooms and other moderation functions.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-26%20at%2015-03-34%20The%20OERu%20BigBlueButton%20-%20Home%20Room-withModeratorMenu.png?itok=tsVPmMgZ" width="220" height="143" alt="BigBlueButton conversation window in a browser, with the moderator tools open, showing link to breakout rooms and other moderation functions." loading="lazy" 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-11/Screenshot%202021-11-26%20at%2012-02-53%20Create%20Droplets%20-%20DigitalOcean.png?itok=RFCxKkbH" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Digital Ocean &#039;Droplet&#039; creation screen showing options selected for a BBB server&quot;}" role="button" title="Digital Ocean &#039;Droplet&#039; creation screen showing options selected for a BBB server" data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Digital Ocean &#039;Droplet&#039; creation screen showing options selected for a BBB server&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-26%20at%2012-02-53%20Create%20Droplets%20-%20DigitalOcean.png?itok=qpszFHwz" width="106" height="220" alt="Digital Ocean &#039;Droplet&#039; creation screen showing options selected for a BBB server" loading="lazy" 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-11/Screenshot%202021-11-22%20at%2012-42-19%20bbbtest%20milll%20ws%20-%20DigitalOcean.png?itok=9s5Cj-8F" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Performance graphs for a Digital Ocean &#039;Droplet&#039; configured to run BBB.&quot;}" role="button" title="Performance graphs for a Digital Ocean &#039;Droplet&#039; configured to run BBB." data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Performance graphs for a Digital Ocean &#039;Droplet&#039; configured to run BBB.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-22%20at%2012-42-19%20bbbtest%20milll%20ws%20-%20DigitalOcean.png?itok=Sy3JvvuB" width="220" height="166" alt="Performance graphs for a Digital Ocean &#039;Droplet&#039; configured to run BBB." loading="lazy" 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/2021-11/Screenshot%202021-11-22%20at%2012-42-52%20milll%20ws%20-%20Metaname.png?itok=eILrt0_H" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Metaname DNS configuration for the milll.ws domain.&quot;}" role="button" title="The Metaname DNS configuration for the milll.ws domain." data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Metaname DNS configuration for the milll.ws domain.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-22%20at%2012-42-52%20milll%20ws%20-%20Metaname.png?itok=8iNSxMhC" width="220" height="166" alt="The Metaname DNS configuration for the milll.ws domain." loading="lazy" 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/2021-11/Screenshot%202021-11-23%20at%2014-20-22%20BigBlueButton.png?itok=vRbjT27h" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Greenlight front-end (providing user management and &#039;room&#039; configuration) for BigBlueButton.&quot;}" role="button" title="The Greenlight front-end (providing user management and &#039;room&#039; configuration) for BigBlueButton." data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Greenlight front-end (providing user management and &#039;room&#039; configuration) for BigBlueButton.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-23%20at%2014-20-22%20BigBlueButton.png?itok=4W4E__KP" width="220" height="144" alt="The Greenlight front-end (providing user management and &#039;room&#039; configuration) for BigBlueButton." loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-6"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2021-11/Screenshot%202021-11-30%20at%2023-04-07%20The%20OERu%20BigBlueButton%20-%20Home%20Room.png?itok=ofBw87DD" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Joining a BBB session, you get the option of using a microphone to participate or being a &#039;listen-only&#039; participant.&quot;}" role="button" title="Joining a BBB session, you get the option of using a microphone to participate or being a &#039;listen-only&#039; participant." data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Joining a BBB session, you get the option of using a microphone to participate or being a &#039;listen-only&#039; participant.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-30%20at%2023-04-07%20The%20OERu%20BigBlueButton%20-%20Home%20Room.png?itok=RsOTQSIE" width="220" height="133" alt="Joining a BBB session, you get the option of using a microphone to participate or being a &#039;listen-only&#039; participant." loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-7"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2021-11/Screenshot%202021-11-30%20at%2023-04-22%20The%20OERu%20BigBlueButton%20-%20Home%20Room%20-%20echo%20test.png?itok=H8WlXV4f" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;If you select &quot;microphone&quot;, you will next be asked to speak into it and do an &#039;echo test&#039; to calibrate BBB&#039;s echo cancellation algorithms.&quot;}" role="button" title="If you select &quot;microphone&quot;, you will next be asked to speak into it and do an &#039;echo test&#039; to calibrate BBB&#039;s echo cancellation algorithms." data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;If you select &quot;microphone&quot;, you will next be asked to speak into it and do an &#039;echo test&#039; to calibrate BBB&#039;s echo cancellation algorithms.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-30%20at%2023-04-22%20The%20OERu%20BigBlueButton%20-%20Home%20Room%20-%20echo%20test.png?itok=KvueJqpL" width="220" height="133" alt="If you select &quot;microphone&quot;, you will next be asked to speak into it and do an &#039;echo test&#039; to calibrate BBB&#039;s echo cancellation algorithms." loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-8"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2021-11/Screenshot%202021-11-30%20at%2023-04-49%20The%20OERu%20BigBlueButton%20-%20Home%20Room-webcam.png?itok=pEnZWxzO" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;And you can share your webcam feed, too. &quot;}" role="button" title="And you can share your webcam feed, too. " data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;And you can share your webcam feed, too. &quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-30%20at%2023-04-49%20The%20OERu%20BigBlueButton%20-%20Home%20Room-webcam.png?itok=tXwJ7Cpf" width="220" height="133" alt="And you can share your webcam feed, too. " loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-9"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2021-11/Screenshot%202021-11-26%20at%2015-00-54%20The%20OERu%20BigBlueButton%20-%20Home%20Room.png?itok=55K_wxG1" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;BigBlueButton in effect with 3 participants (all the author from different devices). Note the public chat.&quot;}" role="button" title="BigBlueButton in effect with 3 participants (all the author from different devices). Note the public chat." data-colorbox-gallery="gallery-field_image-Ty9W6WjDqVs" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;BigBlueButton in effect with 3 participants (all the author from different devices). Note the public chat.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-11/Screenshot%202021-11-26%20at%2015-00-54%20The%20OERu%20BigBlueButton%20-%20Home%20Room.png?itok=j1O8G0e-" width="220" height="143" alt="BigBlueButton in effect with 3 participants (all the author from different devices). Note the public chat." loading="lazy" 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>This tutorial was developed for the <a href="https://samoaksi.ws/">Samoan Knowledge Society Initiative</a>, in which the OER Foundation is very pleased to be involved. (Update 2022-02-10: we have also created a <a href="https://vimeo.com/663520867">video tutorial</a> based on this written tutorial)</p> <p><a href="https://bigbluebutton.org">BigBlueButton</a> (BBB) is a full-featured video conferencing system designed for large scale remote learning at the university level. In some ways, BBB goes beyond feature parity with widely used proprietary video conferencing systems like Zoom or Microsoft Teams, Webex, or Google Hangouts. It's main differences are that BBB</p> <ul><li>was designed from the gound up for educational use, and</li> <li>is Free and Open Source Software (FOSS) and anyone can install it (as we describe in this tutorial) at no cost other than whatever cost you pay for hosting the application and a bit of your time.</li> </ul><p>You can read all about BigBlueButton and how it works using the <a href="https://docs.bigbluebutton.org">excellent documentation</a> provided by its development community.</p> <p>There are no per-user license fees (or <em>any</em> fees), and your users don't need to install special software, vastly simplifying its adoption: BBB works brilliantly on any desktop computer, laptop, or mobile device (tablet or phone) with a modern web browser that implements the <a href="https://webrtc.org">WebRTC</a> open web standard (which is all modern browsers).</p> <p>The <a href="https://github.com/bigbluebutton/">source code for BBB's internal components, and its many ancillary tools</a> is available for anyone to learn from or improve - that's the fundamental benefit of FOSS. Since the outbreak of COVID19, the team at a small Ottowa, Canada-based company, Blindside Networks, has been working tirelessly with the BBB community to improve the BBB code, which is being used all over the world by millions of people every day.</p> <p>This tutorial walks you through provisioning and building your own BigBlueButton instance on a server running <a href="https://ubuntu.com/download/server">Ubuntu GNU/Linux</a> - version 20.04 is the Long Term Support (LTS) version as of this writing - using <a href="https://www.docker.com/docker-community">Docker</a> containers for the various software services and components, with the containers managed by <a href="https://docs.docker.com/compose/">Docker Compose</a>.</p> <p><em>Table of contents</em></p> <ul class="table-of-contents"><li> <p><a href="#introduction">Introduction</a></p> <ul><li> <p><a href="#references">References</a></p> </li> <li> <p><a href="#copying-and-pasting">Copying and pasting</a></p> </li> </ul></li> <li> <p><a href="#information-you-need-to-know">Information you need to know</a></p> </li> <li> <p><a href="#preferences">Preferences</a></p> </li> <li> <p><a href="#set-up-your-virtual-server">Set up your virtual server</a></p> </li> <li> <p><a href="#configure-your-domain-to-point-at-your-server">Configure your Domain to point at your Server</a></p> <ul><li> <p><a href="#best-practice-access-your-server-as-a-non-root-user">Best Practice: access your server as a non-root user</a></p> </li> </ul></li> <li> <p><a href="#a-few-post-install-updates">A few post-install updates</a></p> <ul><li> <p><a href="#update-the-servers-hosts-file">Update the server's hosts file</a></p> </li> </ul></li> <li> <p><a href="#firewall-configuration">Firewall Configuration</a></p> </li> <li> <p><a href="#install-postfix-so-the-server-can-send-email">Install Postfix so the server can send email</a></p> <ul><li> <p><a href="#email-test">Email test</a></p> </li> </ul></li> <li> <p><a href="#install-nginx-webserver-and-lets-encrypt-tools">Install Nginx webserver and Let's Encrypt tools</a></p> </li> <li> <p><a href="#set-up-coturn-certificates">Set up COTURN certificates</a></p> </li> <li> <p><a href="#set-up-nginx-reverse-proxy-configuration">Set up Nginx Reverse Proxy Configuration</a></p> <ul><li> <p><a href="#extra-security-with-dhparampem">Extra security with dhparam.pem</a></p> </li> </ul></li> <li> <p><a href="#install-docker">Install Docker</a></p> </li> <li> <p><a href="#install-docker-compose">Install Docker-compose</a></p> </li> <li> <p><a href="#git-clone-bbb-docker-repository">Git-Clone BBB Docker repository</a></p> </li> <li> <p><a href="#configure-your-bbb">Configure your BBB</a></p> <ul><li> <p><a href="#fix-minor-configuration-error-that-blocks-jodconverter">Fix minor configuration error that blocks JODConverter</a></p> </li> </ul></li> <li> <p><a href="#build-your-bbb">Build your BBB</a></p> </li> <li> <p><a href="#visit-your-new-bbb">Visit your new BBB</a></p> </li> <li> <p><a href="#create-an-admin-user">Create an admin user:</a></p> </li> <li> <p><a href="#run-your-first-conference">Run your first conference</a></p> </li> <li> <p><a href="#next-steps">Next Steps</a></p> </li> </ul><h2><a id="user-content-introduction" href="#introduction" name="introduction" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Introduction</h2> <h3><a id="user-content-references" href="#references" name="references" class="heading-permalink" aria-hidden="true" title="Permalink"></a>References</h3> <p>For this tutorial, we are using a <a href="https://digitalocean.com">Digital Ocean</a> 'Droplet' as our cloud server. You can use an almost identical process to provision a server from any one of a thousand other commodity GNU/Linux cloud hosting providers.</p> <p>To demonstrate the process of defining a sub domain and its IPv4 and IPv6 addresses (via A and AAAA records respectively) I'm using my preferred local domain registrar <a href="https://metaname.net">Metaname</a> (based here in Christchurch, New Zealand, as am I. I know the developer/owner of the service and I trust him). You can use any domain registrar that gives you the ability to configure your 'DNS zone file'. Some of might have your own DNS server or institutional name servers, in which case you might have to request these changes be made on your behalf.</p> <h3><a id="user-content-copying-and-pasting" href="#copying-and-pasting" name="copying-and-pasting" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Copying and pasting</h3> <p>We will be providing a bunch of command lines that you can copy and paste into a terminal on the computer you're using to access your virtual server. Links like</p> <p><code>this one</code></p> <p>are intended for you to copy and paste into the command line. On Linux, please note that in some cases if "CTRL-V" doesn't work to paste, try using "CTRL+SHIFT+V" (that's because in some terminals, "CTRL+V" had a pre-existing purpose as terminals have been around since long before desktops and 'copy-paste" were invented and the arbitrary CTRL-C, CTRL-X, CTRL-V key combos were chosen by Microsoft without any consideration for prior art). Similarly, if you want to copy <em>from</em> a terminal, you might need to use "CTRL+SHIFT+C". Try it.</p> <p>When we ask you edit a file (using the editor you choose to assign to the EDIT shell variable below), we expect you to complete the recommended changes, and then <em>save the file</em> and exit back to the command line.</p> <p>We'll aim to show you what to change using a 'code' box:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Like this one <span>which</span> is suitable <span>for</span> multi-line content <span>(</span>like what you <span>find</span> <span>in</span> a <span>file</span><span>)</span></pre></div></div> <h2><a id="user-content-information-you-need-to-know" href="#information-you-need-to-know" name="information-you-need-to-know" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Information you need to know</h2> <p>Info you need to have handy to complete this tutorial:</p> <ul><li>You need to know the IPv4 address of your server. Token: [IPv4] - it'll be in the form of nnn.nnn.nnn.nnn where nnn = 0-254, example value 143.110.228.88</li> <li>If your server has one, you need to know its IPv6 address. Token: [IPv6] - it'll be in the form of hhhh:hhhh:hhhh:hhhh:hhhh:hhhh:hhhh where h = 0-9a-f, i.e. a hex value, example 2604:a880:4:1d0::402:b000</li> <li>You need to have a domain name (or a sub domain name) to point at your server's IPv4 and IPv6. Token: [Domain], example domain bbbtest.milll.ws (in this case, bbbtest is the subdomain of the milll.ws domain)</li> <li>The email address your server should send status or admin-related email to. Token [Admin email], example webmaster@[Domain]</li> <li>You'll need a set of authenticating SMTP account settings including - these can be used both on the host and in the BBB installation: <ul><li>[SMTP server] - the domain name or IP address of an existing SMTP server, e.g. about.oerfoundation.org (our one), or smtp.googlemail.com,</li> <li>[SMTP port] - the port number for the service you're using. It'll usually be one of 587 or 465. Older, unauthenticated SMTP servers used to use port 25, but that's now blocked by most ISPs due to its abuse by spammers.</li> <li>[SMTP security] - usually this'll be something like 'SSL' (usually associated with port 465) or 'StartTLS' (usually associated with port 587).</li> <li>[SMTP username] - the username - often an email address - for the authenticating SMTP service, and lastly,</li> <li>[SMTP password] - the password accompanying the username.</li> </ul></li> <li>You'll want an email address that you can check to send test emails to: token [Test email], e.g. <a href="mailto:you@youremailprovider.tld">you@youremailprovider.tld</a>, and finally</li> <li>You'll want a 'from' email address that users of your system will see when they get emails from you. Token: [Outgoing email], e.g. <a href="mailto:notifications@milll.ws">notifications@milll.ws</a>.</li> </ul><h2><a id="user-content-preferences" href="#preferences" name="preferences" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Preferences</h2> <p>You'll need to copy and paste the following lines into any terminal you're using to complete this tutorial, so that that session is aware of your preferred values:</p> <p><code>EDIT=$(which nano)</code></p> <p><code>SITE=bbbtest.milll.ws</code></p> <p>We're going to reference these periodically in the following process, so it's important you set these correctly. To verify them, you can run this in any terminal at any time to verify that the values are still defined and current:</p> <p><code>echo "Our variables = $EDIT and $SITE"</code></p> <h2><a id="user-content-set-up-your-virtual-server" href="#set-up-your-virtual-server" name="set-up-your-virtual-server" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up your virtual server</h2> <p>You'll need to log into your cloud service provider's dashboard - in our case, we've used DigitalOcean. That means pointing our browser to <a href="https://digitalocean.com">https://digitalocean.com</a> and either using "Log In" (if we already have an account) or "Sign Up" (if we don't).</p> <p>Once you're logged in, you'll want to go to 'Create' and select 'Droplets' (Digital Ocean calls each of their virtual servers a "Droplet").</p> <p>We've included a screenshot of the Create Droplet page, with the relevant options selected. They are as follows:</p> <ul><li>Choose an image: we choose 'Ubuntu 20.04 LTS'</li> <li>Choose a plan: we choose 'Basic' <ul><li>for CPU options: we choose 'Premium AMD with NVMe SSD'</li> <li>for the size, we choose either the '$24/mo' (choose the '$48/mo' option if you want to be able to support large groups, like 100+ simultaneous participants).</li> </ul></li> <li>Choose a datacenter region: we choose 'San Franciso, zone 3' (which we've determined to be in the 'centre of the internet', i.e. the most central point to give the best website performance to a global audience. You might want to try a different place depending on your location in the globe and your audience).</li> <li>VPC Network - we just leave the default.</li> <li>Select additional options: we check 'IPv6' and 'Monitoring' but <em>not</em> User data.</li> <li>Authentication: if we have added one or more SSH keys to our DigitalOcean profile select 'SSH keys'. If not, you can select 'Password'. It's far more efficient and secure to use SSH whereever possible.</li> <li>We're ony creating 1 Droplet and we Choose a hostname of [Domain]</li> <li>We don't need to add any tags</li> <li>We <em>won't</em> Add backups.</li> </ul><p>Then we can <code>Create Droplet</code>...</p> <p>When the Droplet is finished, we can get the [IPv4] and [IPv6] addresses.</p> <p>We can also either log in via the command line from a local GNU/Linux or UNIX (e.g. MacOS) terminal or via a graphical SSH client (on Microsoft Windows, most people use Putty) via <code>ssh root@[IPv4]</code> or <code>ssh root@[IPv6]</code> (replacing those tokens with their actual IPv4 or IPv6 addresses). If we've set a key, we should get logged in without the need to enter any passwords. If not, we have to enter a password provided by DigitalOcean.</p> <h2><a id="user-content-configure-your-domain-to-point-at-your-server" href="#configure-your-domain-to-point-at-your-server" name="configure-your-domain-to-point-at-your-server" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configure your Domain to point at your Server</h2> <p>Once you have selected and registered your domain with a domain registrar - we are using Metaname - you can set up (usually through a web interface provided by the registrar)</p> <ul><li>an "A Record" which associates your website's name (your [Domain], e.g. milll.ws, or a subdomain of that domain, e.g. bbb.milll.ws, where 'bbb' is the subdomain part) to the IPv4 address of your server. You should just be able to enter your server's IPv4 address, the domain name (or sub-domain) you want to use for your server.</li> <li>if your registrar offers it it's also important to define an IPv6 record, which is called an "AAAA Record"... you put in the same domain name or subdomain and then your IPv6 address instead of your IPv4 one.</li> </ul><p>I've attached an screenshot of the Metaname interface for configuring these DNS zone records.</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> <p>Once your domain A and AAAA records are configured, you should be able to log into your server via <code>ssh root@[Domain]</code>, or, for example, <code>ssh root@bbb.milll.ws</code>. Although it should be instant, depending on your registrar, it might take as long as a few hours (even 24) for your domain name assignments to propogate through the DNS network and be available on your computer.</p> <h3><a id="user-content-best-practice-access-your-server-as-a-non-root-user" href="#best-practice-access-your-server-as-a-non-root-user" name="best-practice-access-your-server-as-a-non-root-user" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Best Practice: access your server as a non-root user</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 <a href="https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html">Putty</a>...</p> <p>But this will log you into your server as the 'root' user.</p> <p>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></p> <p>You will be required to set a password for the user here. Then you can add the new user to several relevant groups that confer the required administrative capabilities.</p> <p><code>adduser $U ssh</code><br /><code>adduser $U admin</code><br /><code>adduser $U sudo</code></p> <p>If you want to change the password for user [username] you can run:</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><br /><code>ssh-keygen -t rsa -b 2048</code><br /><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> <p>The rest of the tutorial can be run as your 'sudo-capable' non-root user.</p> <h2><a id="user-content-a-few-post-install-updates" href="#a-few-post-install-updates" name="a-few-post-install-updates" class="heading-permalink" aria-hidden="true" title="Permalink"></a>A few post-install updates</h2> <h3><a id="user-content-update-the-servers-hosts-file" href="#update-the-servers-hosts-file" name="update-the-servers-hosts-file" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Update the server's hosts file</h3> <p>This is required due to some quirks of the auto-detection of your domain name used by BigBlueButton - you'll need it later.</p> <p><code>sudo $EDIT /etc/hosts</code></p> <p>and add (or make sure it already has) the following:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>127.0.1.1 <span>[</span>First part of domain<span>]</span> 127.0.0.1 localhost <span>[</span>IPv4<span>]</span> <span>[</span>Domain<span>]</span> <span>[</span>IPv6<span>]</span> <span>[</span>Domain<span>]</span></pre></div></div> <p>For example - my test system has these details:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>127.0.1.1 bbbtest 127.0.0.1 localhost 143.110.228.88 bbbtest.milll.ws <span>2604</span>:a880:<span>4</span>:1d0::<span>402</span>:b000 bbbtest.milll.ws</pre></div></div> <p>Do an initial post-install update to the latest software versions:</p> <p><code>sudo apt-get update &amp;&amp; sudo apt-get dist-upgrade</code></p> <p>and add a couple useful apps that, for example, track changes to our system configuration for future reference (etckeeper) and allow us to do network troubleshooting (net-tools):</p> <p><code>sudo apt-get install -y etckeeper net-tools</code></p> <p>We also want to enable console logins in the event we have trouble using SSH:</p> <p><code>wget -qO- https://repos-droplet.digitalocean.com/install.sh | sudo bash</code></p> <h2><a id="user-content-firewall-configuration" href="#firewall-configuration" name="firewall-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Firewall Configuration</h2> <p>We need to configure firewall for admin access via SSH and access to internet for Docker containers</p> <p><code>sudo ufw allow OpenSSH</code></p> <p><code>sudo ufw allow in on docker0</code></p> <p><code>sudo ufw allow from 172.0.0.0/8 to any</code></p> <p>Then we need to adjust the default firewall policy:</p> <p><code>sudo $EDIT /etc/default/ufw</code></p> <p>You'll find this:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Set the default forward policy to ACCEPT, DROP or REJECT. Please note that</span> <span># if you change this you will most likely want to adjust your rules</span> <span>DEFAULT_FORWARD_POLICY</span>=<span>"DROP"</span></pre></div></div> <p>which you need to change to</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Set the default forward policy to ACCEPT, DROP or REJECT. Please note that</span> <span># if you change this you will most likely want to adjust your rules</span> <span>DEFAULT_FORWARD_POLICY</span>=<span>"ACCEPT"</span></pre></div></div> <p>Next you need to edit this file</p> <p><code>sudo $EDIT /etc/ufw/sysctl.conf</code></p> <p>And change (near the top of the file):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Uncomment this to allow this host to route packets between interfaces</span> <span>#net/ipv4/ip_forward=1</span> <span>#net/ipv6/conf/default/forwarding=1</span> <span>#net/ipv6/conf/all/forwarding=1</span></pre></div></div> <p>to</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Uncomment this to allow this host to route packets between interfaces</span> net<span>/</span>ipv4<span>/</span><span>ip_forward</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>default<span>/</span><span>forwarding</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>all<span>/</span><span>forwarding</span>=<span>1</span></pre></div></div> <p>(removing the "#" uncomments those lines)</p> <p>Next, you have to restart the server's networking stack to apply the changes you've made:</p> <p><code>sudo systemctl restart systemd-networkd</code></p> <p>Luckily, this is generally instantaneous, so your connection to your server shouldn't be interrupted.</p> <p>Then you either start or (if it's already running for some reason) restart the firewall</p> <p><code>sudo ufw enable</code></p> <p>or</p> <p><code>sudo service ufw restart</code></p> <p>(running both won't do any harm)</p> <p>And we also update the firewall configuration to tell it to automatically start at boot time.</p> <p><code>sudo $EDIT /etc/ufw/ufw.conf</code></p> <p>Change <code>ENABLED=no</code> to <code>ENABLED=yes</code>.</p> <p>Next we need to set up some specialised rules for the "COTURN" functionality of the BigBlueButton system. <a href="https://coturn.github.io/">COTURN</a> is a FOSS application that provides both <a href="https://www.youtube.com/watch?v=4dLJmZOcWFc">TURN and STUN (video)</a> (<a href="https://blog.ivrpowers.com/post/technologies/what-is-stun-turn-server/">alternative non-video reference</a>) functionality which are core to the WebRTC protocol on which modern video conferencing applications are built, and are necessary for your users to connect their audio and video to a session, particularly if they're behind an institutional firewall.</p> <p><em>Note</em>: in some cases, <em>institutions will have implemented a rather over-the-top-paranoid 'default deny' for video conference hosts</em> which will block access to your BBB instance unless you can get them to add your site's domain name to their institutional whitelist. Sadly, there's no easy way around this.</p> <p><code>sudo ufw allow 7443/tcp</code></p> <p><code>sudo ufw allow 16384:32768/udp</code></p> <p><code>sudo ufw allow 3478/udp</code></p> <p><code>sudo ufw allow 5349/tcp</code></p> <p><code>sudo ufw allow from 10.7.7.0/24</code></p> <p>Ok, that's it for the firewall shenanigans.</p> <h2><a id="user-content-install-postfix-so-the-server-can-send-email" href="#install-postfix-so-the-server-can-send-email" name="install-postfix-so-the-server-can-send-email" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install Postfix so the server can send email</h2> <p>It's generally a good idea to make sure any server you build can send email, and has a sensible default email address to send stuff to: stuff like administrative messages alerting you to failed tasks, or other system problems. We recommend that you designate an email address for whoever's responsible for this server. We've got a more <a href="/node/28">comprehsensive howto</a> for setting this up if you're wanting extra details.</p> <p>We achieve the outgoing mail functionality using the industrial strength Postfix SMTP server (it's widely used by ISPs for large-scale email services) along with a command line SMTP client that allows us to test our solution from the command line:</p> <p><code>sudo apt install postfix bsd-mailx</code></p> <p>During this installation, you'll be asked by the installer to make some decisions to do preliminary configuration of the Postfix installation. Select the default asnwers except as follows:</p> <ul><li>Select "Internet Site with Smarthost",</li> <li>fill in the [Domain] name you've chosen for your server,</li> <li>the domain name or IP address and port (in the form [SMTP server]:[port], examples might be smtp.oeru.org:465, or 10.11.143.22:465) of your "smarthost" who'll be doing the authenticating SMTP for you - note: <strong>our configuration works with port 465</strong>, and</li> <li>the email address to which you want to receive system-related messages, namely [Admin email].</li> </ul><p>Once that's installed, we need to set up our default email address for this server:</p> <p><code>sudo $EDIT /etc/aliases</code></p> <p>This file will containerd</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># See man 5 aliases for format</span> postmaster: root</pre></div></div> <p>update it to include an email address to send stuff intended for the system admin (aka 'root'):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># See man 5 aliases for format</span> postmaster: root   root: <span>[</span>Admin email<span>]</span></pre></div></div> <p>after writing that, we have to convert that file into a form understood by Postix:</p> <p><code>sudo newaliases</code></p> <p>Next, we need to create a new file with the SMTP 'relay host' aka 'smart host' details:</p> <p><code>sudo $EDIT /etc/postfix/relay_password</code></p> <p>In it you need to put the following:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>[</span>SMTP Server<span>]</span> <span>[</span>SMTP username<span>]</span>:<span>[</span>SMTP password<span>]</span></pre></div></div> <p>Here's an example:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>about.oerfoundation.org demosmtp<span>@</span>milll.ws:7TLM6qGoqZXHfkDmkh6</pre></div></div> <p>Once you've set that up, we have to again prepare that file for use by Postfix:</p> <p><code>sudo postmap /etc/postfix/relay_password</code></p> <p>Finally, we need to update Postfix's main configuration to tell it to use our authenticating SMTP details:</p> <p><code>sudo $EDIT /etc/postfix/main.cf</code></p> <p>You'll need to make main.cf look like this - specifically commenting out <code>smtp_tls_security_level=may</code> by adding a '#' at the start of the line, and then adding the details below <code># added to configure accessing the relay host via authenticating SMTP</code>. You should also take this opportunity to confirm that your [Domain], [SMTP server] and [SMTP port] are set correctly.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>smtpd_banner = <span>$myhostname</span> ESMTP <span>$mail_name</span> <span>(</span>Ubuntu<span>)</span> biff = no   <span># appending .domain is the MUA's job.</span> append_dot_mydomain = no   <span># Uncomment the next line to generate "delayed mail" warnings</span> <span>#delay_warning_time = 4h</span>   readme_directory = no   <span># See http://www.postfix.org/COMPATIBILITY_README.html -- default to 2 on</span> <span># fresh installs.</span> compatibility_level = <span>2</span>   <span># TLS parameters</span> <span>smtpd_tls_cert_file</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem <span>smtpd_tls_key_file</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key <span>smtpd_tls_security_level</span>=may   <span>smtp_tls_CApath</span>=<span>/</span>etc<span>/</span>ssl<span>/</span>certs <span>## Commented out for SmartHost configuration</span> <span>#smtp_tls_security_level=may</span> smtp_tls_session_cache_database = btree:<span>${data_directory}</span><span>/</span>smtp_scache     smtpd_relay_restrictions = permit_mynetworks permit_sasl_authenticated defer_unauth_destination myhostname = <span>[</span>Domain<span>]</span> alias_maps = hash:<span>/</span>etc<span>/</span>aliases alias_database = hash:<span>/</span>etc<span>/</span>aliases myorigin = <span>/</span>etc<span>/</span>mailname mydestination = <span>$myhostname</span>, bbbtest.milll.ws, localhost.milll.ws, localhost relayhost = <span>[</span>SMTP server<span>]</span>:<span>465</span> mynetworks = 127.0.0.0<span>/</span><span>8</span> <span>[</span>::ffff:127.0.0.0<span>]</span><span>/</span><span>104</span> <span>[</span>::<span>1</span><span>]</span><span>/</span><span>128</span> mailbox_size_limit = <span>0</span> recipient_delimiter = + inet_interfaces = all inet_protocols = all   <span># added to configure accessing the relay host via authenticating SMTP</span> smtp_sasl_auth_enable = <span>yes</span> smtp_sasl_password_maps = hash:<span>/</span>etc<span>/</span>postfix<span>/</span>relay_password smtp_sasl_security_options = smtp_tls_security_level = encrypt   <span># add this if you're using Ubuntu 20.04, and comment out (with a "#") the</span> <span># earlier line smtp_tls_security_level = may to save errors in 'postfix check'</span> <span># and uncomment this line (by removing the #)</span> smtp_tls_wrappermode = <span>yes</span></pre></div></div> <p>Here's an example of what the final code could look like (don't use these exact values as they won't work for you):</p> <p>First, I commented this out:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>#</span><span>smtp_tls_security_level</span>=may</pre></div></div> <p>Made sure this was my [Domain]</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>myhostname = bbbtest.milll.ws</pre></div></div> <p>Made sure this had my [SMTP server] and [SMTP port] details:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>relayhost = about.oerfoundation.org:<span>465</span></pre></div></div> <p>And I added this at the end:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># added to configure accessing the relay host via authenticating SMTP</span> smtp_sasl_auth_enable = <span>yes</span> smtp_sasl_password_maps = hash:<span>/</span>etc<span>/</span>postfix<span>/</span>relay_password smtp_sasl_security_options = smtp_tls_security_level = encrypt   <span># add this if you're using Ubuntu 20.04, and comment out (with a "#") the</span> <span># earlier line smtp_tls_security_level = may to save errors in 'postfix check'</span> <span># and uncomment this line (by removing the #)</span> smtp_tls_wrappermode = <span>yes</span></pre></div></div> <p>After this, the Postfix configuration is done. We can check that we haven't got any typos via</p> <p><code>sudo postfix check</code></p> <p>If not, we can apply our configuration changes:</p> <p><code>sudo postfix reload</code></p> <p>and we can confirm it all worked correctly by checking the log file for Postfix:</p> <p><code>sudo less +G /var/log/mail.log</code></p> <p>Note: you might see a warning like this: <code>postfix/postfix-script: warning: symlink leaves directory: /etc/postfix/./makedefs.out</code> - it's spurious and you don't need to worry about it.</p> <h3><a id="user-content-email-test" href="#email-test" name="email-test" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Email test</h3> <p>Next we can test our outgoing SMTP by sending a test message via the command line - <em>use a test email that you can check!</em>:</p> <p><code>mail [Test email]</code></p> <p>for example:</p> <p><code>mail demo@oerfoundation.org</code></p> <p>After you hit Enter, you'll be shown</p> <p><code>Subject:</code> {Enter an email subject, e.g. "Test email from [Domain]"}</p> <p>After that, hit Enter, and you'll be shown a blank line. On that line:</p> <p>{Enter your email text, e.g. "This is just a test."}</p> <p>Then, to finish your email, type <em>CTRL-D</em> which will show you another line</p> <p><code>CC:</code> {Enter any CC email addresses, e.g. <a href="mailto:mybackupemail@anotherprovider.tld">mybackupemail@anotherprovider.tld</a> }</p> <p>After you hit Enter, your email should be sent if all your configuration options are accepted by your remote host (the SMTP server at the address [SMTP server] with all the other details you entered).</p> <p>You can check if it worked by looking at the Postfix log again:</p> <p><code>sudo less +G /var/log/mail.log</code></p> <p>If it sent the email, you'll see something like (but with different IP addresses, serial numbers, and addresses) - the key bit is the <code>status=sent</code> bit and the <code>250</code> code from the SMTP server:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>08FBD2B501C: <span>to</span>=<span>&lt;</span>demo<span>@</span>oerfoundation.org<span>&gt;</span>, <span>relay</span>=dovecot<span>[</span>172.22.1.250<span>]</span>:<span>24</span>, <span>delay</span>=<span>1.3</span>, <span>delays</span>=<span>1.3</span><span>/</span><span>0.02</span><span>/</span><span>0</span><span>/</span><span>0.04</span>, <span>dsn</span>=2.0.0, <span>status</span>=sent <span>(</span><span>250</span> 2.0.0 <span>&lt;</span>demo<span>@</span>oerfoundation.org<span>&gt;</span> MMokCa2TpWEffAAAPgmdMA Saved<span>)</span></pre></div></div> <p>You can also check for receipt of email and verify receipt (note, if you don't get it quickily, check your email spam folder).</p> <h2><a id="user-content-install-nginx-webserver-and-lets-encrypt-tools" href="#install-nginx-webserver-and-lets-encrypt-tools" name="install-nginx-webserver-and-lets-encrypt-tools" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install Nginx webserver and Let's Encrypt tools</h2> <p><code>sudo apt install -y nginx-full ca-certificates letsencrypt ssl-cert</code></p> <p>You need to tell your firewall to open the ports that the Nginx webserver will use:</p> <p><code>sudo ufw allow "Nginx Full"</code></p> <p>And then we need to create a special configuration for Let's Encrypt and then an identify verification directory:</p> <p><code>sudo mkdir /etc/nginx/includes</code></p> <p><code>sudo mkdir /var/www/letsencrypt</code></p> <p>To create the specific configuration, we create this file:</p> <p><code>sudo $EDIT /etc/nginx/includes/letsencrypt.conf</code></p> <p>And fill it with this information:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Rule for legitimate ACME Challenge requests</span> location ^~ <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> default_type <span>"text/plain"</span>; <span># this can be any directory, but this name keeps it clear</span> root <span>/</span>var<span>/</span>www<span>/</span>letsencrypt; <span>}</span>   <span># Hide /acme-challenge subdirectory and return 404 on all requests.</span> <span># It is somewhat more secure than letting Nginx return 403.</span> <span># Ending slash is important!</span> location = <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> <span>return</span> <span>404</span>; <span>}</span></pre></div></div> <p>We'll reference this file in our Nginx configuration file for the reverse proxy functionality.</p> <h2><a id="user-content-set-up-coturn-certificates" href="#set-up-coturn-certificates" name="set-up-coturn-certificates" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up COTURN certificates</h2> <p>In preparation for creating our Let's Encrypt SSL certificate for our [Domain], we're going to set up a 'hook' which is triggered when our SSL certificate is created, and it copies our current certificate to a place where the COTURN server can find it.</p> <p><code>sudo $EDIT /etc/letsencrypt/renewal-hooks/deploy/coturn.sh</code></p> <p>Copy and past the following into your file (<strong>replacing [Domain] with your domain name</strong>):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>#!/bin/bash</span>   <span>DOMAIN</span>=<span>[</span>Domain<span>]</span> <span>DEST</span>=<span>/</span>etc<span>/</span>coturn-ssl   <span>#if [[ 1 == 1 ]]; then</span> <span>if</span> <span>[</span><span>[</span> <span>$RENEWED_DOMAINS</span> == <span>*</span><span>"<span>$DOMAIN</span>"</span><span>*</span> <span>]</span><span>]</span>; <span>then</span> <span>cp</span> <span>-L</span> <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>${DOMAIN}</span><span>/</span>fullchain.pem <span>$DEST</span> <span>cp</span> <span>-L</span> <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>${DOMAIN}</span><span>/</span>privkey.pem <span>$DEST</span> <span>echo</span> <span>"updated <span>$DOMAIN</span> certificates in <span>$DEST</span>"</span> <span>fi</span></pre></div></div> <p>Next, make that script executable:</p> <p><code>sudo chmod a+x /etc/letsencrypt/renewal-hooks/deploy/coturn.sh</code></p> <p>so that it is run automatically anytime the relevant certificate is created or renewed.</p> <p>After that, we have to create a place for the COTURN-specific certificates to go:</p> <p><code>sudo mkdir /etc/coturn-ssl</code></p> <h2><a id="user-content-set-up-nginx-reverse-proxy-configuration" href="#set-up-nginx-reverse-proxy-configuration" name="set-up-nginx-reverse-proxy-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Set up Nginx Reverse Proxy Configuration</h2> <p>Next we have to set up our Nginx webserver on our virtual server, which will be the Docker container host for our BigBlueButton system. The reverse proxy configuration will allow the virtual server to receive web requests from BBB users and pass them securely through to the right Docker container.</p> <p>Create a suitable configuration file - at the OERF we use the convention of calling our configuration</p> <p><code>sudo $EDIT /etc/nginx/sites-available/$SITE</code></p> <p>In this file, you'll past in the following, replacing [Domain] as appropriate - now might be a good idea to try out search and replace in your text editor!</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>server <span>{</span> listen <span>80</span>; <span># if your host doens't support IPv6, comment out the following line</span> listen <span>[</span>::<span>]</span>:<span>80</span>; server_name <span>[</span>Domain<span>]</span>;   access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>Domain<span>]</span>.access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>Domain<span>]</span>.error.log;   include includes<span>/</span>letsencrypt.conf;   <span># redirect all HTTP traffic to HTTPS.</span> location <span>/</span> <span>{</span> <span>return</span> <span>302</span> https:<span>//</span><span>$server_name</span><span>$request_uri</span>; <span>}</span> <span>}</span>   server <span>{</span> listen <span>443</span> ssl http2 default_server; <span># if your host doens't support IPv6, comment out the following line</span> listen <span>[</span>::<span>]</span>:<span>443</span> ssl http2 default_server; server_name <span>[</span>Domain<span>]</span>;   <span># We comment these out *after* we have successfully geneated our Let's Encrypt certificate for [Domain].</span> ssl_certificate <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key; <span># we start with these commented out until after we can generate our Let's Encrypt certificate for [Domain]!</span> <span>#ssl_certificate /etc/letsencrypt/live/[Domain]/fullchain.pem;</span> <span>#ssl_certificate_key /etc/letsencrypt/live/[Domain]/privkey.pem;</span> ssl_dhparam <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem;   access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>Domain<span>]</span>.access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>Domain<span>]</span>.error.log;   location <span>/</span> <span>{</span> proxy_http_version <span>1.1</span>; <span># for BBB 2.4, the port used is 48087. For earlier versions of BBB, it's 8080</span> proxy_pass http:<span>//</span>127.0.0.1:<span>48087</span>; proxy_set_header Host <span>$host</span>; proxy_set_header X-Real-IP <span>$remote_addr</span>; proxy_set_header X-Forwarded-For <span>$proxy_add_x_forwarded_for</span>; proxy_set_header X-Forwarded-Proto <span>$scheme</span>; proxy_set_header Upgrade <span>$http_upgrade</span>; <span>#proxy_set_header Connection $connection_upgrade;</span> proxy_set_header Connection <span>"Upgrade"</span>; proxy_cache_bypass <span>$http_upgrade</span>; <span>}</span> <span>}</span></pre></div></div> <p><em><strong>Update 2021-12-30</strong></em> With the release of BBB 2.4 a few weeks ago, the Docker configuration now has one 'breaking' change: the port has been changed to 48087! So, if you are installing an older version of BBB, the previous default was port 8080. If you get a 502 error after setting up your containers, check to make sure you haven't been bitten by this little detail!</p> <p>Right - back to the process...</p> <p>After creating it, we have to 'enable' the configuration:</p> <p><code>sudo ln -sf /etc/nginx/sites-available/$SITE /etc/nginx/sites-enabled/</code></p> <p>It's also not a bad idea to disable the Nginx default configuration, as it can sometimes interfere with things:</p> <p><code>sudo rm /etc/nginx/sites-enabled/default</code></p> <h3><a id="user-content-extra-security-with-dhparampem" href="#extra-security-with-dhparampem" name="extra-security-with-dhparampem" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Extra security with dhparam.pem</h3> <p>We're going to create a 'dhparam' certificate for your configuration. These take a long time to generate - between 10-30 minutes, depending not on the speed of your computer, but on the rate at which it creates 'random events' that allow it to create a suitablely complex random prime number.</p> <p><code>sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048</code></p> <p>If your time isn't limited, you can increase the size of your dhparam from 2048 to 4096 - it'll take quite a lot longer to create.</p> <p>Ok - now that we've created our dhparam.pem, referenced in our Nginx configuration for [Domain], we should have everything in place. We can now test Nginx's configuration:</p> <p><code>sudo nginx -t</code></p> <p>If you don't get any errors or warnings, you can activate the new configuration:</p> <p><code>sudo service nginx reload</code></p> <p>You server should now successfully respond to your [Domain], but it'll redirect to HTTPS using the default (self-signed) certificates that are valid as far as Nginx is concerned, but your browser won't let you access the page due to those inappropriate certificates. You can test it by putting <code>http://[Domain]</code> into your browser and seeing if it redirects you to <code>https://[Domain]</code> and a 'bad certificate` (or similar) page.</p> <p>To fix that, we can now generate a Let's Encrypt certificate which <em>will</em> be valid.</p> <p><code>sudo letsencrypt certonly --webroot -w /var/www/letsencrypt -d ${SITE}</code></p> <p>Since this is your first time running the letsencrypt script, you'll be asked for a contact email (so the Let's Encrypt system can warn you if your certificates are going to be expiring soon!) - use your [Admin email] for this. You can also opt in to allowing them to get anonymous statistics from your site.</p> <p>Once you've done that, the Let's Encrypt system will verify that you (the person requesting the certificate) also controls the server that's requesting it (using the details specified in the <code>/etc/nginx/includes/letsencrypt.conf</code> file, along with data that the letsencrypt script writes into a special file in the <code>/var/www/letsencrypt</code> directory) and, all going well, you'll see a "Congratulations!" message telling you that you have new certificates for your [Domain].</p> <p>Now is a good time to check if your renew-hook worked properly - there should now be two files in <code>/etc/coturn-ssl</code>, namely <code>fullchain.pem</code> and <code>privkey.pem</code>. If that's not the case, something might have gone wrong.</p> <p>Then you can re-edit your Nginx confguration file:</p> <p><code>sudo $EDIT /etc/nginx/sites-available/$SITE</code></p> <p>and comment out the default certificates and uncomment the [Domain]-specific certificates, like this (we'll assume you've already got your [Domain] substituted!):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>  ...   <span># We comment these out *after* we have successfully geneated our Let's Encrypt certificate for [Domain].</span> <span>#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;</span> <span>#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key; </span> <span># we start with these commented out until after we can generate our Let's Encrypt certificate for [Domain]!</span> ssl_certificate <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>Domain<span>]</span><span>/</span>fullchain.pem; ssl_certificate_key <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>Domain<span>]</span><span>/</span>privkey.pem; ssl_dhparam <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem;   ...  </pre></div></div> <p>Once you've got that going, you again check to make sure your Nginx config is valid:</p> <p><code>sudo nginx -t</code></p> <p>and apply your configruation changes:</p> <p><code>sudo service nginx reload</code></p> <p>If you now go to <code>http://[Domain]</code> in your browser, you should be redirected to <code>https://[Domain]</code> and you shouldn't get any <em>certificate</em> errors, although you might get a "502" error because the service that reverse proxy configuration is trying to send you to doesn't yet exist! That's what we're expecting.</p> <p>One more thing - we need to make sure that the <code>coturn.sh</code> renewal hook script has run. We can check to make sure that there are two files, <code>fullchain.pem</code> and <code>privkey.pem</code> in the <code>/etc/coturn-ssl</code> directory. If we start the Docker containers before these files exist, the Docker daemon creates them <em>as directories</em> by default which can lead to all sorts of trickiness. Run</p> <p><code>sudo ls -l /etc/coturn-ssl/</code></p> <p>and make sure you get a result like this:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>-rw-r--r--</span> <span>1</span> root root <span>5588</span> Dec <span>13</span> 08:<span>42</span> fullchain.pem <span>-rw-------</span> <span>1</span> root root <span>1704</span> Dec <span>13</span> 08:<span>42</span> privkey.pem</pre></div></div> <h2><a id="user-content-install-docker" href="#install-docker" name="install-docker" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install Docker</h2> <p>Now we have to set up the Docker container support by first registering a new package source...</p> <p><code>sudo apt-get install -y apt-transport-https curl gnupg lsb-release</code></p> <p><code>sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg</code></p> <p><code>sudo echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list &gt; /dev/null</code></p> <p>updating our list of available packages to include the Docker-related packages...</p> <p><code>sudo apt-get update</code></p> <p>and then install them:</p> <p><code>sudo apt-get install -y docker-ce docker-ce-cli containerd.io</code></p> <p>Which installs a bunch of dependencies as well.</p> <p>To make sure that our non-root user can talk to the Docker server, we need to jump through a couple more hoops:</p> <p><code>sudo addgroup docker</code></p> <p><code>sudo adduser dave docker</code></p> <p>After doing this, the easiest thing is to log out and log back in again to make sure my user can access the new permissions. To confirm that my user has docker group privileges, I can run</p> <p><code>id</code></p> <p>which should give a result like</p> <p><code>uid=1000(dave) gid=1000(dave) groups=1000(dave),4(adm),27(sudo),1001(docker)</code></p> <p>with the latter being the confirmation.</p> <h2><a id="user-content-install-docker-compose" href="#install-docker-compose" name="install-docker-compose" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install Docker-compose</h2> <p>Now we're almost to the BigBlueButton part - we just need to install the Docker Compose framework:</p> <p><code>sudo apt install python3-pip</code></p> <p><code>sudo pip install -U pip</code></p> <p><code>sudo pip install -U docker-compose</code></p> <p>Once you've done that, you should be able to run <code>docker-compose</code> at your command prompt and it should give you the docker-compose help page. If so, great work! Almost there.</p> <h2><a id="user-content-git-clone-bbb-docker-repository" href="#git-clone-bbb-docker-repository" name="git-clone-bbb-docker-repository" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Git-Clone BBB Docker repository</h2> <p>Now it's time to install the set of Docker containers that make up the BigBlueButton stack of coordinated services.</p> <p>We make a place for the docker configuration and data to live:</p> <p><code>sudo mkdir -p /home/docker/</code></p> <p>go into it</p> <p><code>cd /home/docker</code></p> <p>and then we use the magic of 'git' (if you don't know it, this is one of the single most powerful tools programmers use - everyone would benefit from knowing how 'version control' works - git is by far the most widely used version control aka 'source code control' system in the world. It's open source.) to 'clone' the BigBlueButton developers' Docker definitions code:</p> <p><code>sudo git clone -b main --recurse-submodules https://github.com/bigbluebutton/docker.git bbb-docker</code></p> <p>That command puts all the code into a directory called <code>bbb-docker</code> so let's go there:</p> <p><code>cd bbb-docker</code></p> <p>and in there, we run this command to gram a second layer of code that is referenced by the first layer we already downloaded in the previous step:</p> <p><code>sudo git submodule update --init</code></p> <p>Finally, we run this handy script provided by the BBB developers to set up a working Docker Compose configuration:</p> <p><code>sudo ./scripts/setup</code></p> <p>This will script will ask you some questions about your system. These are the questions <em>and</em> the answers we'll use - note, where I've written [IPv4] and [IPv6] below, you should see the actual IPv4 and IPv6 addresses for your server:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>Should greenlight be included? <span>(</span>y<span>/</span>n<span>)</span>: y Should an automatic HTTPS Proxy be included? <span>(</span>y<span>/</span>n<span>)</span>: n Should a coturn be included? <span>(</span>y<span>/</span>n<span>)</span>: y Coturn needs TLS to <span>function</span> properly. Since automatic HTTPS Proxy is disabled, you must provide a relative or absolute path to your certificates. Please enter path to cert.pem: <span>/</span>etc<span>/</span>coturn-ssl<span>/</span>fullchain.pem Please enter path to key.pem: <span>/</span>etc<span>/</span>coturn-ssl<span>/</span>privkey.pem Should a Prometheus exporter be included? <span>(</span>y<span>/</span>n<span>)</span>: y Please enter the domain name: bbbtest.milll.ws Should the recording feature be included? IMPORTANT: this is currently a big privacy issues, because it will record everything <span>which</span> happens <span>in</span> the conference, even when the button suggests, that it does not. <span>make</span> sure that you always get people<span>'s consent, before they join a room! https://github.com/bigbluebutton/bigbluebutton/issues/9202 Choice (y/n): y Is [IPv4] your external IPv4 address? (y/n): y Is [IPv6] your external IPv6 address? (y/n): y</span></pre></div></div> <p>Once you finish answering these questions, the script creates a file called <code>.env</code> (the leading '.' means it's a 'hidden' file that won't show up in normal directory listings - you have to know it's there, as it holds important system values and shouldn't be deleted.</p> <h2><a id="user-content-configure-your-bbb" href="#configure-your-bbb" name="configure-your-bbb" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configure your BBB</h2> <p>But now, we're going to tweak it, because it holds all the important customised values we need to configure our BigBlueButton service.</p> <p><code>sudo $EDIT .env</code></p> <p>When you're editing the file, scroll down through it and adjust the values you find as follows.</p> <p>Uncomment this one</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>ALLOW_MAIL_NOTIFICATIONS</span>=<span>true</span></pre></div></div> <p>Set the following</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>SMTP_SERVER</span>=<span>[</span>SMTP server<span>]</span> <span>SMTP_PORT</span>=<span>[</span>SMTP port<span>]</span> <span>SMTP_DOMAIN</span>=<span>[</span>Domain<span>]</span> <span>SMTP_USERNAME</span>=<span>[</span>SMTP username<span>]</span> <span>SMTP_PASSWORD</span>=<span>[</span>SMTP password<span>]</span> <span>SMTP_AUTH</span>=plain <span>SMTP_STARTTLS_AUTO</span>=<span>true</span> <span>#</span> <span>SMTP_SENDER</span>=<span>[</span>Outgoing email<span>]</span></pre></div></div> <p>and finally set</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>DEFAULT_REGISTRATION</span>=invite</pre></div></div> <p>This last one makes it possible for you to invite external people to make use of your BigBlueButton instance - they can create a log in and create their own rooms over which they'll have some control. These can be people in your organisation or community for whom you want your BBB to be available as a resource.</p> <h3><a id="user-content-fix-minor-configuration-error-that-blocks-jodconverter" href="#fix-minor-configuration-error-that-blocks-jodconverter" name="fix-minor-configuration-error-that-blocks-jodconverter" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Fix minor configuration error that blocks JODConverter</h3> <p>One of the containers used in the BBB stack is called <a href="https://github.com/bigbluebutton/docker/tree/develop/mod/jodconverter">JODConverter</a> which, in standard Free and Open Source Software tradition, makes use of an <a href="https://github.com/EugenMayer/docker-image-jodconverter">'upstream' development</a> by another developer, <a href="https://github.com/EugenMayer">EugenMayer</a>, which in turn makes use of an <a href="https://hub.docker.com/r/bellsoft/liberica-openjdk-debian">upstream development</a>...</p> <p>The issue is that a recent update (in early December 2021) has broken the process of launching this container. This is a big problem because the JODConverter provides the services of converting uploaded presentations and downloaded documents (like BBB's Public Chat or Shared Notes) in various useful file formats.</p> <p>Turns out the fix is easy. While still in <code>bbb-docker</code>, Just run</p> <p><code>sudo $EDIT mod/jodconverter/Dockerfile</code></p> <p>and add the following line to the very bottom of the file:</p> <p><code>CMD ["--spring.config.additional-location=optional:/etc/app/"]</code></p> <p>Save it, and you're done. I have <a href="https://github.com/bigbluebutton/docker/issues/178">submitted an issue</a> with the fix I've found to the <code>bigbluebutton/docker</code> project.</p> <p>Once that configuration is done, it's finally time to...</p> <h2><a id="user-content-build-your-bbb" href="#build-your-bbb" name="build-your-bbb" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Build your BBB</h2> <p>The first time it's run, this command will trigger the building of a set of no less than 22 separate Docker containers, each running its own crucial service as part of the BigBlueButton stack.</p> <p><code>sudo docker-compose up -d</code></p> <p><em>Note: this process can take a LONG time, like an hour or more</em> depending on your server's internet connection speed.</p> <p>If you want to see how long it takes the first time, run this instead:</p> <p><code>sudo time docker-compose up -d</code></p> <p>which will give you a readout of the time the command takes to complete.</p> <h2><a id="user-content-visit-your-new-bbb" href="#visit-your-new-bbb" name="visit-your-new-bbb" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Visit your new BBB</h2> <p>Once you next see the command prompt ($ or #), it means that your BBB system is starting up. You can see the status of the containers by running</p> <p><code>sudo docker-compose ps</code></p> <p>which should give you something that looks like this, once everything is running (it might take a few minutes!):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre> Name Command State Ports <span>-----------------------------------------------------------------------------------------------------------</span> bbb-docker_apps-akka_1 <span>/</span>bin<span>/</span><span>sh</span> <span>-c</span> dockerize - ... Up bbb-docker_bbb-web_1 <span>/</span>entrypoint.sh Up <span>(</span>healthy<span>)</span> bbb-docker_coturn_1 docker-entrypoint.sh <span>--ext</span> ... Up bbb-docker_etherpad_1 <span>/</span>entrypoint.sh Up <span>9001</span><span>/</span>tcp bbb-docker_freeswitch_1 <span>/</span>bin<span>/</span><span>sh</span> <span>-c</span> <span>/</span>entrypoint.sh Up bbb-docker_fsesl-akka_1 <span>/</span>bin<span>/</span><span>sh</span> <span>-c</span> dockerize - ... Up bbb-docker_greenlight_1 bin<span>/</span>start Up 10.7.7.1:<span>5000</span>-<span>&gt;</span><span>80</span><span>/</span>tcp bbb-docker_html5-backend-<span>1</span>_1 <span>/</span>entrypoint.sh Up bbb-docker_html5-backend-<span>2</span>_1 <span>/</span>entrypoint.sh Up bbb-docker_html5-frontend-<span>1</span>_1 <span>/</span>entrypoint.sh Up bbb-docker_html5-frontend-<span>2</span>_1 <span>/</span>entrypoint.sh Up bbb-docker_jodconverter_1 <span>/</span>docker-entrypoint.sh <span>--sp</span> ... Up bbb-docker_kurento_1 <span>/</span>entrypoint.sh Up <span>(</span>healthy<span>)</span> bbb-docker_mongodb_1 docker-entrypoint.sh mongo ... Up <span>(</span>healthy<span>)</span> <span>27017</span><span>/</span>tcp bbb-docker_nginx_1 <span>/</span>docker-entrypoint.sh ngin ... Up bbb-docker_periodic_1 <span>/</span>entrypoint.sh Up bbb-docker_postgres_1 docker-entrypoint.sh postgres Up <span>(</span>healthy<span>)</span> <span>5432</span><span>/</span>tcp bbb-docker_prometheus-exporter_1 python server.py Up <span>9688</span><span>/</span>tcp bbb-docker_recordings_1 <span>/</span>bin<span>/</span><span>sh</span> <span>-c</span> <span>/</span>entrypoint.sh Up bbb-docker_redis_1 docker-entrypoint.sh redis ... Up <span>(</span>healthy<span>)</span> <span>6379</span><span>/</span>tcp bbb-docker_webhooks_1 <span>/</span>bin<span>/</span><span>sh</span> <span>-c</span> <span>/</span>entrypoint.sh Up bbb-docker_webrtc-sfu_1 .<span>/</span>docker-entrypoint.sh npm ... Up 127.0.0.1:<span>3008</span>-<span>&gt;</span><span>3008</span><span>/</span>tcp</pre></div></div> <p>At that point, you can visit <code>https://[Domain]</code> and instead of a 502 error, you should see the BigBlueButton 'Greenlight' front page.</p> <p>Now the final step...</p> <h2><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:</h2> <p>To create an admin user, you run this:</p> <p><code>sudo docker-compose exec greenlight bundle exec rake admin:create</code></p> <p>It will give you a login email address and a randomly generated password. Use these to log in. In the profile (top right Admin menu dropdown), you can alter the admin user's email to a real email (perhaps [Admin email]?), and change the password if you like.</p> <p>Then you can go back to the Admin menu and select "Organisation" which should put you on the Manage Users page. You then invite yourself to join as a user by sending yourself an email (which, if your SMTP settings are correct, will work). If you don't receive it in a minute or two, check your spam folder.</p> <p>Either log out of Greenlight before clicking the link in the email or open the link (by copying and pasting it) in a different browser where you're not logged in to this Greenlight instance, and create a user account for yourself.</p> <p>Then log back into Greenlight as the Admin user (if you've previously logged out) and refresh the "Manage Users" page. You should find your newly created user. Select 'Edit' from the vertical 3 dotted menu. On the "Update your Account Info" page, set the User Role for your user to 'Admin" and click the "Update" button. You should now be able to log out as Admin, and back in as your own user, but with administrative privileges.</p> <h2><a id="user-content-run-your-first-conference" href="#run-your-first-conference" name="run-your-first-conference" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Run your first conference</h2> <p>Now it's time to try running a video conference. Click on the "Home" link (top right menu) and you should see that you have a default room called "Home Room" with a URL specified in the form [Domain]/b/[first 3 letters of your name]-[3 random hex digits]-[3 more random hex digits]. For me, it might look something like this: https://[Domain]/b/dav-a5b-73z</p> <p>Click "Start" to initiate a conference in that Home Room.</p> <p>In the conference room, you should select 'Microphone' so you can test speaking and listening, and also test your video camera (assming you have access to both on the computing device you're using to access BBB - note, a modern cellphone normally offers all of these.</p> <p><em>We always encourage people participating in video conferences (regardless of the technology) to employ headphones to separate the audio from the conference form their input. It greatly improves the audio quality for all involved!</em>.</p> <p>If you select 'Microphone', there will be a brief delay whil your browser negotiates with the COTURN server in your BBB stack to link your data and audio channels. Once it's done that, it will give you an "echo test" window (see included screen shots). You should speak into your microphone when you see that screen and check whether you can hear yourself. This will confirm that your audio settings are right (or not) and will help the BBB system adjust its echo cancellation algorithms.</p> <p>You can then also clidk the 'camera' icon (bottom middle of the page) to activate your webcam if you have one (or choose one if you have multiple cameras) as well as the video quality.</p> <p>You can also try to 'Start recording' if you want to test your ability to record a session. Note that there is a delay after the end of a session in which you've recorded before the recording is displayed on the 'room' page in Greenlight. It might take minutes or even hourse to generate depending on the power of your server and the length of the session.</p> <p>To see other administrative functionality including moderation and breakout rooms, click on the "gear" icon next to the "Users" heading in the left hand column. You can also experiment with the "Public Chat" and the "Shared Notes". Both can be saved at any time (via the top right 3 dot menu in that section) and will be included in any recordings you make.</p> <p><em>Note</em> the contents of Public Chat and Shared Notes will be wiped at the end of a session unless you explicitly save them or record the session (at least briefly at the end).</p> <p>You can provide that "room address" to anyone and they can join your room (via an modern browser) when you have a session running. Alternatively, you can click on the 3 dotted menu associated with your room below the 'Search for room" form, and change the default properties of your room, including configuring it to let anyone who knows the room's address start a session. You can also create additional rooms (as an Admin user, you can set the limits on many of these properties via Organisation in the top right menu under your user name.</p> <p>Have fun with your new, world class, cost-effective, large-scale BigBlueButton video conferencing application!</p> <h2><a id="user-content-next-steps" href="#next-steps" name="next-steps" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Next Steps</h2> <p>In a future tutorial, we'll provide information on how to troubleshoot BBB issues, how to upgrade it as new versions are made available by developers, and how to ensure that your recordings, user database, and configuration are backed up incrementally, encrypted, in remote storage.</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> <h2 class="comment-field__title">Blog comments</h2> <article data-comment-user-id="0" id="comment-826" class="comment js-comment by-anonymous has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/826#comment-826" class="permalink" rel="bookmark" hreflang="en">It says in the nginx-config…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1641281859"></span> </h3> <div class="comment__meta"> <div class="comment__submitted"> <span class="comment__author"><span>LPup (not verified)</span></span> <span class="comment__pubdate">Tue 04/01/2022 - 20:32</span> </div> </div> <div class="comment__content"> <div class="clearfix text-formatted field field-comment--comment-body field-name-comment-body field-type-text-long field-label-hidden"> <div class="field__items"> <div class="field__item"><p>It says in the nginx-config to change the port for the proxy and not forget to change it in the .env file. Where do I need to put what into the .env file for using a different port than 8080?</p> </div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=826&amp;1=default&amp;2=en&amp;3=" token="ab5iIZGMF89MSnCPgbrcuI7yHqfoKEyjDb9VtJ299fg"></drupal-render-placeholder> </div> </div> </article> <div class="indented"><article data-comment-user-id="1" id="comment-827" class="comment js-comment by-node-author has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/827#comment-827" class="permalink" rel="bookmark" hreflang="en">Hmm - thanks for pointing…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1641282307"></span> </h3> <div class="comment__meta"> <div class="comment__submitted"> <span class="comment__author"><a title="View user profile." href="/user/1" class="username">dave</a></span> <span class="comment__pubdate">Tue 04/01/2022 - 20:44</span> </div> </div> <div class="comment__content"> <p class="comment__parent visually-hidden">In reply to <a href="/comment/826#comment-826" class="permalink" rel="bookmark" hreflang="en">It says in the nginx-config…</a> by <span>LPup (not verified)</span></p> <div class="clearfix text-formatted field field-comment--comment-body field-name-comment-body field-type-text-long field-label-hidden"> <div class="field__items"> <div class="field__item"><p>Hmm - thanks for pointing that out. Yes, I think I made an error with that instruction (I've updated the Nginx config). If you implement BBB today via this set of instructions, you'll be installing BBB 2.4 - port 8080 was used in 2.3 and earlier versions. For 2.4, use port 48087. To be honest, I'm not sure of the right way to alter it if you have to due to other services running on the same system. Writing it manually in the docker-compose.yml file will be overwritten the next time you do an update... It might be necessary to change the port specified in the relevant container's Dockerfile under <code>mods</code>...</p></div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=827&amp;1=default&amp;2=en&amp;3=" token="IQwvVUQ-NlVwsrO5mMYIbQJJDVotfViSFCA7rOx4_Aw"></drupal-render-placeholder> </div> </div> </article> </div> <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=46&amp;2=field_blog_comments&amp;3=comment" token="bVuNCPDsukUifzhtwLxvL6UanvQCaG2zyJeoiuzQckE"></drupal-render-placeholder> </div> </section> Wed, 10 Nov 2021 02:42:41 +0000 dave 46 at http://tech.oeru.org OERF FOSS Tech Hosting Conventions http://tech.oeru.org/oerf-foss-tech-hosting-conventions <span class="field field--name-title field--type-string field--label-hidden">OERF FOSS Tech Hosting Conventions</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--overview"> <span class="field__item-wrapper"><a href="/taxonomy/term/76" hreflang="en">overview</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--mariadb"> <span class="field__item-wrapper"><a href="/taxonomy/term/48" hreflang="en">mariadb</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--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--ssh"> <span class="field__item-wrapper"><a href="/taxonomy/term/77" hreflang="en">ssh</a></span> </div> <div class="field__item field__item--restic"> <span class="field__item-wrapper"><a href="/taxonomy/term/78" hreflang="en">restic</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 class="field__item field__item--git"> <span class="field__item-wrapper"><a href="/taxonomy/term/79" hreflang="en">git</a></span> </div> <div class="field__item field__item--gitlab"> <span class="field__item-wrapper"><a href="/taxonomy/term/80" hreflang="en">gitlab</a></span> </div> <div class="field__item field__item--foss"> <span class="field__item-wrapper"><a href="/taxonomy/term/10" hreflang="en">foss</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" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Mon 30/08/2021 - 11:44</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-08/SSH_Session_Screenshot_about.oerfoundation.org-20210830.png?itok=yu7PbTtx" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A snapshot of an SSH session on about.oerfoundation.org, located in San Francisco, CA, USA, and its performance stats as viewed from Christchurch, New Zealand. &quot;}" role="button" title="A snapshot of an SSH session on about.oerfoundation.org, located in San Francisco, CA, USA, and its performance stats as viewed from Christchurch, New Zealand. " data-colorbox-gallery="gallery-field_image-YhI9kzW9JIo" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A snapshot of an SSH session on about.oerfoundation.org, located in San Francisco, CA, USA, and its performance stats as viewed from Christchurch, New Zealand. &quot;}"><img src="/sites/default/files/styles/medium/public/2021-08/SSH_Session_Screenshot_about.oerfoundation.org-20210830.png?itok=6cfu9o9O" width="220" height="105" alt="A snapshot of an SSH session on about.oerfoundation.org, located in San Francisco, CA, USA, and its performance stats as viewed from Christchurch, New Zealand. " loading="lazy" 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-08/Editing_tech.oeru_.org_on_about.oerfoundation.org-20210830.png?itok=-EfYK9sU" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Editing the docker-compose.yml file for this site using the Vim editor on about.oerfoundation.org via SSH.&quot;}" role="button" title="Editing the docker-compose.yml file for this site using the Vim editor on about.oerfoundation.org via SSH." data-colorbox-gallery="gallery-field_image-YhI9kzW9JIo" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Editing the docker-compose.yml file for this site using the Vim editor on about.oerfoundation.org via SSH.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-08/Editing_tech.oeru_.org_on_about.oerfoundation.org-20210830.png?itok=npeVOBsh" width="220" height="176" alt="Editing the docker-compose.yml file for this site using the Vim editor on about.oerfoundation.org via SSH." loading="lazy" 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-08/Screenshot%202021-08-30%20at%2016-21-09%20OERu.png?itok=TwAuGCsB" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A snapshot of OERu FOSS software projects in our Gitlab instance. All are visible to anyone and we invite contributions.&quot;}" role="button" title="A snapshot of OERu FOSS software projects in our Gitlab instance. All are visible to anyone and we invite contributions." data-colorbox-gallery="gallery-field_image-YhI9kzW9JIo" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A snapshot of OERu FOSS software projects in our Gitlab instance. All are visible to anyone and we invite contributions.&quot;}"><img src="/sites/default/files/styles/medium/public/2021-08/Screenshot%202021-08-30%20at%2016-21-09%20OERu.png?itok=3FmBKhhs" width="202" height="220" alt="A snapshot of OERu FOSS software projects in our Gitlab instance. All are visible to anyone and we invite contributions." loading="lazy" 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>At the <a href="https://oerfoundation.org">OER Foundation</a> we host <a href="/node/34">an array</a> of Free and Open Source Software (FOSS) services. To keep things manageable, we have established a set of conventions to which we adhere, and I'm describing them here in case it's helpful to others who can either learn from, or improve on, our experience.</p> <p>To begin with, we use <a href="https://ubuntu.com">Ubuntu Linux</a> for all of our hosts. We deploy the current Long Term Support (LTS) version of Ubuntu (at the time of this writing, it's 20.04) on commodity Linux hosting services, like Digital Ocean and Hetzner.</p> <h2><a id="user-content-host-access" href="#host-access" name="host-access" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Host access</h2> <p>We access all our hosts on the command line, via secure, end-to-end encrypted connections, using <a href="https://openssh.com">OpenSSH</a>. We don't use a server management tool (neither an open source one, like ISPConfig or Webmin, nor proprietary ones like Plex or CPanel). They would just slow us down. We prefer the efficiency and power afforded us by SSH. At any given time, I'm logged into a half a dozen remote systems, issuing updates, deploying code, diagnosing issues, checking resource usage, shoring up disk space, or tweaking Docker configurations.</p> <h2><a id="user-content-software-on-the-host" href="#software-on-the-host" name="software-on-the-host" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Software on the host</h2> <p>On the host, we run the following:</p> <ol><li>We run <a href="https://etckeeper.branchable.com/">etckeeper</a> on each server to track changes in configuration. In general, we have these changes pushed to a central server to provide a reference in the event of a disaster (e.g. the server is compromised and needs to be 'nuked from orbit'...).</li> <li>The <a href="https://nginx.org">Nginx</a> web server - it's our reverse proxy of choice. All of our <a href="/node/11">Let's Encrypt</a> certificates terminate at the host server, greatly simplifying the deployment of the individual services running on it.</li> <li>The <a href="https://postfix.org">Postfix</a> SMTP server - it's our outgoing mailserver of choice, and we <a href="/node/28">set it up as a smarthost</a>, so that the server can send status and administrative messages to us via our <a href="https://mailcow.email">MailCow</a> email infrastructure using authenticating secure SMTP.</li> <li>The <a href="https://docs.docker.com/get-started/overview/">Docker</a> containerisation framework using the <a href="https://docs.docker.com/compose/">Docker Compose</a> system to coordinate collections of containers that work together to provide specific services. Docker allows us to run a bunch of effectively independent Linux systems, each with potentially different sets of complex software dependencies, efficiently and reliably on a single host.</li> <li>For each service, with an individual domain or subdomain name, we get a <a href="https://letsencrypt.org">Let's Encrypt</a> <a href="https://en.wikipedia.org/wiki/Transport_Layer_Security#SSL_1.0,_2.0,_and_3.0">SSL certificate</a> using the letsencrypt scripting toolchain. It is free of cost, and <a href="/node/11">easy to build into our workflow</a>, so it's a 'no-brainer' as far as we're concerned, ensuring all of our sites protect the data of their users with full encrypted links!</li> <li>For applications we deploy that depend on the MySQL database, we run a single instance of the fully MySQL-compatible <a href="https://mariadb.org/">MariaDB</a> on our host, and the Docker containers connect to it via the internal network. We do it this way to easy the process of backing up the various MariaDB databases, which would be much more fiddly if we were running a bunch of individual MariaDB or MySQL instances in Docker containers. We use MariaDB because we prefer its development model, design decisions, and the fact that it's not being run by the Oracle Corporation, which owns the MySQL project.</li> <li>We use <a href="https://restic.net/">Restic</a> to perform remote encrypted incremental file backups for the filesystems on each server. We send them to a development server we have which has oodles of disk space (<a href="https://en.wikipedia.org/wiki/Btrfs">BTRFS</a> <a href="https://en.wikipedia.org/wiki/Standard_RAID_levels#RAID_1">RAID1</a> if you're curious) for safekeeping.</li> </ol><p>When each new Ubuntu LTS release comes out, we tend to create an install image for our commodity hosting provider (most recently, Digital Ocean), which has all these things pre-installed.</p> <h2><a id="user-content-docker-deployments" href="#docker-deployments" name="docker-deployments" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Docker deployments</h2> <p>For the various services we deploy using Docker - more specifically, using the very handy Docker Compose scripting toolchain, we have a bunch of conventional practices.</p> <ol><li>We put all our Docker Composer recipes in a common directory: <code>/home/docker</code> - each service goes in a directory using the domain name of that service. For example, this Tech blog, tech.oeru.org is in the directory <code>/home/docker/tech.oeru.org</code> on our server about.oerfoundation.org. Each service directory has a <code>docker-compose.yml</code> file which defines the collection of Docker containers making up each service. It also specifies the local directors in which data we want to make persistent, even surviving the removal of relevant Docker containers. In the case of this site, the containers include an Nginx webserver container which submits requests delivered to it by our hosts's Nginx reverse proxy to a PHP scripting engine container which, in turn, consults the Drupal 8 source code making up the site, and the MariaDB on the host which stores the data. There is also a PHP container which automatically runs the behind-the-scenes automated 'cron' tasks that every Drupal site requires.</li> <li>We store that per-service persistent data in a similarly named directory under <code>/home/data</code>, so this site's data is stored in <code>/home/data/tech.oeru.org</code>. That data includes, in the case of this site, an Nginx directory, with a generic Drupal website configuration, and a directory to contain all the Drupal 8 core source code and that for theme, module, and library dependencies. In a few cases, where we're using tools with specialised Docker deployment practices, like Mailcow, BigBlueButton, and Discourse, the persistent data for the site is stored under the <code>/home/docker</code> directory to avoid unnecessarily complicating our use of those tools. So our conventions are just that - not hard and fast, if there's a good reason to compromise them.</li> </ol><h2><a id="user-content-keeping-track" href="#keeping-track" name="keeping-track" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Keeping track</h2> <p>For all of our service configurations, and, in particular our Docker deployments, we use <a href="https://git-scm.com">Git</a> to provide source code versioning and management. We also use it to deploy code that we have developed ourselves. Where possible, we use (and contribute back to!) upstream git repositories supplied by the communities surrounding many of the FOSS services we offer. We see that as doing our part to be good, contributing FOSS community members.</p> <p>All of our git repositories are held on our own, self-hosted <a href="https://about.gitlab.com/community/">Gitlab</a> instance: <a href="https://git.oeru.org/explore/projects">https://git.oeru.org</a> - anyone is welcome to peruse the repositories, and we invite anyone interested in contributing to <a href="/contact">request a an account</a> (give us an idea of what you're interested in doing and how you'd like to participate!).</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=39&amp;2=field_blog_comments&amp;3=comment" token="mc3_yQFNlAQZsoRapw9jke6XUslZiwMrWV52Y56Nyas"></drupal-render-placeholder> </div> </section> Sun, 29 Aug 2021 23:44:43 +0000 dave 39 at http://tech.oeru.org 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" 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-palsgrNdYu0" 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" 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-palsgrNdYu0" 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" 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-palsgrNdYu0" 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" 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><span>#DEFAULT_FORWARD_POLICY="DROP"</span> <span>DEFAULT_FORWARD_POLICY</span>=<span>"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><span># Uncomment this to allow this host to route packets between interfaces</span> net<span>/</span>ipv4<span>/</span><span>ip_forward</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>default<span>/</span><span>forwarding</span>=<span>1</span> net<span>/</span>ipv6<span>/</span>conf<span>/</span>all<span>/</span><span>forwarding</span>=<span>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><span>[</span>client<span>]</span> <span>user</span>=root <span>password</span>=<span>[</span>database root password<span>]</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>CREATE USER <span>"[your username]"</span><span>@</span><span>"localhost"</span> IDENTIFIED BY <span>"[your database password]"</span>; CREATE USER <span>"[your username]"</span><span>@</span><span>"%"</span> IDENTIFIED BY <span>"[your database password]"</span>; GRANT ALL ON <span>*</span>.<span>*</span> to <span>"[your username]"</span><span>@</span><span>"localhost"</span> WITH GRANT OPTION; GRANT ALL ON <span>*</span>.<span>*</span> to <span>"[your username]"</span><span>@</span><span>"%"</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><span># Instead of skip-networking the default is now to listen only on</span> <span># localhost which is more compatible and is not less secure.</span> <span>#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>CREATE DATABASE <span>[</span>database name<span>]</span> CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER <span>"[database user]"</span><span>@</span><span>"%"</span> IDENTIFIED BY <span>"[database password]"</span>; GRANT ALL ON <span>[</span>database name<span>]</span>.<span>*</span> to <span>"[database user]"</span><span>@</span><span>"%"</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><span>#</span> <span># Set [domain name] and [port] below to make this work</span> <span>#</span> <span># HTTP does *soft* redirect to HTTPS</span> <span>#</span> server <span>{</span> <span># add [IP-Address:]80 in the next line if you want to limit this to a single interface</span> listen <span>80</span>; listen <span>[</span>::<span>]</span>:<span>80</span>; server_name <span>[</span>domain name<span>]</span>; root <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>scr; index index.php;   <span># change the file name of these logs to include your server name</span> <span># if hosting many services...</span> access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>domain name<span>]</span>_access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>domain name<span>]</span>_error.log;   <span># for let's encrypt renewals!</span> include includes<span>/</span>letsencrypt.conf;   <span># redirect all HTTP traffic to HTTPS.</span> location <span>/</span> <span>{</span> <span>return</span> <span>302</span> https:<span>//</span><span>[</span>domain name<span>]</span><span>$request_uri</span>; <span>}</span> <span>}</span> <span>#</span> <span># HTTPS</span> <span>#</span> <span># This assumes you're using Let's Encrypt for your SSL certs (and why wouldn't</span> <span># you!?)... https://letsencrypt.org</span> server <span>{</span> <span># add [IP-Address:]443 ssl in the next line if you want to limit this to a single interface</span> listen <span>443</span> ssl; listen <span>[</span>::<span>]</span>:<span>443</span> ssl; <span># Note: these are *temporary* certificates, created when your host was set up</span> <span># 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>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>ssl-cert-snakeoil.pem; ssl_certificate_key <span>/</span>etc<span>/</span>ssl<span>/</span>private<span>/</span>ssl-cert-snakeoil.key; <span># these will be used after we finish the Let's Encrypt process</span> <span>#ssl_certificate /etc/letsencrypt/live/[domain name]/fullchain.pem;</span> <span>#ssl_certificate_key /etc/letsencrypt/live/[domain name]/privkey.pem;</span> ssl_protocols TLSv1 TLSv1.1 TLSv1.2; <span># to create this, see https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html</span> ssl_dhparam <span>/</span>etc<span>/</span>ssl<span>/</span>certs<span>/</span>dhparam.pem; keepalive_timeout 20s;   server_name <span>[</span>domain name<span>]</span>; root <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>src; index index.php;   <span># change the file name of these logs to include your server name</span> <span># if hosting many services...</span> access_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>domain name<span>]</span>_access.log; error_log <span>/</span>var<span>/</span>log<span>/</span>nginx<span>/</span><span>[</span>domain name<span>]</span>_error.log;   location <span>/</span> <span>{</span> <span># a good value for [port] is 8080, unless it's already in use by another service on your server...</span> proxy_pass http:<span>//</span>127.0.0.1:<span>[</span>port<span>]</span>; proxy_set_header Upgrade <span>$http_upgrade</span>; proxy_set_header Connection <span>"upgrade"</span>; proxy_set_header Host <span>$http_host</span>; proxy_set_header X-Real-IP <span>$remote_addr</span>; proxy_set_header X-Forwarded-For <span>$proxy_add_x_forwarded_for</span>; proxy_set_header X-Forwarded-Host <span>$server_name</span>; proxy_set_header X-Forwarded-Proto https; proxy_connect_timeout <span>900</span>; proxy_send_timeout <span>900</span>; proxy_read_timeout <span>900</span>; send_timeout <span>900</span>; <span>}</span> <span>#</span> <span># These "harden" your security</span> add_header <span>'Access-Control-Allow-Origin'</span> <span>"*"</span>; <span># from https://gist.github.com/Stanback/7145487</span> add_header <span>'Access-Control-Allow-Credentials'</span> <span>'true'</span> always; add_header <span>'Access-Control-Allow-Methods'</span> <span>'GET, POST, PUT, DELETE, OPTIONS'</span> always; add_header <span>'Access-Control-Allow-Headers'</span> <span>'Accept,Authorization,Cache-Control,Content-Type,DNT,If-Modified-Since,Keep-Alive,Origin,User-Agent,X-Requested-With'</span> always; <span>#</span> <span># for H5P embedding</span> <span>#add_header 'X-Frame-Options' 'ALLOW-FROM https://h5p.oeru.org';</span> <span># required to be able to read Authorization header in frontend</span> add_header <span>'Access-Control-Expose-Headers'</span> <span>'Authorization'</span> always; <span># tested at https://csp-evaluator.withgoogle.com/</span> <span># works, but only B+ on MozOBs https://observatory.mozilla.org/analyze.html</span> add_header X-XSS-Protection <span>"1; mode=block"</span>; <span>}</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><span># Rule for legitimate ACME Challenge requests</span> location ^~ <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> default_type <span>"text/plain"</span>; <span># this can be any directory, but this name keeps it clear</span> root <span>/</span>var<span>/</span>www<span>/</span>letsencrypt; <span>}</span>   <span># Hide /acme-challenge subdirectory and return 404 on all requests.</span> <span># It is somewhat more secure than letting Nginx return 403.</span> <span># Ending slash is important!</span> location = <span>/</span>.well-known<span>/</span>acme-challenge<span>/</span> <span>{</span> <span>return</span> <span>404</span>; <span>}</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> <span># Note: these are *temporary* certificates, created when your host was set up</span> <span># they are only in use to get Nginx to start up properly and let you create your let's encrypt certificates!</span> <span>#ssl_certificate /etc/ssl/certs/ssl-cert-snakeoil.pem;</span> <span>#ssl_certificate_key /etc/ssl/private/ssl-cert-snakeoil.key;</span> <span># these will be used after we finish the Let's Encrypt process</span> ssl_certificate <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>domain name<span>]</span><span>/</span>fullchain.pem; ssl_certificate_key <span>/</span>etc<span>/</span>letsencrypt<span>/</span>live<span>/</span><span>[</span>domain name<span>]</span><span>/</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>define<span>(</span><span>'AUTH_KEY'</span>, <span>'?4^Huc~R1=WW+T_p~.0dH$XJ`&gt;U*MoreMmZ{@tORSFG3aX37#ZS+0ou{j^DS3{f&lt;'</span><span>)</span>; define<span>(</span><span>'SECURE_AUTH_KEY'</span>, <span>'7.k4htjPnrA/?6JJlogA4Wp*o|,&amp;&amp;&gt;;20ppqeqHq#gI &lt;%gDz[o( hpRRB|!jws%'</span><span>)</span>; define<span>(</span><span>'LOGGED_IN_KEY'</span>, <span>'fTX,WkI=doAUE%?{zHp5.?fN%WWtBuy~`Scntr&lt;]I1WvlF6i=7J kjO0Z%%~Z-`N'</span><span>)</span>; define<span>(</span><span>'NONCE_KEY'</span>, <span>'?p&amp;]/*(G-+W!0#[&amp;Y6KKj)j Ok5QI(SUc@@rv,ivtF&gt; AR;Yv+Yu#&gt;$B$&lt;P9Ld|j'</span><span>)</span>; define<span>(</span><span>'AUTH_SALT'</span>, <span>'H6s2H~KP]Z7YXTFt|8[Lgz[1~5wF+PJzxR^KW$|he+9|RF/vi@}/|&lt;8bkC:w)qW%'</span><span>)</span>; define<span>(</span><span>'SECURE_AUTH_SALT'</span>, <span>'@- j6Kn CjP/mdbmLXtkC+&gt;1&gt;+H-8pXETlJ4+]b-9x_/t*.D}VA1w&lt;^A?0 R&lt;f+1'</span><span>)</span>; define<span>(</span><span>'LOGGED_IN_SALT'</span>, <span>' y9:oh)]nA$}%9N-xk1MAQN1bH 8z{UD/e~K|G5{(9y|,n2E*,KwYPIf~HwhHT J'</span><span>)</span>; define<span>(</span><span>'NONCE_SALT'</span>, <span>'|B?p#Q4|.=[VL8)2AX;zy-R2;x#dqIo=!C3,;OACT%-uaQ7Li5KSVSSnLahwlZ+o'</span><span>)</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><span>&lt;</span>?php <span>/**</span> <span>*</span> The base configuration <span>for</span> WordPress <span>*</span> <span>*</span> The wp-config.php creation script uses this <span>file</span> during the <span>*</span> installation. You don<span>'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>', '</span><span>[</span>database name<span>');   /** MySQL database username */ define('</span>DB_USER<span>', '</span><span>[</span>database user<span>]</span><span>');   /** MySQL database password */ define('</span>DB_PASSWORD<span>', '</span><span>[</span>database password<span>]</span><span>');   /** MySQL hostname */ //define('</span>DB_HOST<span>', '</span>10.10.10.1<span>'); define('</span>DB_HOST<span>', '</span>172.17.0.1<span>');   /** Database Charset to use in creating database tables. */ define('</span>DB_CHARSET<span>', '</span>utf8mb4<span>'); //define('</span>DB_CHARSET<span>', '</span>utf8<span>');   /** The Database Collate type. Don'</span>t change this <span>if</span> <span>in</span> doubt. <span>*/</span> define<span>(</span><span>'DB_COLLATE'</span>, <span>''</span><span>)</span>;   <span>/**</span><span>#@+</span> <span>*</span> Authentication Unique Keys and Salts. <span>*</span> <span>*</span> Change these to different unique phrases<span>!</span> <span>*</span> You can generate these using the <span>{</span><span>@</span><span>link</span> https:<span>//</span>api.wordpress.org<span>/</span>secret-key<span>/</span><span>1.1</span><span>/</span>salt<span>/</span> WordPress.org secret-key service<span>}</span> <span>*</span> You can change these at any point <span>in</span> <span>time</span> to invalidate all existing cookies. This will force all <span>users</span> to have to log <span>in</span> again. <span>*</span> <span>*</span> <span>@</span>since 2.6.0 <span>*/</span> <span>[</span>wordpress keys and salts<span>]</span>   <span>/**</span> <span>*</span> WordPress Database Table prefix. <span>*</span> <span>*</span> You can have multiple installations <span>in</span> one database <span>if</span> you give each <span>*</span> a unique prefix. Only numbers, letters, and underscores please<span>!</span> <span>*/</span> <span>$table_prefix</span> = <span>'wp_'</span>;   <span>/**</span> <span>*</span> For developers: WordPress debugging mode. <span>*</span> <span>*</span> Change this to <span>true</span> to <span>enable</span> the display of notices during development. <span>*</span> It is strongly recommended that plugin and theme developers use WP_DEBUG <span>*</span> <span>in</span> their development environments. <span>*</span> <span>*</span> For information on other constants that can be used <span>for</span> debugging, <span>*</span> visit the Codex. <span>*</span> <span>*</span> <span>@</span><span>link</span> https:<span>//</span>codex.wordpress.org<span>/</span>Debugging_in_WordPress <span>*/</span> define<span>(</span><span>'WP_DEBUG'</span>, <span>false</span><span>)</span>; <span>//</span>define<span>(</span><span>'WP_DEBUG'</span>, <span>true</span><span>)</span>; define<span>(</span><span>'CONCATENATE_SCRIPTS'</span>, <span>false</span><span>)</span>; define<span>(</span><span>'SCRIPT_DEBUG'</span>, <span>true</span><span>)</span>; define<span>(</span><span>'WP_DEBUG'</span>, <span>true</span><span>)</span>; define<span>(</span><span>'WP_DISABLE_FATAL_ERROR_HANDLER'</span>, <span>true</span> <span>)</span>; <span>//</span> <span>5.2</span> and later   <span>/*</span> We are behind a reverse proxy <span>*/</span> <span>if</span> <span>(</span>isset<span>(</span><span>$_SERVER</span><span>[</span><span>'HTTP_X_FORWARDED_FOR'</span><span>]</span><span>)</span><span>)</span> <span>{</span> <span>$forwarded_address</span> = explode<span>(</span><span>','</span>, <span>$_SERVER</span><span>[</span><span>'HTTP_X_FORWARDED_FOR'</span><span>]</span><span>)</span>; <span>$_SERVER</span><span>[</span><span>'REMOTE_ADDR'</span><span>]</span> = <span>$forwarded_address</span><span>[</span><span>0</span><span>]</span>; <span>}</span> <span>if</span> <span>(</span>isset<span>(</span><span>$_SERVER</span><span>[</span><span>'HTTP_X_FORWARDED_PROTO'</span><span>]</span><span>)</span> <span>&amp;&amp;</span> <span>$_SERVER</span><span>[</span><span>'HTTP_X_FORWARDED_PROTO'</span><span>]</span> == <span>'https'</span><span>)</span> <span>{</span> <span>$_SERVER</span><span>[</span><span>'HTTPS'</span><span>]</span> = <span>'on'</span>; <span>}</span>   <span>/*</span> Disable the default <span>'ad hoc'</span> cron mechanism. We<span>'ll use actual cron instead. */ define('</span>DISABLE_WP_CRON<span>', true);   /* That'</span>s all, stop editing<span>!</span> Happy blogging. <span>*/</span>   <span>/**</span> Absolute path to the WordPress directory. <span>*/</span> <span>if</span> <span>(</span> <span>!</span>defined<span>(</span><span>'ABSPATH'</span><span>)</span> <span>)</span> define<span>(</span><span>'ABSPATH'</span>, <span>dirname</span><span>(</span>__FILE__<span>)</span> . <span>'/'</span><span>)</span>;   <span>/*</span> Multisite <span>*/</span> <span>//</span> see https:<span>//</span>wordpress.org<span>/</span>support<span>/</span>article<span>/</span>create-a-network<span>/</span> define<span>(</span><span>'WP_ALLOW_MULTISITE'</span>, <span>true</span><span>)</span>; <span>/*</span>define<span>(</span><span>'MULTISITE'</span>, <span>true</span><span>)</span>; define<span>(</span><span>'SUBDOMAIN_INSTALL'</span>, <span>false</span><span>)</span>; define<span>(</span><span>'DOMAIN_CURRENT_SITE'</span>, <span>'[domain name]'</span><span>)</span>; define<span>(</span><span>'PATH_CURRENT_SITE'</span>, <span>'/'</span><span>)</span>; define<span>(</span><span>'SITE_ID_CURRENT_SITE'</span>, <span>1</span><span>)</span>; define<span>(</span><span>'BLOG_ID_CURRENT_SITE'</span>, <span>1</span><span>)</span>;<span>*/</span>   <span>/*</span> disable trash, immediately permanently delete <span>*/</span> define<span>(</span><span>'EMPTY_TRASH_DAYS'</span>, <span>0</span><span>)</span>; <span>/*</span> <span>set</span> the default theme <span>for</span> the network <span>*/</span> define<span>(</span><span>'WP_DEFAULT_THEME'</span>, <span>'oeru_course'</span><span>)</span>;   <span>/**</span> Caching-related configuration <span>*/</span> <span>/**</span> Redis <span>*/</span> define<span>(</span><span>'WP_REDIS_HOST'</span>, <span>'redis'</span><span>)</span>; define<span>(</span><span>'WP_REDIS_PASSWORD'</span>, <span>'[redis password]'</span><span>)</span>; define<span>(</span><span>'WP_REDIS_PATH'</span>, <span>'/tmp/cache'</span><span>)</span>;   <span>/*</span> WEnotes plugin configuration, commented out by default <span>*/</span> <span>/*</span> this is optional<span>(</span><span>!!</span><span>)</span> - only use <span>if</span> you<span>'re deploying the OERu WEnotes stack - contact us if you want help! */ /*define('</span>WENOTES_HOST<span>', '</span><span>[</span>couchdb host<span>]</span><span>'); define('</span>WENOTES_PORT<span>', '</span><span>80</span><span>'); define('</span>WENOTES_DB<span>', '</span><span>[</span>couchdb mention database<span>]</span><span>'); define('</span>WENOTES_USER<span>', '</span><span>[</span>couchdb user<span>]</span><span>'); define('</span>WENOTES_PASS<span>', '</span><span>[</span>couchdb password<span>]</span><span>');*/   // go to /wp-admin/maint/repair.php to see if a repair is needed... //define( '</span>WP_ALLOW_REPAIR<span>', true );     /** Sets up WordPress vars and included files. */ require_once(ABSPATH . '</span>wp-settings.php<span>');</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>version: <span>"3"</span>   services: redis: image: redis:alpine command: redis-server <span>--requirepass</span> <span>[</span>redis password<span>]</span> networks: default: aliases: - redis.<span>[</span>domain name<span>]</span> php: image: oeru<span>/</span>php74-fpm-wpms links: - redis volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>src:<span>/</span>var<span>/</span>www<span>/</span>html environment: - <span>SMTP_HOST</span>=<span>[</span>smtp host<span>]</span> - <span>SMTP_PORT</span>=<span>587</span> - <span>SMTP_REPLYTO_EMAIL</span>=<span>[</span>smtp reply-to email address<span>]</span> - <span>SMTP_AUTH_USER</span>=<span>[</span>smtp user<span>]</span> - <span>SMTP_AUTH_PASSWORD</span>=<span>[</span>smtp password<span>]</span> restart: unless-stopped networks: default: aliases: - <span>[</span>domain name<span>]</span> nginx: image: oeru<span>/</span>nginx-buster-wp links: - php - redis ports: - <span>"127.0.0.1:[port]:80"</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>nginx<span>/</span>conf.d:<span>/</span>etc<span>/</span>nginx<span>/</span>conf.d - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>nginx<span>/</span>cache:<span>/</span>var<span>/</span>cache<span>/</span>nginx - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>src:<span>/</span>var<span>/</span>www<span>/</span>html restart: unless-stopped networks: default: aliases: - nginx.<span>[</span>domain name<span>]</span> cron: image: oeru<span>/</span>php74-fpm-wpms-cron links: - php - nginx volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>src:<span>/</span>var<span>/</span>www<span>/</span>html environment: - <span>SMTP_HOST</span>=<span>[</span>smtp host<span>]</span> - <span>SMTP_PORT</span>=<span>587</span> - <span>SMTP_REPLYTO_EMAIL</span>=<span>[</span>smtp reply-to email address<span>]</span> - <span>SMTP_AUTH_USER</span>=<span>[</span>smtp user<span>]</span> - <span>SMTP_AUTH_PASSWORD</span>=<span>[</span>smtp password<span>]</span> restart: unless-stopped networks: default: aliases: - cron.<span>[</span>domain name<span>]</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><span># Caching configuration</span> <span># https://easyengine.io/wordpress-nginx/tutorials/multisite/subdirectories/fastcgi-cache-with-purging/</span> <span>#</span> fastcgi_cache_path <span>/</span>var<span>/</span>cache<span>/</span>nginx <span>levels</span>=<span>1</span>:<span>2</span> <span>keys_zone</span>=WORDPRESS:500m <span>inactive</span>=60m; fastcgi_cache_key <span>"<span>$scheme</span><span>$request_method</span><span>$host</span><span>$request_uri</span>"</span>; fastcgi_cache_use_stale error timeout invalid_header http_500;   <span># from https://www.nginx.com/resources/wiki/start/topics/recipes/wordpress/</span> <span>#</span> <span># using subdir approach, not subdomain...</span> map <span>$uri</span> <span>$blogname</span><span>{</span> ~^<span>(</span>?P<span>&lt;</span>blogpath<span>&gt;/</span><span>[</span>^<span>/</span><span>]</span>+<span>/</span><span>)</span>files<span>/</span><span>(</span>.<span>*</span><span>)</span> <span>$blogpath</span> ; <span>}</span>   map <span>$blogname</span> <span>$blogid</span><span>{</span> default <span>-999</span>; <span>#Ref: http://wordpress.org/extend/plugins/nginx-helper/</span> include <span>/</span>var<span>/</span>www<span>/</span>html<span>/</span>wp-content<span>/</span>uploads<span>/</span>nginx-helper<span>/</span>map.conf; <span>}</span>   <span># statements for each of your virtual hosts to this file</span> server <span>{</span> listen <span>80</span>; root <span>/</span>var<span>/</span>www<span>/</span>html; index index.php index.html index.htm;   <span>#</span> <span># Caching configuration</span> <span>#</span> <span>#fastcgi_cache start</span> <span>set</span> <span>$skip_cache</span> <span>1</span>;   <span># POST requests and urls with a query string should always go to PHP</span> <span>if</span> <span>(</span><span>$request_method</span> = POST<span>)</span> <span>{</span> <span>set</span> <span>$skip_cache</span> <span>1</span>; <span>}</span> <span>if</span> <span>(</span><span>$query_string</span> <span>!</span>= <span>""</span><span>)</span> <span>{</span> <span>set</span> <span>$skip_cache</span> <span>1</span>; <span>}</span>   <span># Don't cache uris containing the following segments</span> <span>if</span> <span>(</span><span>$request_uri</span> ~<span>*</span> <span>"(/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>)</span> <span>{</span> <span>set</span> <span>$skip_cache</span> <span>1</span>; <span>}</span> <span># Don't use the cache for logged in users or recent commenters</span> <span>if</span> <span>(</span><span>$http_cookie</span> ~<span>*</span> <span>"comment_author|wordpress_[a-f0-9]+|wp-postpass|wordpress_no_cache|wordpress_logged_in"</span><span>)</span> <span>{</span> <span>set</span> <span>$skip_cache</span> <span>1</span>; <span>}</span> <span># end main Caching functionality</span>   <span>#</span> <span># Flow of serving pages</span> <span>#</span> <span># from https://www.nginx.com/resources/wiki/start/topics/recipes/wordpress/</span>   <span>#avoid php readfile()</span> location ^~ <span>/</span>blogs.dir <span>{</span> internal; <span>alias</span> <span>/</span>var<span>/</span>www<span>/</span>html<span>/</span>wp-content<span>/</span>blogs.dir; access_log off; log_not_found off; expires max; <span>}</span>   <span>if</span> <span>(</span><span>!</span>-e <span>$request_filename</span><span>)</span> <span>{</span> rewrite <span>/</span>wp-admin$ <span>$scheme</span>:<span>//</span><span>$host</span><span>$uri</span><span>/</span> permanent; rewrite ^<span>(</span><span>/</span><span>[</span>^<span>/</span><span>]</span>+<span>)</span>?<span>(</span><span>/</span>wp-.<span>*</span><span>)</span> <span>$2</span> <span>last</span>; rewrite ^<span>(</span><span>/</span><span>[</span>^<span>/</span><span>]</span>+<span>)</span>?<span>(</span><span>/</span>.<span>*</span>\.php<span>)</span> <span>$2</span> <span>last</span>; <span>}</span>   <span># from https://www.digitalocean.com/community/tutorials/how-to-install-wordpress-with-nginx-on-ubuntu-14-04</span> <span># with other bits from https://premium.wpmudev.org/blog/wordpress-multisite-wordpress-nginx/</span> location <span>/</span> <span>{</span> try_files <span>$uri</span> <span>$uri</span><span>/</span> <span>/</span>index.php?<span>$args</span>; <span>}</span> error_page <span>404</span> <span>/</span><span>404</span>.html;   <span># Directives to send expires headers and turn off 404 error logging.</span> location ~<span>*</span> ^.+\.<span>(</span>xml<span>|</span>ogg<span>|</span>ogv<span>|</span>svg<span>|</span>svgz<span>|</span>eot<span>|</span>otf<span>|</span>woff<span>|</span>mp4<span>|</span>ttf<span>|</span>css<span>|</span>rss<span>|</span>atom<span>|</span>js<span>|</span>jpg<span>|</span>jpeg<span>|</span>gif<span>|</span>png<span>|</span>ico<span>|</span><span>zip</span><span>|</span>tgz<span>|</span>gz<span>|</span>rar<span>|</span>bz2<span>|</span>doc<span>|</span>xls<span>|</span>exe<span>|</span>ppt<span>|</span><span>tar</span><span>|</span>mid<span>|</span>midi<span>|</span>wav<span>|</span>bmp<span>|</span>rtf<span>)</span>$ <span>{</span> access_log off; log_not_found off; expires max; <span>}</span>   location ~ \.php$ <span>{</span> try_files <span>$uri</span> =<span>404</span>; fastcgi_pass php:<span>9000</span>; fastcgi_split_path_info ^<span>(</span>.+\.php<span>)</span><span>(</span><span>/</span>.+<span>)</span>$; fastcgi_keep_conn on; fastcgi_index index.php; include fastcgi_params; fastcgi_param SCRIPT_FILENAME <span>$document_root</span><span>$fastcgi_script_name</span>; fastcgi_param PATH_INFO <span>$fastcgi_path_info</span>; fastcgi_intercept_errors on; fastcgi_buffer_size 128k; fastcgi_buffers <span>256</span> 16k; fastcgi_busy_buffers_size 256k; fastcgi_temp_file_write_size 256k; fastcgi_read_timeout <span>500</span>; <span>#</span> <span># caching functionality</span> fastcgi_cache_bypass <span>$skip_cache</span>; fastcgi_no_cache <span>$skip_cache</span>; fastcgi_cache WORDPRESS; fastcgi_cache_valid 60m; <span>}</span> <span>#</span> <span># caching functionality</span> location ~ <span>/</span>purge<span>(</span><span>/</span>.<span>*</span><span>)</span> <span>{</span> fastcgi_cache_purge WORDPRESS <span>"<span>$scheme</span><span>$request_method</span><span>$host</span>$1"</span>; <span>}</span> location = <span>/</span>favicon.ico <span>{</span> log_not_found off; access_log off; <span>}</span> location = <span>/</span>robots.txt <span>{</span> allow all; log_not_found off; access_log off; <span>}</span> location ~ ^<span>(</span><span>/</span><span>[</span>^<span>/</span><span>]</span>+<span>/</span><span>)</span>?files<span>/</span><span>(</span>.+<span>)</span> <span>{</span> try_files <span>/</span>wp-content<span>/</span>blogs.dir<span>/</span><span>$blogid</span><span>/</span>files<span>/</span><span>$2</span> <span>/</span>wp-includes<span>/</span>ms-files.php?<span>file</span>=<span>$2</span> ; access_log off; log_not_found off; expires max; <span>}</span> <span>}</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><span>/*</span> Multisite <span>*/</span> <span>//</span> see https:<span>//</span>wordpress.org<span>/</span>support<span>/</span>article<span>/</span>create-a-network<span>/</span> <span>//</span>define<span>(</span><span>'WP_ALLOW_MULTISITE'</span>, <span>true</span><span>)</span>; define<span>(</span><span>'MULTISITE'</span>, <span>true</span><span>)</span>; define<span>(</span><span>'SUBDOMAIN_INSTALL'</span>, <span>false</span><span>)</span>; define<span>(</span><span>'DOMAIN_CURRENT_SITE'</span>, <span>'[domain name]'</span><span>)</span>; define<span>(</span><span>'PATH_CURRENT_SITE'</span>, <span>'/'</span><span>)</span>; define<span>(</span><span>'SITE_ID_CURRENT_SITE'</span>, <span>1</span><span>)</span>; define<span>(</span><span>'BLOG_ID_CURRENT_SITE'</span>, <span>1</span><span>)</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>docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> advanced-responsive-video-embedder <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> check-email <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> disable-comments <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> h5p <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> hypothesis <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> redis-cache <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> safe-redirect-manager <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> unconfirmed <span>--activate-network</span> docker-compose <span>exec</span> <span>-u</span> www-data php wp plugin <span>install</span> wp-security-audit-log <span>--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><span>sudo</span> <span>git clone</span> https:<span>//</span>git.oeru.org<span>/</span>oeru<span>/</span>blog-feed-finder.git blog-feed-finder <span>sudo</span> <span>git clone</span> https:<span>//</span>git.oeru.org<span>/</span>oeru<span>/</span>oeru-h5p-tools.git oeru-h5p-tools <span>sudo</span> <span>git clone</span> https:<span>//</span>git.oeru.org<span>/</span>oeru<span>/</span>register-enrol.git register-enrol <span>sudo</span> <span>git clone</span> https:<span>//</span>git.oeru.org<span>/</span>oeru<span>/</span>wenotes.git wenotes <span>sudo</span> <span>git clone</span> https:<span>//</span>git.oeru.org<span>/</span>oeru<span>/</span>wpms-activity-register.git wpms-activity-register <span>sudo</span> <span>git clone</span> https:<span>//</span>git.oeru.org<span>/</span>oeru<span>/</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><span>cd</span> wenotes <span>sudo</span> <span>git clone</span> https:<span>//</span>git.oeru.org<span>/</span>oeru<span>/</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>define<span>(</span><span>'WENOTES_SOURCE_NAME'</span>, <span>'OERu Course Site'</span><span>)</span>; define<span>(</span><span>'WENOTES_SOURCE_URL'</span>, <span>'course.oeru.org'</span><span>)</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><span>//</span> support <span>link</span> <span>for</span> <span>users</span> of this plugin... define<span>(</span><span>'ORE_SUPPORT_FORUM'</span>, <span>'https://forums.oeru.org/t/register-enrol'</span><span>)</span>; define<span>(</span><span>'ORE_SUPPORT_BLOG'</span>, <span>'https://course.oeru.org/support/studying-courses/register-enrol/'</span><span>)</span>; define<span>(</span><span>'ORE_SUPPORT_CONTACT'</span>, <span>'https://oeru.org/contact-us/'</span><span>)</span>; define<span>(</span><span>'ORE_SUPPORT_PASSWORD_MANAGER'</span>, <span>'https://course.oeru.org/lida102/learning-pathways/digital-environments/online-hygiene/#Password_managers'</span><span>)</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 Protecting your users with Let's Encrypt SSL Certs http://tech.oeru.org/protecting-your-users-lets-encrypt-ssl-certs <span class="field field--name-title field--type-string field--label-hidden">Protecting your users with Let&#039;s Encrypt SSL Certs</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--lets-encrypt"> <span class="field__item-wrapper"><a href="/taxonomy/term/17" hreflang="en">let&#039;s encrypt</a></span> </div> <div class="field__item field__item--install"> <span class="field__item-wrapper"><a href="/taxonomy/term/11" hreflang="en">install</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--_404"> <span class="field__item-wrapper"><a href="/taxonomy/term/13" hreflang="en">14.04</a></span> </div> <div class="field__item field__item--_604"> <span class="field__item-wrapper"><a href="/taxonomy/term/27" hreflang="en">16.04</a></span> </div> <div class="field__item field__item--_804"> <span class="field__item-wrapper"><a href="/taxonomy/term/68" hreflang="en">18.04</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> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><a title="View user profile." href="/user/1" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Thu 08/07/2021 - 14:23</span> <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><a class="visually-hidden focusable skip-link" href="https://tech.oeru.org/node/add/blog_post#main-content">Sk</a>For any website that requires anyone (users or even just a few admins) to transfer secrets to and from it, you want to ensure the data is end-to-end encrypted. Today various browsers (like Firefox) give warnings when you're sending secret data (like passwords) "in the clear", namely unencrypted. In early 2017, Google <a href="http://searchengineland.com/google-starts-giving-ranking-boost-secure-httpsssl-sites-199446">added further urgency</a> to doing the right thing for your users. </p> <p>In the past, getting an SSL certificate to achieve encryption for your domain (the little "lock" icon in browser address bar indicating that your communication with the site is encrypted), was a complicated, expensive proposition, requiring a lot of annoying and time consuming "identity verification" (sometimes via post in the dark old days) and a fee of, in some cases, a couple hundred dollars per year paid to your "SSL Cert Provider" to pay for those administrative services along with the software needed to gin up a long prime number to act as your encryption key (the long string of characters making up your SSL certificate).</p> <p>Thankfully, thanks to the efforts of the <a href="https://letsencrypt.org" title="Let's Encrypt - democratising SSL and making it ubquitous.">Let's Encrypt</a> community, the process is both far far easier, and free of cost. Now there really isn't an excuse for not having an SSL certificate on your site.</p> <p>Members of the Let's Encrypt community have provided a range of useful open source tools you can use to create and maintain certificates on your hosting infrastructure (e.g. the Virtual Machine (VM) on which you're installing web services detailed in the howtos on this site!). In this case we're going to use a tool, "<a href="https://certbot.eff.org/">certbot</a>" provided by the good folks at the <a href="https://eff.org">Electronic Frontier Foundation</a>.</p> <p>For VMs running Ubuntu 18.04 or more recent Long Term Support (LTS) versions of the Ubuntu Linux platform, you should be able to just run:</p> <p><code>sudo apt-get install letsencrypt</code></p> <p>For VMs running Ubuntu 14.04 or 16.04 which are what we use, the install is easy - at your VM command line, run:</p> <pre> <code>sudo add-apt-repository ppa:certbot/certbot sudo apt-get update sudo apt-get install certbot </code></pre> <p>We run Nginx on our VMs, which makes one or more hosted services (normally running in Docker containers) available on the Internet. Strictly speaking, we don't use full "end-to-end" encryption - in our case, on the server-end the encryption terminates at the Nginx server. We, perhaps cavalierly, assume that transfer between the host machine and a Docker container running on that host will be implicitly secure... The only way it could be compromised is if the VM itself is compromised, in which case, the Docker containers running on it could be, too. Avoiding having secure transfer between Nginx on the VM host and the various Docker containers also substantially simplifies setting up each application.</p> <p>Thanks to a service which Nginx provides SNI (or <a href="https://en.wikipedia.org/wiki/Server_Name_Indication">Server Name Indication</a>) - Apache and a few other web servers also provide this - which removes the historical limitation that meant you could only have one SSL certificate per IP address on a web server. The only downside of SNI is that some older browsers (and platforms) don't support it. Since those older technologies are rapidly dying out and it's quite expensive and difficult to have a single IP address for each SSL service on a server, we accept this compromise.</p> <h2>The Let's Encrypt Cert Process</h2> <p>Here's (roughly) how the process works:</p> <ol><li>Point your domain (via A or CNAME record) to point to the/an external IP address on your VM.</li> <li>Set up a domain (or domains) for non-secure hosting (on port 80) via your Nginx instance.</li> <li>The domain's configuration must include a special directory reserved for Let's Encrypt verification content.</li> <li>You request that certbot (on the VM) acquires a certificate for that domain (or domains) at which point <ol><li>the certbot writes a file with a hard-to-guess name to that special directory and requests that the Let's Encrypt infrastructure checks the domain name from outside</li> <li>Let's Encrypt checks that domain name and special directory to see that the expected number appears there, thus verifying that the requesting party actually has the ability to set content at this hard-to-guess filename, and therefore has legitimate claim to being the party controlling the domain name and server.</li> <li>Let's Encrypt registers the certificate request in the name of the party running the certbot (so it can, for example, send emails to the administrator warning them that the certificate needs to be renewed - which happens every 8 weeks or so), and</li> <li>Let's Encrypt sends verification back to your VM's certbot telling it to complete the certificate generation, which it then (digitally) signs in the name of the Let's Encrypt Certificate Authority (which, in turn, is recognised by almost all web browsers out-of-the-box - no mean feat, I can tell you).</li> </ol></li> <li>You get an alert telling you that you have created a valid SSL certificate.</li> <li>You alter your Nginx domain configuration to <ol><li>redirect connections to port 80 (un-encrypted) to port 443 (encrypted), and</li> <li>you set up the 443 configuration including your new certificates.</li> </ol></li> <li>You reload your Nginx configuration, and your site will now be end-to-end encrypted.</li> </ol><h2>The Let's Encrypt Snippet</h2> <p>To make it easy to include the relevant directory info, I recommend that you create a new file in your Nginx configuration (substitute your preferred text editor for "vim" in the following - "nano" is a good choice if you haven't already got a preference):</p> <p><code>sudo mkdir /etc/nginx/includes<br /> sudo vim /etc/nginx/includes/letsencrypt.conf</code></p> <p>and make sure it has the following content (note, I learned this thanks to <a href="https://community.letsencrypt.org/t/how-to-nginx-configuration-to-enable-acme-challenge-support-on-all-http-virtual-hosts/5622">someone else's howto</a> on the global Internet knowledge commons :))</p> <p><code># Rule for legitimate ACME Challenge requests<br /> location ^~ /.well-known/acme-challenge/ {<br />     default_type "text/plain";<br />     # this can be any directory, but this name keeps it clear<br />     root /var/www/letsencrypt;<br /> }</code></p> <p><code># Hide /acme-challenge subdirectory and return 404 on all requests.<br /> # It is somewhat more secure than letting Nginx return 403.<br /> # Ending slash is important!<br /> location = /.well-known/acme-challenge/ {<br />     return 404;<br /> }</code></p> <p>Next, make sure your directory exists (note - you only need to do this once per VM) - it shouldn't need an special permissions - it'll be written by the "root" user, and needs to be readable by the Nginx user, usually "www-data" on a Debian or Ubuntu Linux instance.</p> <p><code>mkdir /var/www/letsencrypt</code></p> <h2>Example Nginx Domain Configuration - unencrypted</h2> <p>Here's an example of a pre-cert Nginx domain configuration for example.org and <a href="http://www.example.org">www.example.org</a> (I usually name the configuration file after the main domain it concerns, so my file would be /etc/nginx/sites-available/example.org) - this should also let you do initial test of your app to make sure it works, before adding the additional complexity of SSL. (<em>Replace example.com (and <a href="http://www.example.com">www.example.com</a>) with your own domain!</em>):</p> <p><code>server {</code></p> <p><code>    listen 80; # this is one of our external IPs on the server.<br />     #listen   [::]:80 default ipv6only=on; ## listen for ipv6<br /><br />     # this root directory isn't really relevant in a proxy situation</code><br /><code>    # so I usually set it to the system default<br />     root /usr/share/nginx/www;<br />     index index.html index.htm;<br /><br />     server_name example.org www.example.org;<br /><br />     access_log /var/log/nginx/example.org_access.log;<br />     error_log /var/log/nginx/example.org_error.log;</code></p> <p><code>    # this is where we include the snippet<br />     include /etc/nginx/includes/letsencrypt.conf;</code></p> <p><code>    # this is just an example of a "proxy" configuration<br />     # for, say, a Docker-based service, exposed on the VM's<br />     # local port 8081<br />     location / {<br />         proxy_read_timeout      300;<br />         proxy_connect_timeout   300;<br />         proxy_redirect          off;<br />         proxy_set_header    Host                $http_host;<br />         proxy_set_header    X-Real-IP           $remote_addr;<br />         proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;<br />         proxy_set_header    X-Forwarded-Proto   $scheme;<br />         proxy_pass      http://127.0.0.1:8081;<br />     }</code><br /><code>}</code></p> <p>You can make sure that the configuration is visible to Nginx by adding it into the "sites-enabled" directory via a file link</p> <p><code>sudo ln -sf /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled </code></p> <p>Test things to make sure the new configuration doesn't have typos or bad references:</p> <p><code>sudo nginx -t</code></p> <p>and if not, make it live:</p> <p><code>sudo service nginx reload</code></p> <p>You should now be able to go to <a href="http://example.com">http://example.com</a> (or your domain) and you'll hopefully get your proxied application (if it's set up) or an Nginx error (see you nginx error file for more info!).</p> <p>Now it's time to request the certificate!</p> <h2>Example Certbot invocation</h2> <p>Once cerbot is installed, and a domain is configured, it's pretty straightforward to get a certificate.</p> <p>On the first invocation of certbot, you might get a coloured interface that requests your user details (e.g. name and email address) so that Let's Encrypt can register them for the purposes of future emails. They email if one of your certificates is on the verge of expiring, or if there's been a change to Let's Encrypt policy or process. It's worth being on the list. </p> <p>You can request your certificate with the following:</p> <p><code>sudo certbot certonly --webroot -w /var/www/letsencrypt -d example.org -d www.example.org</code></p> <p>If it works, it gratifyingly results in a message that starts with "Congratulations"!</p> <h2>Example Nginx Domain Configuration - unencrypted</h2> <p>Once you've got your certificate, you can reference it in your configuration. We normally set up a redirect from the unencrypted version of the site to the encrypted on (except for the Let's Encrypt verification directory!):</p> <p><code>server {<br />     listen 80; # listen on port 80 on all IPv4 addresses</code><br /><code>    listen [::]:80; # listen on port 80 on all IPv6 addresses</code><br /><code> <br />     root /var/www/html;<br />     index index.html index.htm;<br /><br />     server_name example.org www.example.org;<br /><br />     access_log /var/log/nginx/example.org_access.log;<br />     error_log /var/log/nginx/example.org_error.log;</code></p> <p><code>    include /etc/nginx/includes/letsencrypt.conf;</code></p> <p><code>    # a 302 is a "soft" redirect. A 301 can never be reversed.<br />     location / {<br />         return 302 https://example.org$request_uri;<br />     }       <br /> }<br /><br /> server {</code><br /><span style="font-family:monospace"><span style="color:#000000;background-color:#ffffff;">    listen 443 ssl; # listen on port 443 on all IPv4 addresses</span></span><br /><span style="font-family:monospace"><span style="color:#000000;background-color:#ffffff;">    listen [::]:443 ssl; # listen on port 443 on all IPv6 addresses</span></span><br /><br /><code>    # this is a deprecated rule, not required in Ubuntu 20.04's nginx<br />     # ssl on; <br />     ssl_certificate /etc/letsencrypt/live/example.org/fullchain.pem;<br />     ssl_certificate_key /etc/letsencrypt/live/example.org/privkey.pem;</code><br /><code>    ssl_protocols TLSv1 TLSv1.1 TLSv1.2;<br />     ssl_dhparam /etc/ssl/certs/dhparam.pem;<br />     keepalive_timeout 20s;</code></p> <p><code>    access_log /var/log/nginx/example.org_access.log;<br />     error_log /var/log/nginx/example.org_error.log;</code></p> <p><code>    root /var/www/html;<br />     index index.html index.htm;</code><br /><br /><code>    server_name example.org www.example.org;<br />    <br />     # this is just an example of a "proxy" configuration<br />     # for, say, a Docker-based service, exposed on the VM's<br />     # local port 8081<br />     location / {<br />         proxy_read_timeout      300;<br />         proxy_connect_timeout   300;<br />         proxy_redirect          off;<br />         proxy_set_header    Host                $http_host;<br />         proxy_set_header    X-Real-IP           $remote_addr;<br />         proxy_set_header    X-Forwarded-For     $proxy_add_x_forwarded_for;<br />         proxy_set_header    X-Forwarded-Proto   $scheme;<br />         proxy_pass      http://127.0.0.1:8081;<br />     }</code><br /><code>}</code></p> <p>Note - you also need to set up the <code>ssl_dhparam</code> file for this configuration to work. You can do this based on <a href="https://michael.lustfield.net/nginx/getting-a-perfect-ssl-labs-score" title="Setting hp ssl_dhparam">these instructions</a> after installing OpenSSL tools:</p> <p><code>sudo apt-get install openssl</code></p> <p>by running (warning - this can take quite a long time, 5-15 minutes in my experience - the system needs to generate sufficient <a href="https://en.wikipedia.org/wiki/Entropy">entropy</a> to achieve acceptable randomness):</p> <pre class="literal-block"> <code>openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096</code> </pre> <p>When you've set up the file, you can enable it:</p> <p><code>sudo ln -sf /etc/nginx/sites-available/example.com /etc/nginx/sites-enabled</code></p> <p>Test the file to ensure there aren't any syntax errors before reloading nginx:</p> <p><code>sudo nginx -t</code></p> <p>If this shows an error, you'll need to fix the file. If all's well, reload nginx to enable the new configuration:</p> <p><code>sudo service nginx reload</code></p> <p>You should now be able to point your browser at your domain name, and it should automatically redirect you to <a href="https://example.org">https://example.org</a> - and (based on the above configuration, <a href="https://www.example.org">https://www.example.org</a> should work too. You might want to redirect <a href="http://www.example.org">www.example.org</a> to example.org or visa versa).</p> <p>A word to the wise - if it doesn't work, check your firewall settings!</p> <p><strong>Update:</strong> discovered this <a href="https://www.digitalocean.com/community/tutorials/how-to-secure-nginx-with-let-s-encrypt-on-ubuntu-14-04">very well done how-to</a> on Let's Encrypt that offers additional background to this one.</p> <h2>Ongoing Certificate Maintenance</h2> <p>One of the nice things about EFF's certbot is that when it's installed, it also installs a nightly cron task (see <code>/etc/cron.d/certbot</code>) which checks all domains registered on the server for renewals. Assuming your domains have been configured in Nginx as described above, renewals should occur automatically, and you'll just receive a periodic email to let you know that they've happened.</p> <p>If you want to check your renewal status, you can run this:</p> <pre> <code>sudo certbot renew --dry-run</code></pre> <p>Good on you for securing your users and your site!</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=11&amp;2=field_blog_comments&amp;3=comment" token="u_cC5Zw8SKo5_CwG7txdoRLRmx6G2xIvWYWTc95IdY0"></drupal-render-placeholder> </div> </section> Thu, 08 Jul 2021 02:23:06 +0000 dave 11 at http://tech.oeru.org Installing NextCloud and Collabora Office Online with Docker on Ubuntu 16.04 http://tech.oeru.org/installing-nextcloud-and-collabora-office-online-docker-ubuntu-1604 <span class="field field--name-title field--type-string field--label-hidden">Installing NextCloud and Collabora Office Online with Docker on Ubuntu 16.04</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--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--mariadb"> <span class="field__item-wrapper"><a href="/taxonomy/term/48" hreflang="en">mariadb</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--php"> <span class="field__item-wrapper"><a href="/taxonomy/term/40" hreflang="en">php</a></span> </div> <div class="field__item field__item--collabora-office"> <span class="field__item-wrapper"><a href="/taxonomy/term/50" hreflang="en">collabora office</a></span> </div> <div class="field__item field__item--nextcloud"> <span class="field__item-wrapper"><a href="/taxonomy/term/51" hreflang="en">nextcloud</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 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--productivity"> <span class="field__item-wrapper"><a href="/taxonomy/term/52" hreflang="en">productivity</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> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><a title="View user profile." href="/user/1" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Mon 29/01/2018 - 17:29</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/2018-01/Files%20-%20OERu%20NextCloud.png?itok=xQHlcyml" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The NextCloud web interface for browsing your files&quot;}" role="button" title="The NextCloud web interface for browsing your files" data-colorbox-gallery="gallery-field_image-9s1vCmuTUlQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The NextCloud web interface for browsing your files&quot;}"><img src="/sites/default/files/styles/medium/public/2018-01/Files%20-%20OERu%20NextCloud.png?itok=6v2Kuyct" width="220" height="164" alt="The NextCloud web interface for browsing your files" loading="lazy" 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/2018-01/NextCloud-AppStore.png?itok=DPeCx5Rd" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The central AppStore (note, almost all apps have no cost and are open source). You get a similar view within your own NextCloud instance.&quot;}" role="button" title="The central AppStore (note, almost all apps have no cost and are open source). You get a similar view within your own NextCloud instance." data-colorbox-gallery="gallery-field_image-9s1vCmuTUlQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The central AppStore (note, almost all apps have no cost and are open source). You get a similar view within your own NextCloud instance.&quot;}"><img src="/sites/default/files/styles/medium/public/2018-01/NextCloud-AppStore.png?itok=WqCJJdGj" width="220" height="175" alt="The central AppStore (note, almost all apps have no cost and are open source). You get a similar view within your own NextCloud instance." loading="lazy" 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/2018-01/NextCloud-Calendar.png?itok=-j0Dq2rG" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The NextCloud shared calendar plugin works with all major calendaring applications alongside your existing digital calendars.&quot;}" role="button" title="The NextCloud shared calendar plugin works with all major calendaring applications alongside your existing digital calendars." data-colorbox-gallery="gallery-field_image-9s1vCmuTUlQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The NextCloud shared calendar plugin works with all major calendaring applications alongside your existing digital calendars.&quot;}"><img src="/sites/default/files/styles/medium/public/2018-01/NextCloud-Calendar.png?itok=bP23WxDf" width="220" height="175" alt="The NextCloud shared calendar plugin works with all major calendaring applications alongside your existing digital calendars." loading="lazy" 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/2018-01/Nextcloud-CollaboraSpreadsheet.png?itok=Ovp0KryQ" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;An example of a fairly complex spreadsheet in the Collabora spreadsheet interface.&quot;}" role="button" title="An example of a fairly complex spreadsheet in the Collabora spreadsheet interface." data-colorbox-gallery="gallery-field_image-9s1vCmuTUlQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;An example of a fairly complex spreadsheet in the Collabora spreadsheet interface.&quot;}"><img src="/sites/default/files/styles/medium/public/2018-01/Nextcloud-CollaboraSpreadsheet.png?itok=CNhDR2y-" width="220" height="157" alt="An example of a fairly complex spreadsheet in the Collabora spreadsheet interface." loading="lazy" 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/2018-01/Nextcloud-CollaboraWordprocessor.png?itok=IOyfA_M4" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A fairly complex document, with variables, shown in the Collabora wordprocessor interface.&quot;}" role="button" title="A fairly complex document, with variables, shown in the Collabora wordprocessor interface." data-colorbox-gallery="gallery-field_image-9s1vCmuTUlQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A fairly complex document, with variables, shown in the Collabora wordprocessor interface.&quot;}"><img src="/sites/default/files/styles/medium/public/2018-01/Nextcloud-CollaboraWordprocessor.png?itok=HPawBI-o" width="220" height="157" alt="A fairly complex document, with variables, shown in the Collabora wordprocessor interface." loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-6"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2018-01/DavInFilemanager.png?itok=rCbwaUUY" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;This is what your NextCloud would look like in your desktop filemanager (this is the Nemo filemanager on a Linux desktop)&quot;}" role="button" title="This is what your NextCloud would look like in your desktop filemanager (this is the Nemo filemanager on a Linux desktop)" data-colorbox-gallery="gallery-field_image-9s1vCmuTUlQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;This is what your NextCloud would look like in your desktop filemanager (this is the Nemo filemanager on a Linux desktop)&quot;}"><img src="/sites/default/files/styles/medium/public/2018-01/DavInFilemanager.png?itok=g2dNm33H" width="220" height="122" alt="This is what your NextCloud would look like in your desktop filemanager (this is the Nemo filemanager on a Linux desktop)" loading="lazy" class="image-style-medium" /> </a> </div> </figure> <figure class="field-type-image__figure image-count-7"> <div class="field-type-image__item"> <a href="http://tech.oeru.org/sites/default/files/styles/max_1300x1300/public/2018-02/CollaboraAdminConsole.png?itok=1tNI9ZdJ" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Collabora Office admin console&quot;}" role="button" title="Collabora Office admin console" data-colorbox-gallery="gallery-field_image-9s1vCmuTUlQ" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Collabora Office admin console&quot;}"><img src="/sites/default/files/styles/medium/public/2018-02/CollaboraAdminConsole.png?itok=iijjMrBK" width="220" height="149" alt="Collabora Office admin console" loading="lazy" 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><strong>Update 2021-11-08</strong>: this post is now getting a bit long-in-the-tooth, and I need to update it to use up-to-date components. It might still be useful to folks, but use it with caution. Also, please note, we're using NextCloud with <a href="https://onlyoffice.com">OnlyOffice</a> (the open source community edition) these days. <strong>Update 2023-06-14</strong>: Here's a <a href="/node/56">new tutorial for Ubuntu 22.04</a>!</p> <p>Dropbox is the best known of the end-user "cloud storage" services for documents, backups, and synchronising data among multiple devices, although now Google's Drive and Microsoft's OneDrive are functionally similar and are being heavily promoted and tied into all sorts of services.</p> <p>Similarly the collaborative editing of documents, spreadsheets, and presentations in the browser, pioneered by Etherpad, but then adopted in a big way by Google Docs (and more recently, Microsoft Office 365), has revolutionised collective note taking, document preparation, and ease of access to these powerful tools by the mainstream of computer users. Only a browser is required, and no other software needs to be installed.</p> <p>But what about people who don't want to entrust all of their data to foreign corporations, holding their data in foreign jurisdiction, in formats that may or may not be retrievable in the event that the supplier fails or changes "strategic direction"? And many of these services involve "mining" their data to extract useful information that vendors sell to others to <em>help them advertise to us in a more targeted way. </em>Yeah, that's creepy.</p> <p>More-over, often if you want to <em>share</em> your data with others, <em>they</em> have to log into the same service, and accept the service's terms and conditions (usually substantially constraining the user's normal rights and freedoms, although who<em> actually</em> reads those, eh?!) in order to do so... so ones use of those services has a magnifying effect on the loss of privacy and control.</p> <p>Some people sensibly prefer to manage their own, or institution-specific, solutions on the infrastructure of their choosing, in a way that doesn't tie anyone into paying ever increasing amounts for data storage as the volumes increase perpetually, month on month.</p> <p>Some of us simply prefer to have control of our own destiny, without a dependence on, for example, file or data storage formats and practices that are completely opaque to them. Our data reflects our creativity energy, and it seems much more comfortable for many of us to be in charge of our own fates rather than entrusting it to a third party who simply sees us a profit centre.</p> <p>Thankfully, the open source world has created an array of possible equivalent systems, and this post describes how you, too, can set up your own equivalent to Dropbox + Google Docs using entirely open source software on any commodity virtual machine hosting system you want to use by adopting NextCloud and Collabora Office.</p> <h2>NextCloud</h2> <p><a href="https://nextcloud.com">NextCloud</a> is <a href="https://nextcloud.com/files/">functionally similar</a> to Dropbox, however, with its active development community and plug-in architecture, it can provide quite a lot more as well, like shared calendaring, email, video conferencing, contact syncing, image/sound/video galleries, <a href="https://nextcloud.com/files/">among many other services</a>.</p> <p>If you prefer not to organise and run your own server, you can purchase a supported server via their website for a cost similar to Dropbox (although, realise that NextCloud is relatively small by comparison and doesn't have the massive economies of scale enjoyed by the bigger players).</p> <p>For those with an interest in history: NextCloud is a fork created by the founder of OwnCloud, after he decided that the company which formed around his OwnCloud project was moving in a direction that was philosophical unpalatable for him. The beauty of open source is that developers can follow their consciences without requiring anyone's permission. The resulting "forks" in code bases and communities then thrive or die based on the strengths of the communities they can build and sustain. This fork is remarkably similar to that which occurred in the OpenOffice community which resulted in the founding of LibreOffice. LibreOffice has thrived and OpenOffice has faded into irrelevance. More on that below.</p> <p>For those with a technological interest, NextCloud is a mature PHP application (but with a modern architecture, including a command line interface, occ) which stores its data in an RDBMS like MySQL, MariaDB, PostgreSQL, or (usually for development purposes) the lightweight SQLite database. Here are <a href="https://docs.nextcloud.com/server/12/admin_manual/installation/index.html">details for would-be administrators</a>.</p> <h2>Collabora Office</h2> <p>Given how much companies like Google and Microsoft invest on Docs and Office 365 respectively, how is it possible for an open source community to create a credible competitor? Turns out it's not as hard as you might think if they leverage the power of open source.</p> <p>A small software company with headquarters in the UK (although their team appears to be from all over), Collabora Office, has taken on the ambitious mission of creating a "collaborative web interface" allowing users to collaborate using <a href="https://libreoffice.org">LibreOffice</a>, one of the most powerful and widely used office package available anywhere. We're currently at Collabora Office 3.0, and the front end is quite nice and functional, but still pretty simple - that can be a good thing for many users. Collabora is progressively re-imagining the user interface of LibreOffice as a collaborative web interface. This isn't easy, but it's <em>much</em> easier than it otherwise would be because the difficult job of creating the heavy-lifting application back-end is already done - LibreOffice is a mature widely used application (albeit with a desktop interface, not a web-based collaborative interface). So we can expect progress will be rapid, and large sets of new capabilities will be "unlocked" as they progress their efforts.</p> <h2>NextCloud and Collabora - better together!</h2> <p>The beauty of the open source software model is that we can connect NextCloud and Collabora office - completely separate and unrelated communities - thanks to a new integration standard, WOPI (Web-application Open Platform Interface) they form a well integrated component model - with the <em>major </em>added benefit of being able to swap in a better file management platform, or a better collaborative productivity package if one or the other emerges, without having to start from scratch.</p> <h2>Setting up your own NextCloud Collabora Server</h2> <p>If you're game to run your own (and, in my experience, it's a surprisingly well behaved system) here's how you do it.</p> <p>In preparation, you'll want to have the following ready:</p> <ul><li>a Linux virtual machine or "VM" (I recommend running the current Ubuntu LTS version, or current Debian) with a user with Sudo privileges...,</li> <li>your domain name for the NextCloud instance, pointing to the IP address of your VM,</li> <li>your domain name for the Collabora instance, also pointing to the IP of your VM, and</li> <li>credentials for an email address capable of sending from a remote server (usually termed an "authenticating SMTP email account")</li> </ul><h3>Secure access with SSH</h3> <p>First things first, make sure you're logged into your host (probably via SSH) as a user who has "sudo" capabilities! You need to log into the host from your local machine. We recommend setting up <a href="https://www.digitalocean.com/community/tutorials/how-to-configure-ssh-key-based-authentication-on-a-linux-server">key-based authentication</a>.</p> <h3>Firewall with UFW</h3> <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 - "<a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-a-firewall-with-ufw-on-ubuntu-16-04" title="Uncomplicated FireWall">ufw</a>" is installed on Ubuntu by default. 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<br /> 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 "vim" as my editor. If you don't know how to/want to use it, replace <strong>vim</strong> with <strong>nano</strong> everywhere you see it in the following - nano's easier to use for simple edits like this):</p> <p><code>sudo vim /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> <p><code>#DEFAULT_FORWARD_POLICY="DROP"<br /> DEFAULT_FORWARD_POLICY="ACCEPT"</code></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 vim /etc/ufw/sysctl.conf</code></p> <p><code># Uncomment this to allow this host to route packets between interfaces<br /> net/ipv4/ip_forward=1<br /> net/ipv6/conf/default/forwarding=1<br /> net/ipv6/conf/all/forwarding=1</code></p> <p>and finally restart the network stack and ufw on your server<code> </code></p> <p><code>sudo service networking restart<br /> sudo service ufw restart</code></p> <h3>Installing the Nginx webserver</h3> <p>In the configuration I'm describing here, you'll need a webserver running on the server - it'll be acting as a "proxy" for the Docker-based Nginx instance described below. I like 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 nginx to be visible via ports 80 and 443, run</p> <p><code>sudo ufw allow "Nginx Full"</code></p> <p><strong>Note</strong>: 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> <h3>Installing MariaDB</h3> <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 server and the client like this.</p> <p><code>sudo apt-get install mariadb-server-10.0 mariadb-client-10.0</code></p> <p>You need to set a root (admin) user password - you might want to create a /root/.my.cnf file containing the following (replacing YOURPASSWORD) to let you access MariaDB without a password from the commandline<code>:</code></p> <p><code>[client]<br /> user=root<br /> password=YOURPASSWORD</code></p> <p>You should now be able to type "mysql" at the command prompt</p> <p>Tweak the configuration so that it's listening on</p> <p><code>sudo vim /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 (127.0.0.1)...</p> <p><code># Instead of skip-networking the default is now to listen only on<br /> # localhost which is more compatible and is not less secure.<br /> #bind-address           = 127.0.0.1<br /> bind-address            = 0.0.0.0</code></p> <p>Then restart MariaDB:</p> <p><code>sudo service mysql restart</code></p> <p>It should now be listening on port 3306 on all interfaces, i.e. 0.0.0.0.</p> <p>Now set up the database which will hold NextCloud's data. Log into the MySQL client on the host (if you've created a .my.cnf 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. It's also a good idea to gin up a password for your "nextcloud" database user. I usually use pwgen (<code>sudo apt-get install pwgen</code>) - for example running this command will give you a single 12 character password without special characters (just numbers and letters):</p> <p><code>pwgen -s 12 1<br /> T7KR2osrMkyC</code></p> <p>At the prompt (which will look something like MariaDB [(none)]&gt;) enter the following lines (putting your password in place of [passwd]):</p> <p><code>CREATE DATABASE nextcloud CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;<br /> CREATE USER "nextcloud"@"%" IDENTIFIED BY "[passwd]";<br /> GRANT ALL ON nextcloud.* to "nextcloud"@"%";<br /> FLUSH PRIVILEGES;</code></p> <p>Then enter \q to exit.</p> <h2>NextCloud and Collabora Office with Docker</h2> <p>We make use of the NextCloud community's <a href="https://hub.docker.com/_/nextcloud/" title="Documentation for the reference NextCloud Docker container.">stable Docker container</a> which they keep up to date. Similarly, the Collabora community has created a <a href="https://hub.docker.com/collabora/code">reference Docker container</a>.</p> <p>The over all architecture consists of five Docker containers (note, done properly, you aim to ensure that each container runs only one service!):</p> <ol><li>the main NextCloud container (running the PHP-FPM service)</li> <li>an identical container to the PHP one which runs the cron service (which does periodic administrative tasks relevant to NextCloud)</li> <li>the self-contained Collabora Office container (running PHP with an Apache web server instance and a full instance of LibreOffice running in headless server mode (never fear, no servers were harmed in the building of this server!) - yes this server doesn't really adhere to the "one-service per container" convention, but I'm ok with that. It's just a convention after all.)</li> <li>a Redis container (which provides performance improving caching for NextCloud), and</li> <li>an Nginx webserver container which makes it easier to manage the configuration and paths of the NextCloud and Collabora servers via WOPI. It means that on the hosting server, we only need to run a proxying web server, which is easy.</li> </ol><p>The way I prefer to implement this set of containers is to use <a href="https://docs.docker.com/compose/">Docker Compose</a> (after first setting up <a href="https://docs.docker.com/install/linux/docker-ce/ubuntu/">Docker support</a> on your server - I'll assume you've followed the complete instructions including <a href="https://docs.docker.com/install/linux/linux-postinstall/">setting up Docker for your non-root user</a>). I suggest using the latest <a href="https://docs.docker.com/compose/install/#install-compose">installation instructions</a> provided by the Docker community. To be honest, I usually use the alternative instructions, <a href="https://docs.docker.com/compose/install/#install-using-pip">employing the "pip" approach</a>. You can upgrade an existing install by issuing (on your Linux VM's command line):</p> <p><code>sudo pip install -U docker-compose </code></p> <p>To set up your server, I recommend setting up a place for your Docker containers (replace "me" with your non-root username on the server) and the associated persistent data (your Docker containers should hold <em>no</em> important data - you should be able to delete and recreate them entirely without losing any important data or configuration):</p> <p><code>sudo mkdir /home/data</code><br /><code>sudo mkdir /home/data/nextcloud</code><br /><code>sudo mkdir /home/data/nextcloud/apps<br /> sudo mkdir /home/data/nextcloud/config<br /> sudo mkdir /home/data/nextcloud/data<br /> sudo mkdir /home/data/nextcloud/redis<br /> sudo mkdir /home/data/nextcloud/resources<br /> sudo mkdir /home/docker<br /> sudo mkdir /home/docker/nextcloud-collabora<br /> sudo chown -R me:me /home/docker<br /> cd /home/docker/nextcloud-collabora</code></p> <p>Here's an example of the required docker-compose.yml file (you can create this via a text editor like "nano" which should be pre-installed on any VM these days, or use my preferred, but less intuitive, editor, vim via <code>vim docker-compose.yml</code> in the /home/docker/nextcloud-collabora directory):</p> <p><code>version: '2'<br /> networks:<br />   back:<br />     driver: bridge<br /> services:<br />   web:<br />     image: nginx<br />     ports:<br />       - 127.0.0.1:8082:80<br />     volumes:<br />       - ./nginx.conf:/etc/nginx/nginx.conf:ro<br />     links:<br />       - app<br />     volumes_from:<br />       - app<br />     environment:<br />       - VIRTUAL_HOST<br />     networks:<br />     - back<br />     restart: unless-stopped      <br />   app:<br />     image: nextcloud:12-fpm<br />     links:<br />       - redis<br />     volumes:<br />       - /home/data/nextcloud/apps:/var/www/html/apps<br />       - /home/data/nextcloud/config:/var/www/html/config<br />       - /home/data/nextcloud/resources:/var/www/html/resources<br />       - /home/data/nextcloud/data:/var/www/html/data<br />     networks:<br />     - back<br />     restart: unless-stopped      <br />   cron:<br />     image: nextcloud:12-fpm<br />     volumes_from:<br />       - app<br />     user: www-data<br />     entrypoint: |<br />       bash -c 'bash -s &lt;&lt;EOF<br />       trap "break;exit" SIGHUP SIGINT SIGTERM<br />       while /bin/true; do<br />         /usr/local/bin/php /var/www/html/cron.php<br />         sleep 900<br />       done<br />       EOF'<br />     networks:<br />       - back<br />     restart: unless-stopped      <br />   redis:<br />     image: redis:alpine<br />     volumes:<br />       - /home/data/nextcloud/redis:/data<br />     networks:<br />       - back<br />     restart: unless-stopped<br />   collab:<br />     image: collabora/code<br />     environment:</code><br /><code>      # put the domain name you select for your NextCloud instance<br />       # here! Escape any . in your domain name by preceding them with \\<br />       domain: your\\.domain\\.tld<br />       username: admin</code><br /><code>      # put your own strong password in here!<br />       password: some-good-password<br />     cap_add:<br />       - MKNOD<br />     networks:<br />       - back<br />     volumes_from:<br />       - app<br />     ports:<br />       - 127.0.0.1:9980:9980<br />     links:<br />       - app<br />     restart: unless-stopped</code></p> <p>You'll need to substitute the domain name you pick for your NextCloud instance - Collabora's container requires that you specify it so that it doesn't accept connections from other (potentially nefarious) containers elsewhere on the Internet!</p> <p>Also note, the "ports" specified above, 8082 for <code>nginx</code> and 9980 for <code>collab</code> are arbitrary - I picked these to ensure they don't conflict with ports being used by other containers on my server - you can use these if you want, or use <code>sudo netstat -punta</code> to see what ports are currently claimed by other services on your server (if there are any) and pick ones that don't clash! If it scroll past too fast, you can pipe it into less to allow you to scroll and search: <code>sudo netstat -punta | less</code> - hit "q" to exit or "/" to initiate a text search.</p> <p>You will also need to provide the "nginx.conf" file referenced in the nginx section of the file. Do that by using your editor, e.g. <code>vim nginx.conf</code>, and enter this content:</p> <p><code>user www-data;</code></p> <p><code>events {<br />   worker_connections 768;<br /> }</code></p> <p><code>http {<br />   upstream backend {</code><br /><code>      # if you don't call your NextCloud server "app" in your<br />       # docker-compose.yml, you'll need to change app below to </code><br /><code>      # whatever you end up calling it.<br />       server app:9000;<br />   }<br />   include /etc/nginx/mime.types;<br />   default_type application/octet-stream;</code></p> <p><code>  server {<br />     listen 80;<br />     <br />     # Add headers to serve security related headers<br />     add_header X-Content-Type-Options nosniff;<br />     add_header X-Frame-Options "SAMEORIGIN";<br />     add_header X-XSS-Protection "1; mode=block";<br />     add_header X-Robots-Tag none;<br />     add_header X-Download-Options noopen;<br />     add_header X-Permitted-Cross-Domain-Policies none;</code></p> <p><code>    root /var/www/html;</code></p> <p><code>    location = /robots.txt {<br />       allow all;<br />       log_not_found off;<br />       access_log off;<br />     }</code></p> <p><code>    location = /.well-known/carddav {<br />       return 301 $scheme://$host/remote.php/dav;<br />     }<br />     location = /.well-known/caldav {<br />       return 301 $scheme://$host/remote.php/dav;<br />     }</code></p> <p><code>    client_max_body_size 1G;<br />     fastcgi_buffers 64 4K;</code></p> <p><code>    gzip off;</code></p> <p><code>    index index.php;<br />     error_page 403 /core/templates/403.php;<br />     error_page 404 /core/templates/404.php;<br />  <br />     location / {<br />         rewrite ^ /index.php$uri;<br />     }</code></p> <p><code>    location ~ ^/(?:build|tests|config|lib|3rdparty|templates|data)/ {<br />         deny all;<br />     }<br />     location ~ ^/(?:\.|autotest|occ|issue|indie|db_|console) {<br />         deny all;<br />     }</code></p> <p><code>    location ~ ^/(?:index|remote|public|cron|core/ajax/update|status|ocs/v[12]|updater/.+|ocs-provider/.+|core/templates/40[34])\.php(?:$|/) {<br />         include fastcgi_params;<br />         fastcgi_split_path_info ^(.+\.php)(/.*)$;<br />         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;<br />         fastcgi_param PATH_INFO $fastcgi_path_info;<br />         fastcgi_param HTTPS on;<br />         #Avoid sending the security headers twice<br />         fastcgi_param modHeadersAvailable true;<br />         fastcgi_param front_controller_active true;<br />         fastcgi_pass backend;<br />         fastcgi_intercept_errors on;<br />         fastcgi_request_buffering off;<br />     }</code></p> <p><code>    location ~ ^/(?:updater|ocs-provider)(?:$|/) {<br />         try_files $uri/ =404;<br />         index index.php;<br />     }</code></p> <p><code>    # Adding the cache control header for js and css files<br />     # Make sure it is BELOW the PHP block<br />     location ~* \.(?:css|js)$ {<br />         try_files $uri /index.php$uri$is_args$args;<br />         add_header Cache-Control "public, max-age=7200";<br />         # Add headers to serve security related headers (It is intended to<br />         # have those duplicated to the ones above)<br />         # Before enabling Strict-Transport-Security headers please read into<br />         # this topic first.<br />         # add_header Strict-Transport-Security "max-age=15768000;<br />         #  includeSubDomains; preload;";<br />         add_header X-Content-Type-Options nosniff;<br />         add_header X-Frame-Options "SAMEORIGIN";<br />         add_header X-XSS-Protection "1; mode=block";<br />         add_header X-Robots-Tag none;<br />         add_header X-Download-Options noopen;<br />         add_header X-Permitted-Cross-Domain-Policies none;<br />         # Optional: Don't log access to assets<br />         access_log off;<br />     }</code></p> <p><code>    location ~* \.(?:svg|gif|png|html|ttf|woff|ico|jpg|jpeg)$ {<br />         try_files $uri /index.php$uri$is_args$args;<br />         # Optional: Don't log access to other assets<br />         access_log off;<br />     }<br />   }<br /> }</code></p> <p>That should be all the configuration you need to make the Docker containers go.</p> <h2>Configuring Nginx to proxy NextCloud and Collabora</h2> <p>The next step is configuring the local nginx proxy servers for NextCloud and Collabora using the nginx instance you installed earlier. That's what responds to the domain name you choose for this service. In our case, the name is <a href="https://docs.oeru.org">https://docs.oeru.org</a> - you can have a look at it to see what you should be seeing when you first start things up! We use <a href="https://letsencrypt.org" title="This is an incredible free and open source service, that is single-handedly making the web a much safer place.">Let's Encrypt</a> to provide secure hosting - <a href="/protecting-your-users-lets-encrypt-ssl-certs">here're my Let's Encrypt instructions</a> on setting it up. The key thing to realise is that your "certificates" need to exist for Nginx to restart with the new configurations below - use the "commenting out the intervening lines" trick mentioned in my instructions to bootstrap the creation of your secure certificates!</p> <p>To configure the proxies, you need to create two configuration files in your /etc/nginx/sites-available/ directory.</p> <h3>NextCloud Proxy Configuration</h3> <p>Create a file with a meaningful name for your NextCloud Proxy, perhaps based on the domain name you've chosen (our file for docs.oeru.org is called "docs") using the same editing approach as the last few (although this is in a different directory) for example <code>sudo vim /etc/nginx/sites-available/docs</code> with the following contents, replacing "nextcloud.domain" with your selected domain name (and the port number 8082 if you've opted to change to a different one!):</p> <p><code>server {<br />     listen 80;<br />     server_name nextcloud.domain;</code></p> <p><code>    include /etc/nginx/includes/letsencrypt.conf;</code></p> <p><code>    # redirect all HTTP traffic to HTTPS.<br />     location / {<br />         return  302 https://nextcloud.domain$request_uri;<br />     }<br /> }</code></p> <p><code># This configuration assumes that there's an nginx container talking to the mautic PHP-fpm container,<br /> # and this is a reverse proxy for that Mautic instance.<br /> server {<br />     listen 443 ssl;<br />     server_name nextcloud.domain;</code></p> <p><code>    ssl_certificate /etc/letsencrypt/live/nextcloud.domain/fullchain.pem;<br />     ssl_certificate_key /etc/letsencrypt/live/nextcloud.domain/privkey.pem;<br />     ssl_protocols TLSv1 TLSv1.1 TLSv1.2;<br />     # to create this, see https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html<br />     ssl_dhparam /etc/ssl/certs/dhparam.pem;<br />     keepalive_timeout 20s;</code></p> <p><code>    include /etc/nginx/includes/letsencrypt.conf;<br />    <br />     location ^~ / {<br />         proxy_pass http://localhost:8082;<br />         proxy_set_header Upgrade $http_upgrade;<br />         proxy_set_header Connection "Upgrade";<br />         proxy_set_header Host $http_host;<br />         proxy_read_timeout 36000s;<br />     }<br />     client_max_body_size 1G;<br />     fastcgi_buffers 64 4K;</code></p> <p><code>    add_header X-Frame-Options "SAMEORIGIN";<br />     add_header Strict-Transport-Security "max-age=31536000; includeSubdomains;";<br /> }</code></p> <h3>Collab Proxy Configuration</h3> <p>Now create a collabora proxy configuration.</p> <p>Note: This will probably never by used by any user directly (there is a resource analysis service on the collabora system that might be of interest) - instead it'll be referenced by the NextCloud instance transparently to your users. </p> <p>In our case, we chose the domain collab.oeru.org and the file is called "collab", created via <code>sudo vim /etc/nginx/sites-available/collab</code> and containing (replace collab.domain with the one you've selected - similarly replace the port number 9980 with whatever you've selected if you've opted for a different one!):</p> <p><code>server {<br />     listen 80;<br />     server_name collab.domain;</code></p> <p><code>    # for let's encrypt renewals!<br />     include /etc/nginx/includes/letsencrypt.conf;</code></p> <p><code>    # redirect all HTTP traffic to HTTPS.<br />     location / {<br />         return  302 https://collab.domain$request_uri;<br />     }<br /> }</code></p> <p><code>server {<br />     listen 443 ssl;<br />     server_name collab.domain;</code></p> <p><code>    ssl_certificate /etc/letsencrypt/live/collab.domain/fullchain.pem;<br />     ssl_certificate_key /etc/letsencrypt/live/collab.domain/privkey.pem;<br />     ssl_protocols TLSv1 TLSv1.1 TLSv1.2;<br />     # to create this, see https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html<br />     ssl_dhparam /etc/ssl/certs/dhparam.pem;<br />     keepalive_timeout 20s;</code></p> <p><code>    # for let's encrypt renewals!<br />     include /etc/nginx/includes/letsencrypt.conf;</code></p> <p><code>    proxy_http_version 1.1;<br />     proxy_buffering off;</code></p> <p><code>    # static files<br />     location ^~ /loleaflet {<br />         proxy_pass https://localhost:9980;<br />         proxy_set_header Host $http_host;<br />     }</code></p> <p><code>    # WOPI discovery URL<br />     location ^~ /hosting/discovery {<br />         proxy_pass https://localhost:9980;<br />         proxy_set_header Host $http_host;<br />     }</code><br /><br /><code>    # download, presentation and image upload<br />     location ^~ /lool {<br />         proxy_pass https://localhost:9980;<br />         proxy_set_header Upgrade $http_upgrade;<br />         proxy_set_header Conection "upgrade";<br />         proxy_set_header Host $http_host;<br />     }<br /> }</code></p> <p>Once those are created, you have to make sure that they're "enabled" (replacing with your file names, of course):</p> <p><code>sudo cd /etc/nginx/sites-enabled<br /> sudo ln -sf ../sites-available/docs .<br /> sudo ln -sf ../sites-available/collab .</code></p> <p>To confirm that there aren't any typos or issues that might make nginx unhappy, run</p> <p><code>sudo nginx -t</code></p> <p>If all's well, get nginx to reread its configuration with the new files:</p> <p><code>sudo service nginx reload</code></p> <h2>Firing it all up!</h2> <p>Phew - congratulations on getting here! We've reached the moment of truth where we need to see if this whole thing will work!</p> <p>We need to make sure we're back in the Docker directory we set up:</p> <p><code>cd /home/docker/nextcloud-collabora</code></p> <p>and then we need to try running our docker-compose script to "pull" in the pre-built Docker containers we've specified in our docker-compose.yml file:</p> <p><code>docker-compose pull</code></p> <p>All going well, after a few minutes (longer or shorter depending on the speed of your server's connection) you should have download the Nginx, Redis, NextCloud and Collabora-CODE Docker images. Then you can run:</p> <p><code>docker-compose up -d &amp;&amp; docker-compose logs -f</code></p> <p>This will attempt to start up the containers (bringing them "up" in daemon mode, thus the -d) and then show you a stream of log messages from the containers, preceded by the container name. This should help you debug any problems that occur during the process (ideally, none).</p> <p>Once you see log messages streaming past, and no obvious "container exited" or other error messages (which will usually contain the word "error" a lot), you should be able to point your browser at your selected domain name and bring it up in your browser!</p> <h3>Setting up the database</h3> <p>On doing so, if all is well, you should be directed through the database set up process for your NextCloud instance. Your details should be:</p> <p>database IP: 172.17.0.1 - this is the default IP of the Docker host server.<br /> database name: nextcloud<br /> database user: nextcloud<br /> database password: (the one you came up with above)</p> <h3>Setting the Admin user</h3> <p>Once that's set and working, NextCloud will install all the relevant database tables and initial data. You'll be asked to set up an <em>admin user</em> account, which can be "admin" (you could make it something different to help stymie nefarious probes that assume you've got a user called "admin" - but don't forget what you've called it!) and some strong password you create (you can use the pwgen utility you used earlier) - I'd recommend recording it somewhere. I would <em>not</em> recommend making your own account, in your name, the main admin account. I recommend creating a second account, <em>with administrator privileges</em> for yourself, but leave the admin account purely for administrative activities.</p> <h3>Configuring Outgoing Email</h3> <p>To allow your NextCloud instance to send outgoing email, so that your site can alert you to security updates that need to be applied, or so that users can request a replacement password if they've forgot theirs, you'll need an <em>authenticating SMTP account</em> somewhere. Most of you already have one. You'll probably want to set up a dedicated email address for this server somewhere, perhaps something like "<a href="mailto:nextcloud@your.domain">nextcloud@your.domain</a>" or similar, with a username (often just the email address) and a password. You'll need the following details:</p> <p>SMTP server : an IP address or a domain name<br /> SMTP username: a username or an email address<br /> SMTP password: a strong password already configured for the username on that server<br /> SMTP login security: whether login is via TLS, SSL, or unsecure (!!), and<br /> SMTP login method: plain, encrypted, "login" or some other value.</p> <p>You should be able to test your email settings to make sure the details you've entered are valid. If you need to adjust these settings later, you can go to the admin menu (top right of the web browser interface) and go to Admin-&gt;Additional Settings  - should have a path of <a href="https://your.domain/settings/admin/additional">https://your.domain/settings/admin/additional</a></p> <h3>Configuring Collabora Office Integration</h3> <p>Once you're logged in as your own user, looking at your own default folders, you can start having a look around. You should have an "admin" menu (assuming you've created your user with Administrator privileges) at the top right of the web interface. If you go to Apps, you can use the search box to search for "Collabora" or go to the "Office &amp; text" App category. You'll need to "enable" the Collabora Online "official" app, at which point it will download the latest version of the connector app and install it (it should appear in your /home/data/nextcloud/apps directory)</p> <p>Once you've done that, go to your top right menu again, selecting Admin, and you should see "Collabora Online" as an option in the left column (which starts with "Basic settings"). Selecting that, you'll need to enter  "<a href="https://collab.domain">https://collab.domain</a>" (replacing with your domain, of course). I don't have any of the other options ticked.</p> <p>If it works, you should have the ability to go back to the home of your NextCloud install, which should show you your top-level folders. If you click the "+" next to the home icon (top left of the folder pane) you should now have the option to create (in addition to "Upload file", "New folder", "New text file") a "New Document", "New Spreadsheet", and "New Presentation". Clicking those should give you the Collabora Office interface for the designated content type.</p> <p>Similarly, you can use the "Upload file" to upload a document in a format that is supported by Collabora Office, once uploaded clicking on the filename should open it for editing in the appropriate Collabora Office interface.</p> <p>It is saved as it is change, you shouldn't need to save it explicitly.</p> <h2>Upgrading it</h2> <p>So, as you're no doubt aware, both NextCloud and Collabora Office are always being improved and updated. I certainly encourage you to keep your installation up-to-date.</p> <p>While you'll periodically see that NextCloud apps have available updates (these can be upgraded through the browser interface) updates to the NextCloud and Collabora Office systems themselves need to be undertaken by upgrading the containers. Luckily it's easy to do (although I strongly urge you to ensure you have a very recent backup of both database and uploaded files - they're the files in /home/data/nextcloud/data:</p> <p>Updating the container should be as easy as either doing another</p> <p><code>docker pull oeru/mautic</code></p> <p>and then shutting down Docker container via a</p> <p><code>docker-compose stop</code></p> <p>removing the old containers (this won't remove any data you want to save if you followed the directions above! But remember to do it in the right directory!) via</p> <p><code>docker-compose rm -v</code></p> <p>and then restarting it via</p> <p><code>docker-compose up -d</code></p> <p>Use <code>docker-compose logs -f</code> to watch the logs - you'll likely see debugging information in the unlikely event that something goes wrong in the upgrade process.</p> <h2>Backing it up</h2> <p>To back up your instance on your server, you need two things: a file system backup of your /home/data/nextcloud directory, and database dumps of your database.</p> <p>There're lots of ways to back up your files (I personally use a bash script that I wrote in a past role, which uses <a href="http://www.nongnu.org/rdiff-backup/">rdiff-backup</a> to create versioned backups either locally or on a remote server, although there're <a href="https://www.howtoforge.com/linux_rdiff_backup">other documented approaches</a> - leave a comment below if you'd like to learn more about my approach!).</p> <p>Backing up your database is as easy installing automysqlbackups:</p> <p><code>sudo apt install automysqlbackups</code></p> <p>You'll find daily versioned dumps of your MariaDB database(s) in /var/lib/automysqlbackups. To run an ad hoc backup (which will replace the previous backup from that day, if there is one) just run</p> <p><code>sudo automysqlbackups</code></p> <h2>Collabora Admin Console</h2> <p>Once you've got everything set up, you can access the admin console of the Collabora Office instance at the collab.domain you specified above - it'll have the path <code>https://collab.domain/loleaflet/dist/admin/admin.html</code> (of course replacing collab.domain with your domain) which gives you useful info about the system resources being used, number of documents being edited and by whom, and some other interesting details. I've included a screen shot.</p> <p>When prompted for login details, use the collab username - "admin" if you used the default I provided, and the password you set in your docker-compose.yml file above.</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> <h2 class="comment-field__title">Blog comments</h2> <article data-comment-user-id="0" id="comment-820" class="comment js-comment by-anonymous has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/820#comment-820" class="permalink" rel="bookmark" hreflang="en">All I get after all of that…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1636139893"></span> </h3> <div class="comment__meta"> <div class="comment__submitted"> <span class="comment__author"><span>Tim (not verified)</span></span> <span class="comment__pubdate">Sat 06/11/2021 - 04:21</span> </div> </div> <div class="comment__content"> <div class="clearfix text-formatted field field-comment--comment-body field-name-comment-body field-type-text-long field-label-hidden"> <div class="field__items"> <div class="field__item"><p>All I get after all of that is &quot;OK&quot;</p> </div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=820&amp;1=default&amp;2=en&amp;3=" token="9x_0sEHuh-dJwj9sa5I_pygyooUxR3CerM32QzLc50E"></drupal-render-placeholder> </div> </div> </article> <article data-comment-user-id="1" id="comment-821" class="comment js-comment by-node-author has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/821#comment-821" class="permalink" rel="bookmark" hreflang="en">Sorry Tim, after all what?</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1636155192"></span> </h3> <div class="comment__meta"> <div class="comment__submitted"> <span class="comment__author"><a title="View user profile." href="/user/1" class="username">dave</a></span> <span class="comment__pubdate">Sat 06/11/2021 - 08:18</span> </div> </div> <div class="comment__content"> <div class="clearfix text-formatted field field-comment--comment-body field-name-comment-body field-type-text-long field-label-hidden"> <div class="field__items"> <div class="field__item"><p>Sorry Tim, after all what? I'd love to provide assistance, but need you to be a bit more specific.</p></div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=821&amp;1=default&amp;2=en&amp;3=" token="RxxscuNlrNd_TL-Wh6nh4yeQjDHOY5TxdqhEDyLKN6E"></drupal-render-placeholder> </div> </div> </article> <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=17&amp;2=field_blog_comments&amp;3=comment" token="aF_f2aYW3SGRfmrsffb5fOJlN5wEXJibvMUiQKm2VjE"></drupal-render-placeholder> </div> </section> Mon, 29 Jan 2018 04:29:13 +0000 dave 17 at http://tech.oeru.org Installing Mastodon with Docker-Compose on Ubuntu 16.04 http://tech.oeru.org/installing-mastodon-docker-compose-ubuntu-1604 <span class="field field--name-title field--type-string field--label-hidden">Installing Mastodon with Docker-Compose on Ubuntu 16.04</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--mastodon"> <span class="field__item-wrapper"><a href="/taxonomy/term/31" hreflang="en">mastodon</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--_604"> <span class="field__item-wrapper"><a href="/taxonomy/term/27" hreflang="en">16.04</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--lets-encrypt"> <span class="field__item-wrapper"><a href="/taxonomy/term/17" hreflang="en">let&#039;s encrypt</a></span> </div> <div class="field__item field__item--ruby-on-rails"> <span class="field__item-wrapper"><a href="/taxonomy/term/22" hreflang="en">ruby on rails</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> </div> <span class="field field--name-uid field--type-entity-reference field--label-hidden"><a title="View user profile." href="/user/1" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Fri 02/06/2017 - 15:02</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/mastodon_userscreen2.png?itok=pyxlNcbL" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Mastodon webapp, showing a federated timeline (right panel - and yes, Mastodon is popular in Japan)&quot;}" role="button" title="The Mastodon webapp, showing a federated timeline (right panel - and yes, Mastodon is popular in Japan)" data-colorbox-gallery="gallery-field_image-s7IGI4s7mgA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Mastodon webapp, showing a federated timeline (right panel - and yes, Mastodon is popular in Japan)&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/mastodon_userscreen2.png?itok=P5YMy5Yf" width="220" height="141" alt="The Mastodon webapp, showing a federated timeline (right panel - and yes, Mastodon is popular in Japan)" loading="lazy" 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/mastodon_user_settings.png?itok=1CcpPZP0" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Mastodon user settings&quot;}" role="button" title="Mastodon user settings" data-colorbox-gallery="gallery-field_image-s7IGI4s7mgA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Mastodon user settings&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/mastodon_user_settings.png?itok=w2QahwB4" width="220" height="141" alt="Mastodon user settings" loading="lazy" 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/mastodon_admin_settings.png?itok=cRSdDIKu" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Mastodon administrator settings&quot;}" role="button" title="Mastodon administrator settings" data-colorbox-gallery="gallery-field_image-s7IGI4s7mgA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Mastodon administrator settings&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/mastodon_admin_settings.png?itok=jrYKu26i" width="220" height="141" alt="Mastodon administrator settings" loading="lazy" 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/mastodon_social_info.png?itok=_LbvxOiq" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Info on a heavily used Mastodon node (Mastodon.Social, the reference node)&quot;}" role="button" title="Info on a heavily used Mastodon node (Mastodon.Social, the reference node)" data-colorbox-gallery="gallery-field_image-s7IGI4s7mgA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Info on a heavily used Mastodon node (Mastodon.Social, the reference node)&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/mastodon_social_info.png?itok=N2x5gmY2" width="220" height="143" alt="Info on a heavily used Mastodon node (Mastodon.Social, the reference node)" loading="lazy" 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/mastodon_nzoss_info.png?itok=mT-9glYy" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Info on a more humble node (the NZ Open Source Society node I run)&quot;}" role="button" title="Info on a more humble node (the NZ Open Source Society node I run)" data-colorbox-gallery="gallery-field_image-s7IGI4s7mgA" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Info on a more humble node (the NZ Open Source Society node I run)&quot;}"><img src="/sites/default/files/styles/medium/public/2017-06/mastodon_nzoss_info.png?itok=93rckq5L" width="220" height="141" alt="Info on a more humble node (the NZ Open Source Society node I run)" loading="lazy" 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>Not long ago, <a href="https://github.com/tootsuite/mastodon" title="Scroll down to see the write up - the source code is front and centre.">Mastodon</a>, an open source, <em>federated</em> alternative to the proprietary network-effect wunderkind, Twitter, came out of no where. Actually, it came out of an insane amount of work done by free and open source powerhouse Eugen Rochko aka <a href="https://github.com/Gargron">Gargron</a> and a small elite developer community, and many predecessors who are part of the <a href="https://www.coactivate.org/projects/disintermedia/blog/2017/04/01/a-brief-history-of-the-gnu-social-fediverse-and-the-federation/">GNU Social Fediverse</a> (kudos to <a href="http://www.coactivate.org/people/strypey/profile">Danyl Strype</a> for compiling that excellent history).</p> <p>Mastodon, unlike Twitter, is entirely community driven - there are no ads, there are no privacy threats, there are no corporate Terms and Conditions to blindly "I Accept". And your Mastodon "persona" can be on a server you control (or that is controlled by someone you trust). Despite being distributed, you're still part of a global network, but one made resilient by its federated, decoupled nature.</p> <p>Instead of "Tweeting" in 140 characters like on Twitter, your "Toots" are limited to 500 characters (a lot more information can usefully be passed). You can follow people (and they you) by learning their handle - which looks like an email. I've got a couple Mastodon accounts, but my main one right now is <a href="mailto:lightweight@mastodon.social">lightweight@mastodon.social</a> (I set it up quite a while back, before I set up my first couple Mastodon servers). Actually, Mastodon's biggest problem (in my opinion) right now is that you can't easily migrate your "main" persona from one server to another without losing a lot of its value (historical toots, followers, those you follow, etc.). You can migrate some things, like those you're following, and any users you've "blocked" but it's still fairly rudimentary.</p> <p>Mastodon includes a nice web interface which will look somewhat familiar to anyone who's used Twitter's "Tweetdeck" web application. Similarly, the GNU Social community has rallied to provide at least 2 separate open source mobile apps (I run <a href="https://play.google.com/store/apps/details?id=com.keylesspalace.tusky&amp;hl=en">Tusky</a> on my <a href="https://lineageos.org" title="Open Source Android - the way it was supposed to be before the OEMs messed it up.">LineageOS</a> powered phone at the moment) - I think there're some for iOS, too, although Apple's not as amenable to open source apps. </p> <p>There's a useful <a href="https://github.com/tootsuite/documentation/blob/master/Using-Mastodon/FAQ.md">Mastodon FAQ</a>.</p> <h2>Running with the Mastodon Herd</h2> <p>The way I implement a complex Ruby on Rails app like Mastodon is to do as much as possible to keep it at arms length (and stop it from getting anything gooey on my virtual machine). To achieve that comforting isolation, I employ Docker Compose on Ubuntu Linux 16.04. See our <a href="/docker-compose-better-way-deploy-rocketchat-wekan-and-mongodb">Docker Compose article</a> on how to install it (and its dependencies, like Docker itself).</p> <p>Once you've got Docker Compose running, you can do what I did. </p> <p>A couple notes:</p> <ul><li>I have an unprivileged user on my server, "ubuntu". You can use any unprivileged users - I'd encourage you to use sudo rather than login as root.</li> <li>I use "vim" as my terminal-based text editor below. I think it's a great tool, but it does have a learning curve. If you're daunted (no shame in that), I recommend using "nano" instead - it'll probably installed on most Ubuntu 16.04 instances. If someone suggests you use "emacs" instead, they're jerkin' yer chain (I used emacs for over a decade, I know what I'm talking about).</li> <li>make sure you have the "git" VCS system installed... <code>sudo apt install git</code> should do it.</li> <li>you'll need nginx installed, too... <code>sudo apt install nginx-full</code> will do that for you.</li> </ul><p>After logging into my server (via SSH remotely) as the ubuntu user (you might have different non-privileged user name, that's ok), I did the following (to avoid permissions problems later on, we'll create a "mastodon" group and user with the id 991, used by the Mastodon app by default, on the hosting platform):</p> <p><code>groupadd -g 991 mastodon<br /> useradd -u 991 -g 991 -c "Mastodon User" -s /usr/bin/nologin -d /home/data/mastodon mastodon</code><br /><code>sudo mkdir -p /home/docker /home/data/mastodon<br /> sudo chown -R ubuntu:ubuntu /home/docker<br /> sudo chown -R mastodon:mastodon /home/data/mastodon</code><br /><code>cd /home/docker<br /> git clone https://github.com/tootsuite/mastodon.git docker-mastodon<br /> cd docker-mastodon</code></p> <p>What you then need to do is ensure you're using the current "tagged" release (it'll make your life easiest). You can find out what tags are available:</p> <p><code>git tag -l </code></p> <p>At present, the latest tag is "v1.4.7" - to use it do this:</p> <p><code>git checkout tags/v1.4.7</code></p> <p>Obviously, replace this with the most recent tag (note, you might have to look through the whole list to find it!). Then you're using the specific collection of files corresponding to the v1.4.7 tagged release. We can carry on...</p> <p><code>cp .env.production.sample .env.production<br /> vim .env.production</code></p> <p>Edit this file to look like the .env.production sample below, but replacing the [tokens] with your values. Then run this:</p> <p><code>vim docker-compose.yml</code></p> <p>Edit this file to look like docker-compose.yml below.</p> <p><code>docker-compose run --rm web rake secret </code></p> <p>Run this last command 3 times - to get 3 secrets - long random strings - for .env.production! Copy and paste your 3 secrets into your .env.production file with your preferred editor as shown below.</p> <p><code>docker-compose build<br /> docker-compose up</code></p> <p>That should download the required Docker images (might take quite a while depending on how fast your server's network connection is) and result in starting 5 different Docker containers, and you'll be able to watch them put out status (and error) messages as they boot and find their various dependencies. If there're no obvious errors, you can hit CTRL-C to shut them down again and restart them in a mode that keeps them running after you log out</p> <p><code>docker-compose up -d</code></p> <p>Note, you can always stop the containers by running docker-compose stop in that directory. You can check their status by running</p> <p><code>docker ps</code></p> <p>which should show you something like this:</p> <p><code>CONTAINER ID        IMAGE                    COMMAND                  CREATED             STATUS              PORTS                                NAMES<br /> c6be9f3eef1e        gargron/mastodon         "bundle exec rails s "   13 days ago         Up 13 days          127.0.0.1:3000-&gt;3000/tcp, 4000/tcp   dockermastodon_web_1<br /> 6a123d9b1843        gargron/mastodon         "bundle exec sidekiq "   13 days ago         Up 13 days          3000/tcp, 4000/tcp                   dockermastodon_sidekiq_1<br /> f06c4a9bc479        gargron/mastodon         "npm run start"          13 days ago         Up 13 days          3000/tcp, 127.0.0.1:4000-&gt;4000/tcp   dockermastodon_streaming_1<br /> 6dbfad0669f8        postgres:alpine          "docker-entrypoint.sh"   2 weeks ago         Up 13 days          5432/tcp                             dockermastodon_postgres_1<br /> 8026b79e976d        redis:alpine             "docker-entrypoint.sh"   4 weeks ago         Up 13 days          6379/tcp                             dockermastodon_redis_1</code></p> <p>You can use the 12 digit IDs to run other Docker commands, like <code>docker inspect [ID]</code> or <code>docker exec -it [ID] bash</code> to log into the container itself and get a bash prompt. After all that's running, you can do some final housekeeping:</p> <p><code>docker-compose run --rm web rails db:migrate<br /> docker-compose run --rm web rails assets:precompile<br /> sudo vim /etc/nginx/sites-available/mastodon</code></p> <p>Edit this to look like the mastodon nginx config file below.</p> <p><code>sudo cd /etc/nginx/sites-enabled<br /> sudo ln -sf ../sites-available/mastodon .</code></p> <p>to enable the new configuration...</p> <p><code>sudo nginx -t</code></p> <p>To check for typos in you file. If you get no errors, you can restart nginx:</p> <p><code>sudo service nginx restart</code></p> <p>When that's done,  to [your domain] in your browser, which should take you to https://[your domain] and create a new user. If your email is set up properly, you'll get an email confirmation, and this will allow you to log in. If that works, I'd encourage you to modify your configuration to use a Let's Encrypt SSL certificate to protect your users' (and your server's) security. <a href="/protecting-your-users-lets-encrypt-ssl-certs">We provide this dedicated howto</a>! The .env.production template below <em>assumes you've done this</em>, so if your Mastodon isn't working, that might be why (you can try turning <code>LOCAL_HTTPS=false</code> temporarily if that's helpful).</p> <p>You will want to create an admin user - create the user first through the web interface, and then on the command line run (replacing <code>[admin username] </code>with the username you set up:</p> <p><code>cd /home/docker</code><code>/docker-mastodon<br /> docker-compose run --rm web rails mastodon:make_admin USERNAME=[admin username]</code></p> <p>Then go to that user's Mastodon preferences and define the relevant info for your instance (see the administration options).</p> <h2>Debugging</h2> <p>If you run in to problems, a very useful Docker Compose option to use (from within the docker-mastodon directory) is </p> <p><code>docker-compose logs -f</code></p> <p>It will provide you with the automatically updating integrated logs of all the containers you've unleashed!</p> <h3>Sample .env.production</h3> <p>Here's a sample with (hopefully obviously named) [placeholders]</p> <p><code># Service dependencies<br /> REDIS_HOST=redis<br /> REDIS_PORT=6379<br /> DB_HOST=postgres<br /> DB_USER=postgres<br /> DB_NAME=postgres<br /> DB_PASS=<br /> DB_PORT=5432</code></p> <p><code># Federation<br /> LOCAL_DOMAIN=[your domain]<br /> LOCAL_HTTPS=true</code></p> <p><code># Application secrets<br /> # Generate each with the `rake secret` task (`docker-compose run --rm web rake secret` if you use docker compose)<br /> PAPERCLIP_SECRET=[first secret]<br /> SECRET_KEY_BASE=[second secret]<br /> OTP_SECRET=</code>[third secret]</p> <p><code># Registrations<br /> # Single user mode will disable registrations and redirect frontpage to the first profile<br /> # SINGLE_USER_MODE=true<br /> # Prevent registrations with following e-mail domains<br /> # EMAIL_DOMAIN_BLACKLIST=example1.com|example2.de|etc</code></p> <p><code># E-mail configuration<br /> SMTP_SERVER=[smtp server domain name]<br /> SMTP_PORT=587<br /> SMTP_LOGIN=[smtp user name]<br /> SMTP_PASSWORD=[smtp user password]<br /> SMTP_FROM_ADDRESS=[sender address for outgoing mastodon emails]<br /> SMTP_DOMAIN=[your site's base domain]<br /> SMTP_OPENSSL_VERIFY_MODE=none</code></p> <p><code># Optional asset host for multi-server setups<br /> # CDN_HOST=assets.example.com</code></p> <p><code># S3 (optional)<br /> # S3_ENABLED=true<br /> # S3_BUCKET=<br /> # AWS_ACCESS_KEY_ID=<br /> # AWS_SECRET_ACCESS_KEY=<br /> # S3_REGION=<br /> # S3_PROTOCOL=http<br /> # S3_HOSTNAME=192.168.1.123:9000</code></p> <p><code># Optional alias for S3 if you want to use Cloudfront or Cloudflare in front<br /> # S3_CLOUDFRONT_HOST=</code></p> <p><code># Streaming API integration<br /> # STREAMING_API_BASE_URL=</code></p> <h3>Sample docker-compose.yml</h3> <p>Here's a sample with [placeholders]. Note - this generates <strong>five Docker containers. </strong>Yeah, like I said, this is a serious, complex app.</p> <p><code>version: '2'<br /> services:<br />   postgres:<br />     restart: unless-stopped<br />     image: postgres:alpine<br />     volumes:<br />      - /home/data/mastodon/postgres:/var/lib/postgresql/data<br />   redis:<br />     restart: unless-stopped<br />     image: redis:alpine<br />     volumes:<br />      - /home/data/mastodon/redis:/data<br />   web:<br />     restart: unless-stopped<br />     build: .<br />     image: gargron/mastodon<br />     env_file: .env.production<br />     command: bundle exec rails s -p 3000 -b '0.0.0.0'<br />     ports:<br />       - "127.0.0.1:3000:3000"<br />     depends_on:<br />       - postgres<br />       - redis<br />     volumes:<br />       - /home/data/mastodon/packs:/mastodon/public/packs</code><br /><code>      - /home/data/mastodon/assets:/mastodon/public/assets</code><br /><code>      - /home/data/mastodon/system:/mastodon/public/system<br />   streaming:<br />     restart: unless-stopped<br />     build: .<br />     image: gargron/mastodon<br />     env_file: .env.production<br />     command: npm run start<br />     ports:<br />       - "127.0.0.1:4000:4000"<br />     depends_on:<br />       - postgres<br />       - redis<br />   sidekiq:<br />     restart: unless-stopped<br />     build: .<br />     image: gargron/mastodon<br />     env_file: .env.production<br />     command: bundle exec sidekiq -q default -q mailers -q pull -q push<br />     depends_on:<br />       - postgres<br />       - redis<br />     volumes:<br />       - /home/data/mastodon/system:/mastodon/public/system</code></p> <h3>Sample nginx mastodon config file</h3> <p>Here's a copy of the nginx configuration file I use (with [placeholders], obviously) - it's the result of quite a lot of tweaking. Have fun!</p> <p><code>map $http_upgrade $connection_upgrade {<br />     default upgrade;<br />     ''      close;<br /> }</code></p> <p><code>server {<br />     listen 80;<br /> #    listen [::]:80;<br />     server_name [your domain];<br />     root /var/www/html;</code></p> <p><code>    # for let's encrypt renewals!<br />     location /.well-known/acme-challenge/ {<br />         default_type text/plain;<br />         root /var/www/html;<br />    }</code></p> <p><code>    # redirect all HTTP traffic to HTTPS.<br />     location / {<br />         return 302 https://[your domain]$request_uri;<br />     }<br /> }</code></p> <p><code>server {<br />     listen 443 ssl;<br /> #    listen [::]:443 ssl;<br />     server_name [your domain];</code></p> <p><code>    ssl_certificate /etc/letsencrypt/live/[your domain]/fullchain.pem;<br />     ssl_certificate_key /etc/letsencrypt/live/[your domain]/privkey.pem;<br />     ssl_protocols TLSv1 TLSv1.1 TLSv1.2;<br />     # from https://0x39b.fr/post/nginx_security/<br />     ssl_session_timeout 1d;<br />     ssl_session_cache shared:SSL:50m;<br />     #ssl_session_tickets off;<br />     ssl_prefer_server_ciphers on;<br />     ssl_ciphers 'ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA256';<br />     # OCSP Stapling ---<br />     # fetch OCSP records from URL in ssl_certificate and cache them<br />     ssl_stapling on;<br />     ssl_stapling_verify on;<br />     # to create this, see https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html<br />     ssl_dhparam /etc/ssl/certs/dhparam.pem;</code></p> <p><code>    # for let's encrypt renewals!<br />     location /.well-known/acme-challenge/ {<br />         default_type text/plain;<br />         root /var/www/html;<br />     }</code></p> <p><code>    keepalive_timeout    70;<br />     sendfile             on;<br />     client_max_body_size 0;<br />    </code>  <code># update from https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md<br />     gzip on;<br />     gzip_vary on;<br />     gzip_proxied any;<br />     gzip_comp_level 6;<br />     gzip_buffers 16 8k;<br />     gzip_http_version 1.1;<br />     gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;</code></p> <p><code>    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains";</code></p> <p><code>    location / {<br />         try_files $uri @proxy;<br />     }</code></p> <p><code>    location ~ ^/(packs|system/media_attachments/files|system/accounts/avatars) {<br />         add_header Cache-Control "public, max-age=31536000, immutable";<br />         try_files $uri @proxy;<br />     }</code></p> <p> </p> <p><code>    location @proxy {<br />         proxy_set_header Host $host;<br />         proxy_set_header X-Real-IP $remote_addr;<br />         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br />         proxy_set_header X-Forwarded-Proto https;</code><br /><code>        proxy_set_header Proxy "";</code><br /><code>        proxy_pass_header Server;<br />         proxy_pass http://localhost:3000;<br />         proxy_buffering off;<br />         proxy_redirect off;<br />         proxy_http_version 1.1;<br />         proxy_set_header Upgrade $http_upgrade;<br />         proxy_set_header Connection $connection_upgrade;<br />         tcp_nodelay on;<br />     }</code></p> <p><code>    location /api/v1/streaming {<br />         proxy_set_header Host $host;<br />         proxy_set_header X-Real-IP $remote_addr;<br />         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;<br />         proxy_set_header X-Forwarded-Proto https;<br />         proxy_set_header Proxy "";<br />         proxy_pass http://localhost:4000;<br />         proxy_buffering off;<br />         proxy_redirect off;<br />         proxy_http_version 1.1;<br />         proxy_set_header Upgrade $http_upgrade;<br />         proxy_set_header Connection $connection_upgrade;<br />         tcp_nodelay on;<br />     }</code></p> <p><code>    error_page 500 501 502 503 504 /500.html;<br />     # this should give you an A+ rating on https://instances.mastodon.xyz/<br />     add_header X-XSS-Protection "1; mode=block";<br />     add_header Content-Security-Policy "default-src 'none'; font-src 'self'; media-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data: blob:; connect-src 'self' wss://[your domain]";<br /> }</code></p> <p>Enjoy!</p> <h2>Keeping Mastodon up to date</h2> <p>To ensure your Mastodon doesn't become a run down abandoned trailerpark node bit rotting quietly in the ether, I recommend you keep it up to date! There is a useful <a href="https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Docker-Guide.md">Mastodon Administrator's guide for Docker instances</a> that I consult every time I want to update. Note, if the "git stash" part of it is too hard, I recommend that any time you change your docker-compose.yml file, you copy it to</p> <p><code>cp docker-compose.yml docker-compose.yml-backup</code></p> <p>That way, you can simply remove docker-compose.yml (double check your docker-compose.yml-backup is up-to-date first!), do the <code>git checkout TAG_NAME</code>, and then</p> <p><code>cp docker-compose.yml-backup docker-compose.yml </code></p> <p>and you're done. Welcome the Fediverse!</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=14&amp;2=field_blog_comments&amp;3=comment" token="MWkKHP2AmYlJPPoRbiZwT_Yf5xOfApOMw_Kb2WBZEpo"></drupal-render-placeholder> </div> </section> Fri, 02 Jun 2017 03:02:31 +0000 dave 14 at http://tech.oeru.org http://tech.oeru.org/installing-mastodon-docker-compose-ubuntu-1604#comments Many simple tools, loosely coupled http://tech.oeru.org/many-simple-tools-loosely-coupled <span class="field field--name-title field--type-string field--label-hidden">Many simple tools, loosely coupled</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--philosophy"> <span class="field__item-wrapper"><a href="/taxonomy/term/28" hreflang="en">philosophy</a></span> </div> <div class="field__item field__item--unix"> <span class="field__item-wrapper"><a href="/taxonomy/term/29" hreflang="en">unix</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--docker"> <span class="field__item-wrapper"><a href="/taxonomy/term/16" hreflang="en">docker</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" class="username">dave</a></span> <span class="field field--name-created field--type-created field--label-hidden">Mon 08/05/2017 - 15:10</span> <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>Our approach to technology here at the <a href="https://oeru.org" title="The Open Education Resource universitas">OERu</a> is inspired by the <a href="http://www.faqs.org/docs/artu/ch01s06.html">UNIX tool philosophy</a> which can be summarised as follows:</p> <blockquote> <p>"create simple tools that do one job well, and make it easy to combine them to work together"</p> </blockquote> <p>In the UNIX (and, somewhat more recently, the Linux) computing environment, this originally meant a lot of small commandline applications like "ls" for listing the contents of file directories, and "grep" for searching directories of files for words and other snippets of content, and "diff" for showing the difference between two files, and many many more. These all output text, and they also accept text as an input - you can <em>chain</em> all of these simple little applications together to create, on the fly, remarkably complex capabilities. This is one of the things that makes Linux and the commandline so powerful for those who have learned its lore (and so intimidating for those who haven't yet done so). </p> <p>This idea of "loosely coupled" tools, working together is also a good way to describe both the OERu technology and documentation philosophy.</p> <p>On this website, the way it manifests is interesting - each time I write a howto article, there're certain common tasks - things like setting up Docker, or creating secure SSL certificates for encrypting user interactions with a web service.</p> <p>Initially, I wrote howtos with all of those details contained in one document, however the instructions fairly quickly become outdated, for example, the install process for Docker or Let's Encrypt is changed by its community (usually to make it faster and more convenient) or to reflect the release of new software dependencies. It doesn't sit well with me to be leaving outdated or inaccurate resources on the web - I feel a responsibility for curating them to improve the signal-to-noise ratio of the 'net. Also, it rapidly becomes an intractable problem to go through old howtos to update all the slight variations on the same instructions to something new (the problem grows exponentially as more howtos are added).</p> <p>So, taking the UNIX approach, I use my experience writing a few howtos to provide insight into parts of each that are repeated. Any section repeated (more or less unchanged) in each howto is a candidate for replacement with a stand-alone howto.</p> <p>As it turns out, the community that's building the Docker container technology has done a good job of keeping their <a href="https://docs.docker.com/engine/installation/linux/ubuntu/">installation documentation</a> up-to-date and making it easy to find the relevant info for our target platforms, Ubuntu Linux 14.04 and 16.04. As a result, there's no point in my repeating their instructions. Instead, I just point my readers there when it's time to install Docker.</p> <p>My first candidate for documentation "<a href="https://en.wikipedia.org/wiki/Code_refactoring">refactoring</a>" was the "<a href="/protecting-your-users-lets-encrypt-ssl-certs">Let's Encrypt</a>" SSL Certificate process (it's a feature of almost all my howtos to date). Having now created that stand-alone howto, I can replace the largely repeated sections of several howtos with a single link to the same place.</p> <p>Yes, this change adds the overhead of the reader of a howto needing to go to a different article on this site, but I think this is greatly outweighed by the benefits: if I need to update or improve the description of this operation, I can simply update one document and ensure it's got the "best of" tips across all the different howtos. Also, if people leave questions or comments, all relevant ones will be in the same place, making it easier for other site visitors to find them.</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=12&amp;2=field_blog_comments&amp;3=comment" token="OgsykCUI1TUoj8ce8ZZ9L5Ax7Aq8mwkhmaCmxsLyqQI"></drupal-render-placeholder> </div> </section> Mon, 08 May 2017 03:10:35 +0000 dave 12 at http://tech.oeru.org