dave's blog http://tech.oeru.org/ en Installing Authentik for Authentication and Single-Sign-On http://tech.oeru.org/installing-authentik-authentication-and-single-sign <span class="field field--name-title field--type-string field--label-hidden">Installing Authentik for Authentication and Single-Sign-On </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--single-sign-on"> <span class="field__item-wrapper"><a href="/taxonomy/term/87" hreflang="en">single sign on</a></span> </div> <div class="field__item field__item--sso"> <span class="field__item-wrapper"><a href="/taxonomy/term/88" hreflang="en">sso</a></span> </div> <div class="field__item field__item--authentik"> <span class="field__item-wrapper"><a href="/taxonomy/term/89" hreflang="en">authentik</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--nginx"> <span class="field__item-wrapper"><a href="/taxonomy/term/30" hreflang="en">nginx</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--python"> <span class="field__item-wrapper"><a href="/taxonomy/term/90" hreflang="en">python</a></span> </div> <div class="field__item field__item--django"> <span class="field__item-wrapper"><a href="/taxonomy/term/91" hreflang="en">django</a></span> </div> <div class="field__item field__item--openid"> <span class="field__item-wrapper"><a href="/taxonomy/term/92" hreflang="en">OpenID</a></span> </div> <div class="field__item field__item--oauth2"> <span class="field__item-wrapper"><a href="/taxonomy/term/23" hreflang="en">oauth2</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/10/2023 - 11:15</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-10/Screenshot%202023-10-26%20at%2017-50-10%20Logged%20in%20to%20FOSSDLE%20Commons%20with%20available%20applications.png?itok=BaDedMB2" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Example of the user &#039;dashboard&#039; of available applications to log into after authenticating into the Authentik system. The array of available services will vary depending on the groups in which the autheticated user has been included.&quot;}" role="button" title="Example of the user &#039;dashboard&#039; of available applications to log into after authenticating into the Authentik system. The array of available services will vary depending on the groups in which the autheticated user has been included." data-colorbox-gallery="gallery-field_image-9R_772UzhgE" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Example of the user &#039;dashboard&#039; of available applications to log into after authenticating into the Authentik system. The array of available services will vary depending on the groups in which the autheticated user has been included.&quot;}"><img src="/sites/default/files/styles/medium/public/2023-10/Screenshot%202023-10-26%20at%2017-50-10%20Logged%20in%20to%20FOSSDLE%20Commons%20with%20available%20applications.png?itok=0ckMhPl9" width="220" height="167" alt="Example of the user &#039;dashboard&#039; of available applications to log into after authenticating into the Authentik system. The array of available services will vary depending on the groups in which the autheticated user has been included." 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-10/Screenshot%202023-10-27%20at%2009-37-44%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=QTsUT2qD" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The default administrator&#039;s page showing basic statistics about the Authentik instance. &quot;}" role="button" title="The default administrator&#039;s page showing basic statistics about the Authentik instance. " data-colorbox-gallery="gallery-field_image-9R_772UzhgE" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The default administrator&#039;s page showing basic statistics about the Authentik instance. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-10/Screenshot%202023-10-27%20at%2009-37-44%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=uYoyGXfX" width="220" height="167" alt="The default administrator&#039;s page showing basic statistics about the Authentik 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-10/Screenshot%202023-10-27%20at%2009-38-13%20Providers%20-%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=iSQ-oLDw" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Sample view of a list of &#039;providers&#039; or applications for which our Authentik provides acceptable authentication/authorisation tokens. &quot;}" role="button" title="Sample view of a list of &#039;providers&#039; or applications for which our Authentik provides acceptable authentication/authorisation tokens. " data-colorbox-gallery="gallery-field_image-9R_772UzhgE" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Sample view of a list of &#039;providers&#039; or applications for which our Authentik provides acceptable authentication/authorisation tokens. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-10/Screenshot%202023-10-27%20at%2009-38-13%20Providers%20-%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=tU5d-Zas" width="220" height="167" alt="Sample view of a list of &#039;providers&#039; or applications for which our Authentik provides acceptable authentication/authorisation tokens. " 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-10/Screenshot%202023-10-27%20at%2009-38-35%20Applications%20-%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=3I3R6Fth" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;An example look at the applications configured for use with our Authentik instance (we plan to expand this greatly!).&quot;}" role="button" title="An example look at the applications configured for use with our Authentik instance (we plan to expand this greatly!)." data-colorbox-gallery="gallery-field_image-9R_772UzhgE" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;An example look at the applications configured for use with our Authentik instance (we plan to expand this greatly!).&quot;}"><img src="/sites/default/files/styles/medium/public/2023-10/Screenshot%202023-10-27%20at%2009-38-35%20Applications%20-%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=-mEDX6dn" width="220" height="167" alt="An example look at the applications configured for use with our Authentik instance (we plan to expand this greatly!)." 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-10/Screenshot%202023-10-27%20at%2009-39-15%20Flows%20-%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=6LkZSLdu" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Sample view of the &#039;flows&#039; defined in our Authentik instance. Arbitrarily complex &#039;flows&#039; (or paths through interaction with the Authentik system) can be defined. &quot;}" role="button" title="Sample view of the &#039;flows&#039; defined in our Authentik instance. Arbitrarily complex &#039;flows&#039; (or paths through interaction with the Authentik system) can be defined. " data-colorbox-gallery="gallery-field_image-9R_772UzhgE" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Sample view of the &#039;flows&#039; defined in our Authentik instance. Arbitrarily complex &#039;flows&#039; (or paths through interaction with the Authentik system) can be defined. &quot;}"><img src="/sites/default/files/styles/medium/public/2023-10/Screenshot%202023-10-27%20at%2009-39-15%20Flows%20-%20Admin%20-%20FOSSLE%20Login%20and%20Authentication.png?itok=cOut-nXA" width="220" height="167" alt="Sample view of the &#039;flows&#039; defined in our Authentik instance. Arbitrarily complex &#039;flows&#039; (or paths through interaction with the Authentik system) can be defined. " 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 OER Foundation offers many Free and Open Source Software services to our learners and educators to provide them with resources for learning, developing Open Educational Resources (OER), and collaborating - both professionally and socially - with one another. Due to this proliferation of 'point source' technologies, users have to create a myriad of user accounts, each requesting an email and password and perhaps a user name. For many, especially those who have not adopted a <a href="/node/25">password keeper</a>, this can be an onerous situation, and often results in very poor 'internet hygiene' due to people choosing simplistic passwords and using the same small set of passwords in many contexts.</p> <p>An alternative to this default approach is for the OER Foundation to adopt an authentication and authorisation solution. Authentication means creating a single link between a real (physical) person and their online (virtual) identity - usually managed with a login and password and might also include one or more additional factors of identification, also known as '<a href="https://en.wikipedia.org/wiki/Multi-factor_authentication">Multi-Factor Authentication</a>' or MFA. Authorisation involves allowing authenticated users access to systems to which they should have access, and not those to which they should not. An suitable authentication and authorisation system can then be used to implement a '<a href="https://en.wikipedia.org/wiki/Single_sign-on">Single Sign-On</a>' (SSO) system that provides the option to all those with credentials on it to access - subject to any authorisation limitations - any of the OER Foundation's services transparently without needing to create a new, separate account.</p> <p>The Authentik project offers <a href="https://goauthentik.io/docs/installation/docker-compose">quite good documentation for Docker Compose installation</a>, too. This tutorial should be seen as a complement to that, perhaps providing a bit more guidance.</p> <ul class="table-of-contents"><li> <p><a href="#preparing-a-suitable-server">Preparing a suitable server</a></p> </li> <li> <p><a href="#ground-work">Ground work</a></p> </li> <li> <p><a href="#configuring-the-reverse-proxy">Configuring the reverse proxy</a></p> </li> <li> <p><a href="#getting-your-lets-encrypt-ssl-certificate">Getting your Let's Encrypt SSL certificate</a></p> </li> <li> <p><a href="#docker-compose-configuration">Docker Compose configuration</a></p> </li> <li> <p><a href="#boom-setting-up-your-authentik-instance">Boom: Setting up your Authentik instance</a></p> </li> <li> <p><a href="#upgrading-your-authentik">Upgrading your Authentik</a></p> </li> <li> <p><a href="#backing-up-your-authentik-data">Backing up your Authentik data</a></p> </li> </ul><h2><a id="user-content-preparing-a-suitable-server" href="#preparing-a-suitable-server" name="preparing-a-suitable-server" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Preparing a suitable server</h2> <p>All of our OER Foundation services share a common hosting platform. You'll need to <a href="/node/59">create a replica of that platform</a> - if you don't already have one - so you can add this service. <em>Everything else in this post assumes you've followed those instructions</em>!</p> <p>Note, the Authentik developers suggest a minimum VPS specification of 2 CPU cores and 2 GB RAM, so a very small instance should suffice.</p> <h2><a id="user-content-ground-work" href="#ground-work" name="ground-work" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Ground work</h2> <p>To set up Authentik, you'll need a few key details specific to your instance. For the rest of this tutorial, we'll use the convention of representing those variables as a name inside [], for example, the domain name that identifies your instance, which is referred to by the token [domain name]. Note - you'll see a few other instances of [] in the content below - the ones with multiple ::: in them, e.g. [:::], are not tokens, they're the way IPv6</p> <p>The tokens for which you'll need values are as follows:</p> <ul><li>your instance [domain name], i.e. a fully qualified domain name or a sub domain by which people will reach your instance. In our case, for example, we use 'auth.fossdle.org', which is the 'auth' subdomain of the 'fossdle.org' fully qualified domain name.</li> <li>Authenticating SMTP (Simple Mail Transfer Protocol, i.e. the language for sending email) 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 host]</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 username]</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> <li> <strong>[smtp from email]</strong> - this is the email address <em>from</em> which the system emails will be sent - it's what recipients will see and the address to whom they'll be able to respond. You might want something like 'noreply@[domain name]'... or maybe (if you do accept responses) 'webmaster@[domain name]'.</li> </ul></li> <li> <strong>[postgresql password]</strong> - a strong random password, say 12-99 characters long, sticking with letters and numbers as some symbols might be mis-interpreted in the context of the configuration file in which it's stored (e.g. if it contains a '#', the rest of the password might be interpreted as a comment. Or if you enclosed it in "", if it includes a " as a symbol, it might be interpreted as prematurely closing your double quotes). To generate, see below.</li> <li> <strong>[authentik secret]</strong> - a random 'secret key' of 50 characters is recommended. Again, see below.</li> </ul><p>To generate (relatively) random passwords/keys, you can install the following:</p> <p><code>sudo apt install -y pwgen</code></p> <p>To generate, say, a 40 character [postgresql password], you could run</p> <p><code>pwgen -s 40 1</code></p> <p>and grab the output, e.g. 28RFCIFtnVM9tFOmT2zx2fKiK5sf42WGK8FHr9Bn</p> <p>For a 50 character [authentik secret] just copy the output of</p> <p><code>pwgen -s 50 1</code></p> <p>Learn more about specifying random passwords via <code>man pwgen</code> or <code>pwgen --help</code> and look at our tutorial on <a href="/node/43">creating good passwords</a>.</p> <p>Also, you'll need to edit files, so it's worth setting up a text editor. If you're new to Linux, 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> <h2><a id="user-content-configuring-the-reverse-proxy" href="#configuring-the-reverse-proxy" name="configuring-the-reverse-proxy" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Configuring the reverse proxy</h2> <p>We need to configure a 'reverse proxy' so that external requests to our new Authentik service are routed to our containers, and so that all communication between our service's users and our service are suitably encrypted via Secure Sockets Layer (SSL).</p> <p>You should already have Nginx and Let's Encrypt installed on your server, so all you need to do is set up a new configuration file. The convention I use would involve doing this:</p> <p><code>sudo $EDIT /etc/nginx/sites-available/[domain name]</code></p> <p>where you put your domain name as the filename as indicated. You can copy and paste the following into the configuration file, replacing tokens like [domain name] with your value:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># Upstream where your authentik server is hosted.</span> upstream authentik <span>{</span> server 127.0.0.1:<span>9443</span>; <span># Improve performance by keeping some connections alive.</span> keepalive <span>10</span>; <span>}</span>   <span># Upgrade WebSocket if requested, otherwise use keepalive</span> map <span>$http_upgrade</span> <span>$connection_upgrade_keepalive</span> <span>{</span> default upgrade; <span>''</span> <span>''</span>; <span>}</span>   server <span>{</span> <span># HTTP server config</span> listen <span>80</span>; listen <span>[</span>::<span>]</span>:<span>80</span>; server_name <span>[</span>domain name<span>]</span>;   include includes<span>/</span>letsencrypt.conf;   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># 301 redirect to HTTPS</span> location <span>/</span> <span>{</span> <span>return</span> <span>301</span> https:<span>//</span><span>$host</span><span>$request_uri</span>; <span>}</span> <span>}</span> server <span>{</span> <span># HTTPS server config</span> listen <span>443</span> ssl http2; listen <span>[</span>::<span>]</span>:<span>443</span> ssl http2; server_name <span>[</span>domain name<span>]</span>;   <span># TLS 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>#ssl_certificate /etc/letsencrypt/live/[domain name]/fullchain.pem;</span> <span>#ssl_certificate_key /etc/letsencrypt/live/[domain name]/privkey.pem;</span> <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;   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># Proxy site</span> location <span>/</span> <span>{</span> proxy_pass https:<span>//</span>authentik; proxy_http_version <span>1.1</span>; proxy_set_header X-Forwarded-Proto <span>$scheme</span>; proxy_set_header X-Forwarded-For <span>$proxy_add_x_forwarded_for</span>; proxy_set_header Host <span>$host</span>; proxy_set_header Upgrade <span>$http_upgrade</span>; proxy_set_header Connection <span>$connection_upgrade_keepalive</span>; <span>}</span> <span>}</span></pre></div></div> <p>Note that if your server has other services on it and, by coincidence, one is already using port 9443, then you can pick another port (e.g. 9444) and see if that works. If so, you'll need to make sure that you replace 9443 in the <code>docker-compose.yml</code> below, too.</p> <p>Once you've saved the file, you can 'enable' it straight away. To do that, you need to do the following (again, replacing the tokens):</p> <p><code>sudo ln -sf /etc/nginx/sites-available/[domain name] /etc/nginx/sites-enabled</code></p> <p>which links the configuration file you've just created into the 'sites-enabled' directory. You can then test whether Nginx accepts the configuration you've added:</p> <p><code>sudo nginx -t</code></p> <p>If there're no errors here, you can make the changes live:</p> <p><code>sudo service nginx reload</code></p> <h2><a id="user-content-getting-your-lets-encrypt-ssl-certificate" href="#getting-your-lets-encrypt-ssl-certificate" name="getting-your-lets-encrypt-ssl-certificate" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Getting your Let's Encrypt SSL certificate</h2> <p>You can now request an SSL certificate for your new domain (making use of the <a href="/node/59">configuration set up the server tutorial</a>), obviously replacing [domain name] with yours:</p> <p>`sudo letsencrypt certonly --webroot -w /var/www/letsencrypt -d [domain name]</p> <p>After you've got your certificate (which should only take a few seconds), you can go back into your proxy configuration (remember it's the same file as /etc/nginx/sites-enabled/[domain name]):</p> <p><code>sudo $EDIT /etc/nginx/sites-available/[domain name]</code></p> <p>and alter the certificates by changing just the following lines:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre> <span># TLS 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> 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>Then you just need to make sure there're no typos:</p> <p><code>sudo nginx -t</code></p> <p>and make the changes live:</p> <p><code>sudo service nginx reload</code></p> <p>Now external people will be able to securely access your Authentik once the service is running!</p> <h2><a id="user-content-docker-compose-configuration" href="#docker-compose-configuration" name="docker-compose-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Docker Compose configuration</h2> <p>The next thing you need is a <code>docker-compose.yml</code> file. I usually put this in the a directory called <code>/home/docker/[domain name]</code> (replacing the token with your domain name!). You'll want to be in that directory:</p> <p><code>cd /home/docker/[domain name]</code></p> <p>Because the developers of Authentik periodically change the default configuration of their <code>docker-compose.yml</code> file, it's best to grab their current one from here:</p> <p><code>wget https://goauthentik.io/docker-compose.yml</code></p> <p>which should result in a file called <code>docker-compose.yml</code> appearing in that directory (run <code>ls -l</code> to confirm).</p> <p>I'd make a backup of this file for future reference:</p> <p><code>cp docker-compose.yml docker-compose.yml-default</code></p> <p>You can then</p> <p><code>$EDIT docker-compose.yml</code></p> <p>and it should look similar to the one below, albeit probably with a different AUTHENTIK_TAG number - the version as of this writing is 2023.10.2 - and with different 'volumes' specified.</p> <p>The only things you'll likely need to change are the designations for the 'volumes:'. See below where I've specified volumes in your <code>/home/data/[domain name]</code> directory rather than the local directory as the default docker-compose.yml defaults to.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>version: <span>'3.4'</span>   services: postgresql: image: docker.io<span>/</span>library<span>/</span>postgres:<span>12</span>-alpine restart: unless-stopped healthcheck: test: <span>[</span><span>"CMD-SHELL"</span>, <span>"pg_isready -d $<span>${POSTGRES_DB}</span> -U $<span>${POSTGRES_USER}</span>"</span><span>]</span> start_period: 20s interval: 30s retries: <span>5</span> timeout: 5s volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>postgres:<span>/</span>var<span>/</span>lib<span>/</span>postgresql<span>/</span>data environment: - <span>POSTGRES_PASSWORD</span>=<span>${PG_PASS:?database password required}</span> - <span>POSTGRES_USER</span>=<span>${PG_USER:-authentik}</span> - <span>POSTGRES_DB</span>=<span>${PG_DB:-authentik}</span> env_file: - .env redis: image: docker.io<span>/</span>library<span>/</span>redis:alpine command: <span>--save</span> <span>60</span> <span>1</span> <span>--loglevel</span> warning restart: unless-stopped healthcheck: test: <span>[</span><span>"CMD-SHELL"</span>, <span>"redis-cli ping | grep PONG"</span><span>]</span> start_period: 20s interval: 30s retries: <span>5</span> timeout: 3s volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>redis:<span>/</span>data server: image: <span>${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}</span>:<span>${AUTHENTIK_TAG:-2023.10.2}</span> restart: unless-stopped command: server environment: AUTHENTIK_REDIS__HOST: redis AUTHENTIK_POSTGRESQL__HOST: postgresql AUTHENTIK_POSTGRESQL__USER: <span>${PG_USER:-authentik}</span> AUTHENTIK_POSTGRESQL__NAME: <span>${PG_DB:-authentik}</span> AUTHENTIK_POSTGRESQL__PASSWORD: <span>${PG_PASS}</span> AUTHENTIK_ERROR_REPORTING__ENABLED: <span>"true"</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>media:<span>/</span>media - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>custom-templates:<span>/</span>templates env_file: - .env ports: - <span>"127.0.0.1:<span>${AUTHENTIK_PORT_HTTP:-9080}</span>:9080"</span> - <span>"127.0.0.1:<span>${AUTHENTIK_PORT_HTTPS:-9443}</span>:9443"</span> worker: image: <span>${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}</span>:<span>${AUTHENTIK_TAG:-2023.10.2}</span> restart: unless-stopped command: worker environment: AUTHENTIK_REDIS__HOST: redis AUTHENTIK_POSTGRESQL__HOST: postgresql AUTHENTIK_POSTGRESQL__USER: <span>${PG_USER:-authentik}</span> AUTHENTIK_POSTGRESQL__NAME: <span>${PG_DB:-authentik}</span> AUTHENTIK_POSTGRESQL__PASSWORD: <span>${PG_PASS}</span> AUTHENTIK_ERROR_REPORTING__ENABLED: <span>"true"</span> user: root volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>media:<span>/</span>media - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>certs:<span>/</span>certs - <span>/</span>var<span>/</span>run<span>/</span>docker.sock:<span>/</span>var<span>/</span>run<span>/</span>docker.sock - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>custom-templates:<span>/</span>templates env_file: - .env</pre></div></div> <p>One you've got that saved, there's just one thing left! You just need to set up your specific configuration file, which is held in a 'hidden' file, <code>.env</code> in <code>/home/docker/[domain name]</code>.</p> <p>This is what you should put in it, replacing [tokens]. Note that you can comment out any line you want by putting a '#' at the start of the line.</p> <p>Note that if you set the AUTHENTIK_TAG below and uncomment the line (remove the #) you won't (normally) need to update the <code>docker-compose.yml</code> file to apply updates to Authentik in future.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># AUTHENTIK_TAG=2023.10.2</span> <span>PG_PASS</span>=<span>[</span>postgresql password<span>]</span> <span>AUTHENTIK_SECRET_KEY</span>=<span>[</span>authentik secret<span>]</span> <span>AUTHENTIK_ERROR_REPORTING__ENABLED</span>=<span>true</span> <span># SMTP Host Emails are sent to</span> <span>AUTHENTIK_EMAIL__HOST</span>=<span>[</span>smtp host<span>]</span> <span>AUTHENTIK_EMAIL__PORT</span>=<span>[</span>smtp port<span>]</span> <span># Optionally authenticate (don't add quotation marks to your password)</span> <span>AUTHENTIK_EMAIL__USERNAME</span>=<span>[</span>smtp username<span>]</span> <span>AUTHENTIK_EMAIL__PASSWORD</span>=<span>[</span>smtp password<span>]</span> <span># Use StartTLS</span> <span>AUTHENTIK_EMAIL__USE_TLS</span>=<span>true</span> <span># Use SSL</span> <span>AUTHENTIK_EMAIL__USE_SSL</span>=<span>false</span> <span>AUTHENTIK_EMAIL__TIMEOUT</span>=<span>10</span> <span># Email address authentik will send from, should have a correct @domain</span> <span>AUTHENTIK_EMAIL__FROM</span>=<span>[</span>smtp from email<span>]</span> <span>AUTHENTIK_PORT_HTTP</span>=<span>9080</span> <span>AUTHENTIK_PORT_HTTPS</span>=<span>9443</span></pre></div></div> <p>Once that's done and saved, you can start your Authentik service!</p> <p><code>docker-compose up -d &amp;&amp; docker-compose logs -f</code></p> <p>This will trigger Docker to download the container images specified in your <code>docker-compose.yml</code> (based on AUTHENTIK_TAG), and start them up. That, in turn, will cause Docker Compose to create the directories specified in your <code>volumes:</code> stanzas in your <code>docker-compose.yml</code> file in <code>/home/data/[domain name]</code>, including creating your PostgreSQL database.</p> <h2><a id="user-content-boom-setting-up-your-authentik-instance" href="#boom-setting-up-your-authentik-instance" name="boom-setting-up-your-authentik-instance" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Boom: Setting up your Authentik instance</h2> <p>Once that is up and running, you should be able to point your browser at</p> <p><code>https://[domain name]/if/flow/initial-setup/</code></p> <p>Here's more <a href="https://goauthentik.io/docs/">documentation</a> to help you from here. Also, <a href="https://www.youtube.com/watch?v=Nh1qiqCYDt4&amp;list=PLH73rprBo7vSkDq-hAuXOoXx2es-1ExOP"><strong>cooptonian</strong>'s YouTube Channel</a> is most helpful!</p> <h2><a id="user-content-upgrading-your-authentik" href="#upgrading-your-authentik" name="upgrading-your-authentik" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Upgrading your Authentik</h2> <p>Periodically, you'll see in the admin interface (or via an email to the site's administrators) that your Authenitik can be upgraded to a newer version.</p> <p>Before doing anything to upgrade, I encourage you to make a copy of both your key files:</p> <p><code>DATE=$(date +%d-%m-%Y</code>)<code> </code>cp docker-compose.yml docker-compose.yml-$DATE<code> </code>cp .env .env-$DATE`</p> <p>because having them handy might save your bacon if, for example, you need to revert back to your old configuration.</p> <p>Sometimes the Authentik developers change the configuration of their default <code>docker-compose.yml</code> file. You'll want to verify that your version is the same <em>except for any changes to <code>volumes:</code> you've made</em> as follows:</p> <p>Download the current reference version as <code>docker-compose.yml-default</code> (so it doesn't overwrite your <code>docker-compose.yml</code>):</p> <p><code>wget -O docker-compose.yml-default https://goauthentik.io/version/2023.10/docker-compose.yml</code></p> <p>Then, you'll want to run</p> <p><code>diff docker-compose.yml-default docker-compose.yml</code></p> <p>to see how the new file differs. If it differs in areas other than <code>volumes:</code> and the value for <code>AUTHENTIK_TAG</code>, you'll want to apply those changes to your <code>docker-compose.yml</code> via</p> <p><code>$EDIT docker-compose.yml</code></p> <p>Also, because upgrades might well result in a change to your PostgreSQL database's schema, you'll want a full backup of the existing database!</p> <p>Once you have that, you can run</p> <p><code>docker-compose pull &amp;&amp; docker-compose up -d &amp;&amp; docker-compose logs -f</code></p> <p>to pull the new containers, run them, and watch the logs of the upgrade as it progresses and, hopefully, completes successfully.</p> <h2><a id="user-content-backing-up-your-authentik-data" href="#backing-up-your-authentik-data" name="backing-up-your-authentik-data" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Backing up your Authentik data</h2> <p>At the OER Foundation, we recognise that our Authentik is pretty mission-critical. To protect our users' data integrity and <em>privacy</em>, we do nightly encrypted incremental file backups and hourly PostgreSQL database backups. For the former, we use a <a href="https://restic.net">Restic</a> remote server <a href="https://git.oeru.org/dave/restic-backup">script</a> we've written (which requires that you have either sufficient local storage, or (preferably) another server you can access via SSH which has sufficient disk space to hold versioned backups), and the latter, we use a <a href="https://git.oeru.org/oeru/docker-compose-dbbackup">backup script we've developed</a>.</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=60&amp;2=field_blog_comments&amp;3=comment" token="gczezEir2FTP_q4_d-D-ocoNHXv3-ZNo43C6OApzx08"></drupal-render-placeholder> </div> </section> Mon, 23 Oct 2023 22:15:36 +0000 dave 60 at http://tech.oeru.org From FOSS to LibreSoftware - it's about clarity and values http://tech.oeru.org/foss-libresoftware-its-about-clarity-and-values <span class="field field--name-title field--type-string field--label-hidden">From FOSS to LibreSoftware - it&#039;s about clarity and values</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--foss"> <span class="field__item-wrapper"><a href="/taxonomy/term/10" hreflang="en">foss</a></span> </div> <div class="field__item field__item--libre-software"> <span class="field__item-wrapper"><a href="/taxonomy/term/86" hreflang="en">libre software</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:18</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>Any reader of this site will notice that we often talk about 'Free and Open Source Software' which we usually abbreviate as FOSS. For those who aren't intimately familiar with the history, trajectory, and nuances (warning - they are <em>big</em> topics) of the <a href="https://fsf.org">Free Software</a> and <a href="https://opensource.com">Open Source Software</a> camps - both subsets of information technology, itself a subset of digital technology - its significance is both arcane and something of a barrier to understanding.</p> <p>Free Software has always had the very unfortunate ambiguity in the English-speaking world due to the word "Free" having two major meanings: free as in liberty or freedom or speech, or free as in gratis or no-cost. It seems that for most, they focus on the latter meaning and miss the point of the name. It's a constant battle to explain it to people ('free as in freedom, not free as in beer" is what you'll often hear). But, as any marketer will tell you, if you have to explain it, it's a failed band.</p> <p>To be clear, even though most 'Free Software' is available at no cost, that is coincidental. The word Free in this case does not refer to cost, but rather the freedoms available to the <em>users</em> of the software. It is quite possible for 'Free Software' to be both 'commercial', and available at a cost, taking into account that any user has the freedom to distribute the source code at any price they want, including gratis.</p> <p>The term "open source" is better marketing, in that it describes some relatively unambiguous properties of software - that the source code is available for others to view and, usually, do other things with, with various strings attached, depending on the perspective of the onlooker - but it fails to connote any of the philosophical values held by those who engage in creating, maintaining, and using it. It describes a few properties of a software project, but it doesn't offer insight into <em>why</em> that software is open source, which many of us think is crucially important.</p> <p>In light of these short comings of both Free and Open Source Software as terms to describe what is important to us at the OER Foundation, we have started referring to 'LibreSoftware' instead. Even though, in the Angelosphere, schools are rapidly (but foolishly, in my opinion) decommissioning their Latin departments, most English-speakers have a passing familiarity with the world 'libre' - the root of liberty. It doesn't have the ambiguities of 'free', and it succinctly puts the affordances of 'Free Software' front-and-centre. It also brings us English-speakers in line with many other cultures with romance languages, where libre is well understood. So that's why we've started talking about LibreSoftware instead of FOSS.</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=58&amp;2=field_blog_comments&amp;3=comment" token="QiGPZGGeCYluDq8dboRcz4UM4ufJWFq1qSfCRzjEhc0"></drupal-render-placeholder> </div> </section> Thu, 12 Oct 2023 00:18:46 +0000 dave 58 at http://tech.oeru.org 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 Updating OER Foundation Web Services for February 2023 http://tech.oeru.org/updating-oer-foundation-web-services-february-2023 <span class="field field--name-title field--type-string field--label-hidden">Updating OER Foundation Web Services for February 2023</span> <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 02/02/2023 - 15:09</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>It's been about six months since my <a href="/node/53">last update</a> and, wow, a lot has changed. The <a href="https://oerfoundation.org">OER Foundation</a> (OERF) has embarked on a new initiative inspired by our old friend the Fediverse and the ramifications to on our sustainability thanks to our newer nemesis, Covid19: the <a href="https://fossdle.org">Free and Open Source Software (FOSS) Digital Learning Ecosystem (DLE)</a>. Here's a paper we wrote which describes the initial <a href="https://www.irrodl.org/index.php/irrodl/article/view/5763/5600">FOSSDLE concept</a> in detail, including technologies, costs, and a case study. But the FOSSDLE concept is evolving rapidly.</p> <p>Thanks to increasing inequity (with the haves getting more, and have-nots less) and the prospects for addressing that imbalance (education, especially for women in the developing world), we need to do something new and different. Something new - to most folks at least - is the relatively rapid rise from obscurity of the '<a href="https://fediverse.info">Fediverse</a>', coincident with Elon Musk taking over Twitter. For those unfamiliar with the term Fediverse (a <a href="https://en.wikipedia.org/wiki/Portmanteau">portmanteau</a> of 'Federated Universe'), it refers to a family of independently developed decentralised FOSS social media technologies, instances of which are 'federated' (link and talk to one another) by means of automated data transfer that adheres to an open standard called "<a href="https://en.wikipedia.org/wiki/ActivityPub">ActivityPub</a>". We have taken note of the diverse range of applications making up the Fediverse, and the power of being able to view the digital world through each of those lenses, on ones own terms. We think that the pedagogical opportunities the Fediverse represents are rich and varied.</p> <p>In addition to running a wide set of Fediverse-enabled technologies, we've also been working to improve our measurement and monitoring game - ensuring that our services are looked after, so we're the first to know when there's a problem. We also want to be able to measure our impact in a way that's compatible with user freedom and privacy... so that's resulted in still more new services.</p> <p>Finally, we've identified a number of new FOSS technologies we consider to be better than those we previously considered 'best-of-breed', and so, in the 'rapid evolution' approach of FOSS technologies, we've implemented them and made the move.</p> <p>Here's where things stand now: in February 2023, we run the following production systems:</p> <p>For the FOSSDLE Commons, we have:</p> <p>A series of Fediverse apps:</p> <ul><li>a <a href="https://wordpress.org">Wordpress</a> <a href="https://wordpress.org/support/article/create-a-network/">Multisite</a> - <a href="https://fossdle.org">https://fossdle.org</a> - our main site, with subsites for related purposes, like support and other documentation. Posts are shared via the Fediverse using the Wordpress <a href="https://wordpress.org/plugins/activitypub/">ActivityPub plugin</a>. Wordpress, a vastly extensible FOSS blogging engine, is the single, most widely use platform on the World Wide Web - <a href="https://kinsta.com/wordpress-market-share/">43% of <em>all websites</em> and 65% of websites that use a Content Management System (CMS)</a> are built on Wordpress. The next two closest web platforms are also FOSS - Joomla (4.6% of CMSs) and Drupal (3%). Wordpress is unrivalled.</li> <li>a <a href="https://joinmastodon.org">Mastodon</a> instance - <a href="https://social.fossdle.org">https://social.fossdle.org</a> - superficially similar to Twitter, but decentralised and open and many other subtle (but important) differences.</li> <li>a <a href="https://pixelfed.org">PixelFed</a> - <a href="https://pic.fossdle.org">https://pic.fossdle.org</a> - similar to Instagram.</li> <li>a <a href="https://join-lemmy.org/">Lemmy</a> instance - <a href="https://link.fossdle.org">https://link.fossdle.org</a> - a link aggregator similar to Reddit.</li> <li>an <a href="https://owncast.online/">OwnCast</a> instance - <a href="https://stream.fossdle.org">https://stream.fossdle.org</a> - a streaming platform like Twitch, but streams and viewer comments are Fediverse content.</li> <li>a <a href="https://writefreely.org/">WriteFreely</a> instance - <a href="https://write.fossdle.org">https://write.fossdle.org</a> - a no-nonsense blogging system similar to a decentralised Fediverse-enabled Medium.</li> </ul><p>In the near term, we plan to add</p> <ul><li>a <a href="https://joinpeertube.org/">PeerTube</a> instance - a distributed YouTube-like video hosting service (update: <a href="https://stream.fossdle.org">stream.fossdle.org</a>).</li> <li>a <a href="https://joinmobilizon.org/">Mobilizon</a> instance - a distributed Event management and registration service (update: <a href="https://events.oeru.org">events.oeru.org</a>).</li> </ul><p>We have also added a number of non-Fediverse apps to support the FOSSDLE Commons initiative.</p> <ul><li>a <a href="https://matrix.org">Matrix</a> server - <a href="https://matrix.fossdle.org">https://matrix.fossdle.org</a>, with an <a href="https://element.io">Element</a> client - <a href="https://chat.fossdle.org">https://chat.fossdle.org</a> - Matrix is a hugely powerful open standard for defining 'chat' applications, and it's also a reference implementation of that standard. It's a fully FOSS alternative to Slack or MS Teams, and integrates with IRC and just about anything else under the sun.</li> <li>an <a href="https://goauthentik.io">Authentik</a> instance - <a href="https://auth.fossdle.org">https://auth.fossdle.org</a> - an authentication and authorisation platform for Single Sign-On.</li> <li>an instance of <a href="https://hedgedoc.org">HedgeDoc</a> - <a href="https://md.fossdle.org">https://md.fossdle.org</a> - a collaborative editing environment for MarkDown content.</li> <li>a <a href="https://wekan.github.io/">Wekan</a> instance - <a href="https://kanban.fossdle.org">https://kanban.fossdle.org</a> - a Kanban project planning and management board similar to Trello.</li> <li>a <a href="/node/25">(Bit|Vault)Warden</a> - <a href="https://safe.fossdle.org">https://safe.fossdle.org</a> - a full-featured password manager like LastPass (but secure) and 1Password, et al.</li> <li>a <a href="https://nextcloud.com">NextCloud</a> instance, with an integrated <a href="https://onlyoffice.com">OnlyOffice</a> instance - <a href="https://hub.fossdle.org">https://hub.fossdle.org</a> - for file sharing and collaborative project work. Like Google Drive, Dropbox, or MS OneDrive.</li> <li>to support our NextCloud, we have integrated an <a href="https://onlyoffice.com">OnlyOffice</a> instance. Together, they behave like a feature-rich Google Suite, but refreshingly Google-free - <a href="https://onlyoffice.oeru.org">https://onlyoffice.oeru.org</a> </li> <li>a <a href="https://discourse.org">Discourse</a> instance - <a href="https://forum.fossdle.org">https://forum.fossdle.org</a> - Discourse is the market leading, modern, full-featured (and fully FOSS) forum.</li> </ul><p>In addition to these FOSSDLE-supporting services, we continue to maintain the following for the OERu and partners:</p> <ul><li>two more <a href="https://wordpress.org/support/article/create-a-network/">WordPress Multisites</a>: <a id="user-content-our-main-course-delivery-site-where-each-course-is-a-subsite---httpscourseoeruorg" href="#our-main-course-delivery-site-where-each-course-is-a-subsite---httpscourseoeruorg" name="our-main-course-delivery-site-where-each-course-is-a-subsite---httpscourseoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our main course delivery site, where each course is a 'subsite' - <a href="https://course.oeru.org">https://course.oeru.org</a> <a id="user-content-our-train-the-educators-blog-and-sandbox-course-site-each-educator-gets-a-blog-subsite-for-their-own-use-and-a-course-sandbox-subsite---httpsedt4oloeruorg" href="#our-train-the-educators-blog-and-sandbox-course-site-each-educator-gets-a-blog-subsite-for-their-own-use-and-a-course-sandbox-subsite---httpsedt4oloeruorg" name="our-train-the-educators-blog-and-sandbox-course-site-each-educator-gets-a-blog-subsite-for-their-own-use-and-a-course-sandbox-subsite---httpsedt4oloeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our train-the-educators blog and sandbox course site. Each educator gets a 'blog' subsite for their own use, and a 'course sandbox' subsite - <a href="https://edt4ol.oeru.org">https://edt4ol.oeru.org</a> </li> <li>a trio of standalone <a href="https://wordpress.org">WordPress</a> instances for the OERF and related initiatives: <a id="user-content-the-oerfs-own-organisational-website---httpsoerfoundationorg" href="#the-oerfs-own-organisational-website---httpsoerfoundationorg" name="the-oerfs-own-organisational-website---httpsoerfoundationorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the OERF's own organisational website - <a href="https://oerfoundation.org">https://oerfoundation.org</a> <a id="user-content-the-centre-for-open-educational-practice-coep---httpscoepnz" href="#the-centre-for-open-educational-practice-coep---httpscoepnz" name="the-centre-for-open-educational-practice-coep---httpscoepnz" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the Centre for Open Educational Practice (COEP) - <a href="https://coep.nz">https://coep.nz</a> <a id="user-content-the-oerfs-initiative-to-help-educators-in-the-developing-world-cope-with-the-challenges-of-covid19---httpsoer4covidoeruorg" href="#the-oerfs-initiative-to-help-educators-in-the-developing-world-cope-with-the-challenges-of-covid19---httpsoer4covidoeruorg" name="the-oerfs-initiative-to-help-educators-in-the-developing-world-cope-with-the-challenges-of-covid19---httpsoer4covidoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the OERF's initiative to help educators in the developing world cope with the challenges of COVID19 - <a href="https://oer4covid.oeru.org">https://oer4covid.oeru.org</a> </li> <li>a pair of <a href="https://drupal.org">Drupal</a> sites (they've been upgraded to Drupal 9.x) <a id="user-content-the-oerfs-information-tech-site-this-site---httpstechoeruorg-and" href="#the-oerfs-information-tech-site-this-site---httpstechoeruorg-and" name="the-oerfs-information-tech-site-this-site---httpstechoeruorg-and" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the OERF's information tech site (this site!) - <a href="https://tech.oeru.org">https://tech.oeru.org</a>, and <a id="user-content-our-h5p-learning-object-builder-site---httpsh5poeruorg" href="#our-h5p-learning-object-builder-site---httpsh5poeruorg" name="our-h5p-learning-object-builder-site---httpsh5poeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our H5P learning object builder site - <a href="https://h5p.oeru.org">https://h5p.oeru.org</a> </li> <li>another <a href="https://www.discourse.org/">Discourse</a> instance - Discourse is the market leading, modern, full-featured (and fully FOSS) forum: <a id="user-content-our-companion-site-for-folks-taking-oeru-courses-for-both-assignments-and-collaboration-with-other-learners---httpsforumsoeruorg" href="#our-companion-site-for-folks-taking-oeru-courses-for-both-assignments-and-collaboration-with-other-learners---httpsforumsoeruorg" name="our-companion-site-for-folks-taking-oeru-courses-for-both-assignments-and-collaboration-with-other-learners---httpsforumsoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our companion site for folks taking OERu courses for both assignments and collaboration with other learners - <a href="https://forums.oeru.org">https://forums.oeru.org</a> <a id="user-content-note-we-redeployed-our-underutilised-our-oeru-community-collaboration-and-support-site-for-educators-as-the-fossdle-forum-above" href="#note-we-redeployed-our-underutilised-our-oeru-community-collaboration-and-support-site-for-educators-as-the-fossdle-forum-above" name="note-we-redeployed-our-underutilised-our-oeru-community-collaboration-and-support-site-for-educators-as-the-fossdle-forum-above" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Note: we redeployed our underutilised our OERu community collaboration and support site for educators as the FOSSDLE forum above. </li> <li>another <a href="https://nextcloud.com">NextCloud</a> for file sharing and many other uses. It's our main collaborative document management system and collaborative authorship and sharing media and documents - <a href="https://docs.oeru.org">https://docs.oeru.org</a> </li> <li>a <a href="https://bigbluebutton.org">BigBlueButton</a> instance - a large scale real-time video conferencing and educational collaboration platform - <a href="https://talk.oeru.org">https://talk.oeru.org</a> </li> <li>a <a href="https://github.com/mautic/mautic">Mautic</a> high-powered mailing list automation management system - <a href="https://mautic.oeru.org">https://mautic.oeru.org</a> </li> <li>a <a href="https://rocket.chat">Rocket.Chat</a> instance - a rich chat service comparable to Slack or Discord but fully open source (and we hold all our own data) - <a href="https://chat.oeru.org">https://chat.oeru.org</a> - Rocket.Chat, though very good, is not nearly as powerful or secure as Matrix, so we have shifted most of our activities to Matrix and Element, which also offers more mature mobile apps.</li> <li>another <a href="https://joinmastodon.org">Mastodon</a> instance - Twitter-esque but de-centralised and non-commercial federated open source social network (part of the <a href="https://en.wikipedia.org/wiki/Fediverse">Fediverse</a>) - <a href="https://mastodon.oeru.org">https://mastodon.oeru.org</a> </li> <li>a <a href="https://mailcow.email">Mailcow</a> instance for managing all our email - Mailcow is a full multi-tenanted SMTP/IMAP/POP email system with active spam filtering, user self-service, DKIM configuration/management, webmail, &amp; much more - even virus scanning for Windows clients - <a href="https://about.oerfoundation.org">https://about.oerfoundation.org</a> - it supplies email services for the oeru.org, oerfoundation.org, coep.nz, and fossdle.org domains, among others!</li> <li>an instance of <a href="https://yourls.org">YourLS</a> - a link shortener - <a href="https://oer.nz">https://oer.nz</a> </li> <li>a <a href="https://matomo.org">Matomo</a> instance - website analytics (like Google Analytics, but without infringing on the EU's <a href="https://gdpr.eu/what-is-gdpr/">GDPR legislation</a>) - <a href="https://stats.oeru.org">https://stats.oeru.org</a> </li> <li>we added a <a href="https://plausible.io">Plausible</a> instance - another very nice FOSS website analytics system which uses different technologies and is similarly privacy-respecting to Matomo. It also serves to provide a check of our Matomo analytics - <a href="https://stats.fossdle.org">https://stats.fossdle.org</a> </li> <li>a <a href="https://mahara.org">Mahara</a> instance - online academic portfolio tool - <a href="https://portfolio.oerfoundation.org">https://portfolio.oerfoundation.org</a> </li> <li>a <a href="https://moodle.org">Moodle</a> instance - market leading learning management system which we use primarily for offering quizzes and awarding certificates for participants - <a href="https://moodle.oeru.org">https://moodle.oeru.org</a> </li> <li>a <a href="https://silverstripe.com">SilverStripe</a> instance - our main OERu website - <a href="https://oeru.org">https://oeru.org</a> </li> <li>a <a href="https://mediawiki.org">MediaWiki</a> instance - collaboration platform for OER development built on the same platform on which Wikipedia is built. All our courses are developed here - <a href="https://wikieducator.org">https://wikieducator.org</a> </li> <li>a <a href="https://gitlab.com/rluna-gitlab/gitlab-ce">GitLab</a> - a software developer version control/collaboration tool where we make <em>all</em> our code available - <a href="https://git.oeru.org">https://git.oeru.org</a> </li> <li>a <a href="https://limesurvey.com">LimeSurvey</a> instance - survey tool - <a href="https://survey.oeru.org">https://survey.oeru.org</a> </li> <li>another <a href="https://bitwarden.com">(BitWarden</a>/VaultWarden](<a href="https://github.com/dani-garcia/vaultwarden">https://github.com/dani-garcia/vaultwarden</a>) instance - password manager and cross-device sync service - <a href="https://safe.oeru.org">https://safe.oeru.org</a> (here's our <a href="/node/25">howto for hosting your own</a>)</li> <li>an instance of <a href="https://sourceforge.net/projects/semanticscuttle">Semantic Scuttle</a> - a collaborative public website bookmarking service - <a href="https://bookmarks.oeru.org">https://bookmarks.oeru.org</a> </li> <li>another Wekan instance - for project planning via the Kanban methodology. Similar to Trello (among other tools) - <a href="https://kanban.oeru.org">https://kanban.oeru.org</a>.</li> <li>a Mobilizon instance - for managing events, and people following, discussing, and signing-up for and attending them - <a href="https://events.oeru.org">https://events.oeru.org</a> </li> <li>a <a href="https://rustdesk.com">RustDesk</a> server instance, allowing us to provide anyone anywhere on just about any computing platform (Linux, Windows, MacOS, iOS, and Android) with live interactive shared desktop support. Equivalent to TeamViewer.</li> <li>a <a href="https://github.com/PagerTree/prometheus-grafana-alertmanager-example">server monitoring solution</a> based on Grafana, Prometheus, Node-exporter, Alertmanager, cAdvisor, and other tools, which provide a graphical monitoring solution for each of our servers.</li> <li>we recently added an instance of <a href="https://github.com/louislam/uptime-kuma">Uptime Kuma</a>, a superb system for periodically (i.e. every minute) checking that all of our services are doing what they ought to be. It alerts us via Matrix integration if any service is misbehaving based on a variety of parameters. It also provides a pretty dashboard with green, orange, and (occasionally) red lights. It also allows us to monitor each service's performance over time.</li> </ul><p>We also maintain development and testing/staging instances of most of these services. We do frequent (daily or, in some cases, hourly) database backups and daily remote incremental encrypted filesystem backups onto a server with a LOT of storage. Aside from one co-located physical server with large disk capacity, our services are all hosted on virtual Linux servers (we mostly run Ubuntu Linux) provided by commodity cloud hosting providers via <a href="https://docs.docker.com/engine/install/ubuntu/">Docker</a> and orchestrated via <a href="https://docs.docker.com/compose/install/">Docker Compose</a>.</p> <h2><a id="user-content-hosting-on-behalf" href="#hosting-on-behalf" name="hosting-on-behalf" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Hosting on behalf</h2> <p>We continue to host services on behalf of other organisations, including <a href="https://col.org">Commonwealth of Learning</a>, <a href="https://oeglobal.org">Open Education Global</a>, and the government of Samoa, in particular their <a href="https://mesc.ws">Ministry of Education, Sport, and Culture</a>'s Innovative Lifelong Learning Lab (the MiLLL).</p> <p>For the MiLLL project:</p> <ul><li>a pair of WordPress Multisites, one for hosting open educational resource-based course sites - <a href="https://course.milll.ws">https://course.milll.ws</a> - and one hosting individual sites for Samoan primary and secondary schools - <a href="https://schools.milll.ws">https://schools.milll.ws</a> </li> <li>a BigBlueButton instance supporting learners, educators, and government staff's video conferencing requirements - <a href="https://bbb.milll.ws">https://bbb.milll.ws</a> </li> <li>a Mastodon instance especially for Samoans wanting to participate in digital social media - <a href="https://mastodon.milll.ws">https://mastodon.milll.ws</a> </li> <li>an instance of Rocket.Chat - <a href="https://chat.milll.ws">https://chat.milll.ws</a> </li> <li>an instance of Discourse - <a href="https://forum.milll.ws">https://forum.milll.ws</a> </li> <li>an instance of Moodle - <a href="https://moodle.milll.ws">https://moodle.milll.ws</a> </li> <li>an instance of BitWardern/VaultWarden - <a href="https://safe.milll.ws">https://safe.milll.ws</a> </li> </ul><p>For the <a href="https://col.org">Commonwealth of Learning</a>:</p> <ul><li>a WordPress Multisite for hosting OER-based course sites for the <a href="https://pacificpartnership.col.org/">Pacific Partnership in Open Distance and Flexible Learning initiative</a> - <a href="https://pacificopencourses.col.org">https://pacificopencourses.col.org</a> </li> </ul><p>For <a href="https://oeglobal.org">Open Education Global</a>:</p> <ul><li>a WordPress Multisite for hosting OER-based course sites - <a href="https://course.oeglobal.org">https://course.oeglobal.org</a> </li> </ul><h2><a id="user-content-costs-and-usage" href="#costs-and-usage" name="costs-and-usage" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Costs and Usage</h2> <p>In the past year we have served tens of thousands of registered users and over 200,000 anonymous learners access our courses (the full content of which are open to all without requiring authentication).</p> <p>It's also important to note that all of these services can be provided at no cost to our collaborators and learners as there are <i>no</i> per-seat license fees (nor <i>any</i> license fees) associated with any of the services. Our only costs are related to the <strong>14 (at last count)</strong> generic 'cloud'-based Virtual Private Servers (VPSs) (all running a FOSS Linux operating system) we hire from a host of competing commodity hosting providers including Digital Ocean, Hetzner, and Linode. We actively avoid using any vendor-specific proprietary services so that we can shift providers with minimal hassle and service interruption if required.</p> <p>Our entire annual IT infrastructure cost, including for our 'on behalf' hosting partners, is now hovering around USD10,000 per year for all of the above. What's more our usage monitoring suggests that most of our servers were operating well below 50% capacity, meaning that we have a plenty of additional headroom.</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=55&amp;2=field_blog_comments&amp;3=comment" token="rOz4417okIuPCWRMzi5SyyVAUFrrwZuYv9Fqu-mTP7I"></drupal-render-placeholder> </div> </section> Thu, 02 Feb 2023 02:09:50 +0000 dave 55 at http://tech.oeru.org Join the Fediverse: installing Mastodon 4.0 on Ubuntu 22.04 with Docker Compose http://tech.oeru.org/join-fediverse-installing-mastodon-40-ubuntu-2204-docker-compose <span class="field field--name-title field--type-string field--label-hidden">Join the Fediverse: installing Mastodon 4.0 on Ubuntu 22.04 with Docker Compose</span> <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 04/11/2022 - 10:31</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-11/Screenshot%202022-11-15%20at%2012-05-24%20FOSSDLE%20Social%20-%20Mastodon.png?itok=QBrCpIpr" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A look at the front page of a (nearly) new Mastodon Instance with the default &#039;dark&#039; theme. In this case, it&#039;s https://social.fossdle.org...&quot;}" role="button" title="A look at the front page of a (nearly) new Mastodon Instance with the default &#039;dark&#039; theme. In this case, it&#039;s https://social.fossdle.org..." data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A look at the front page of a (nearly) new Mastodon Instance with the default &#039;dark&#039; theme. In this case, it&#039;s https://social.fossdle.org...&quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-05-24%20FOSSDLE%20Social%20-%20Mastodon.png?itok=JYOJOjky" width="220" height="169" alt="A look at the front page of a (nearly) new Mastodon Instance with the default &#039;dark&#039; theme. In this case, it&#039;s https://social.fossdle.org..." 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-11/Screenshot%202022-11-15%20at%2012-02-12%20FOSSDLE%20Social%20-%20Mastodon.png?itok=6dlYxyFJ" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A look at the front page of a (nearly) new Mastodon Instance, configured to use the &#039;light&#039; theme. In this case, it&#039;s https://social.fossdle.org...&quot;}" role="button" title="A look at the front page of a (nearly) new Mastodon Instance, configured to use the &#039;light&#039; theme. In this case, it&#039;s https://social.fossdle.org..." data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A look at the front page of a (nearly) new Mastodon Instance, configured to use the &#039;light&#039; theme. In this case, it&#039;s https://social.fossdle.org...&quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-02-12%20FOSSDLE%20Social%20-%20Mastodon.png?itok=eHKILHrW" width="220" height="170" alt="A look at the front page of a (nearly) new Mastodon Instance, configured to use the &#039;light&#039; theme. In this case, it&#039;s https://social.fossdle.org..." 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-11/Screenshot%202022-11-15%20at%2012-18-29%20Dashboard%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=PCAgBKBF" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Mastodon Admin dashboard (note, this is with the &#039;light&#039; theme already enabled on a nearly new instance)... &quot;}" role="button" title="The Mastodon Admin dashboard (note, this is with the &#039;light&#039; theme already enabled on a nearly new instance)... " data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Mastodon Admin dashboard (note, this is with the &#039;light&#039; theme already enabled on a nearly new instance)... &quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-18-29%20Dashboard%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=IeeiQUff" width="135" height="220" alt="The Mastodon Admin dashboard (note, this is with the &#039;light&#039; theme already enabled on a nearly new instance)... " 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-11/Screenshot%202022-11-15%20at%2012-20-30%20Site%20settings%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=SV_x03Rz" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The Admin Settings page showing the settings we use on the social.fossdle.org site.&quot;}" role="button" title="The Admin Settings page showing the settings we use on the social.fossdle.org site." data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The Admin Settings page showing the settings we use on the social.fossdle.org site.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-20-30%20Site%20settings%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=h9Q7qf_Q" width="79" height="220" alt="The Admin Settings page showing the settings we use on the social.fossdle.org site." 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-11/Screenshot%202022-11-15%20at%2012-21-54%20Server%20rules%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=uIM1EgdA" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The &#039;house rules&#039; on the social.fossdle.org instance. &quot;}" role="button" title="The &#039;house rules&#039; on the social.fossdle.org instance. " data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The &#039;house rules&#039; on the social.fossdle.org instance. &quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-21-54%20Server%20rules%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=KypnXkLm" width="220" height="216" alt="The &#039;house rules&#039; on the social.fossdle.org instance. " 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-11/Screenshot%202022-11-15%20at%2012-27-03%20FOSSDLE%20Social%20-%20Mastodon.png?itok=fFWccbxW" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;This is the default Mastodon instance view (with the &#039;light&#039; theme enabled). &quot;}" role="button" title="This is the default Mastodon instance view (with the &#039;light&#039; theme enabled). " data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;This is the default Mastodon instance view (with the &#039;light&#039; theme enabled). &quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-27-03%20FOSSDLE%20Social%20-%20Mastodon.png?itok=HH1hi_Bs" width="188" height="220" alt="This is the default Mastodon instance view (with the &#039;light&#039; theme enabled). " 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-11/Screenshot%202022-11-15%20at%2012-23-34%20Appearance%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=4YNlpOSK" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The personal preferences page, where you can (dis|e)nable the &#039;advanced web interface&#039; as well as set your preferred theme - &#039;light&#039; for me in this case.&quot;}" role="button" title="The personal preferences page, where you can (dis|e)nable the &#039;advanced web interface&#039; as well as set your preferred theme - &#039;light&#039; for me in this case." data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The personal preferences page, where you can (dis|e)nable the &#039;advanced web interface&#039; as well as set your preferred theme - &#039;light&#039; for me in this case.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-23-34%20Appearance%20-%20FOSSDLE%20Social%20-%20Mastodon.png?itok=syh1t4BJ" width="199" height="220" alt="The personal preferences page, where you can (dis|e)nable the &#039;advanced web interface&#039; as well as set your preferred theme - &#039;light&#039; for me in this case." 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-11/Screenshot%202022-11-15%20at%2012-25-20%20FOSSDLE%20Social%20-%20Mastodon.png?itok=38wCJ2nA" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;My view of the &#039;advanced&#039; Mastodon interface. &quot;}" role="button" title="My view of the &#039;advanced&#039; Mastodon interface. " data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;My view of the &#039;advanced&#039; Mastodon interface. &quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-25-20%20FOSSDLE%20Social%20-%20Mastodon.png?itok=mdWbu_5f" width="220" height="185" alt="My view of the &#039;advanced&#039; Mastodon interface. " 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-11/Screenshot%202022-11-15%20at%2012-36-26%20Explore%20FOSSDLE%20Social%20-%20Mastodon.png?itok=hRPyCvJz" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;The current &#039;profile directory&#039; of publicly listed users on social.fossdle.org.&quot;}" role="button" title="The current &#039;profile directory&#039; of publicly listed users on social.fossdle.org." data-colorbox-gallery="gallery-field_image-Sr1Ev5-eszU" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;The current &#039;profile directory&#039; of publicly listed users on social.fossdle.org.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-11/Screenshot%202022-11-15%20at%2012-36-26%20Explore%20FOSSDLE%20Social%20-%20Mastodon.png?itok=U23oYyXu" width="188" height="220" alt="The current &#039;profile directory&#039; of publicly listed users on social.fossdle.org." 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>In the past week or two, with Elon Musk completing his purchase and take-over of Twitter, there has been a torrent of defections from the centralised, closed, for-profit platform to its antithesis, the <a href="https://en.wikipedia.org/wiki/Fediverse">Fediverse</a> - it's a portmanteau of "federation" and "universe". The Fediverse, in short, is a <a href="https://fediverse.party">set of Free and Open Source Software (FOSS) applications</a> created by different quite independent communities of developers, sharing one thing in common: the (somewhat magical) <a href="https://activitypub.rocks/">ActivityPub</a> <a href="https://www.w3.org/TR/activitypub/">open standard</a> for data characterisation and exchange. They are deployed independently by interested individuals and communities to form a 'decentralised network' of ActivityPub-enabled services (<a href="https://fediverse.info/">here's a quick explanation video</a>). Of these many applications (with new ones popping up every day!), probably the most ambitious and widely known is <a href="https://joinmastodon.org">Mastodon</a> (which also features <a href="https://peertube.togart.de/videos/watch/7a087e5f-68e0-4359-8035-bfd9752e9b2e">a short video explanation</a>).</p> <p>For those not (yet) familiar with it, Mastodon is a 'micro-blogging' platform superficially somewhat similar to Twitter, but different in many important details. It is also a collection of FOSS applications, notably an 'instance', aka a server (the subject of this how-to), and a variety of 'clients' that support people interacting with one or more instances. A user installs a client on their phone or desktop, or simply uses a web browser, opening the instance's web address (URL) in a tab. Either the tab or the client becomes their portal for engaging with the Mastodon instance and the various ActivityPub streams the user selects to see via that instance. That selection is done via 'following' other users on the same or different instances, whether those instances are also Mastodon or one of the other Fediverse applications (<a href="https://fediverse.info/explore/projects">here's a fairly complete list</a>). Users can further 'shape' their feeds by employing filters selecting or eliminating those containing specific 'hashtags' (a hashtag is just a word - or CamelCaseCollectionOfWords - in a post or 'toot' that has a '#' hash symbol at the front of it) and/or by blocking/muting specific other Fediverse participants whose contributions they prefer to avoid.</p> <p>We, here at the OER Foundation, have been running Mastodon for 6 years now, but the sudden spike in interest and changes to the various dependent technologies since our <a href="/node/14">previous Mastodon howto</a> have motivated me to create this streamlined, updated version. We're very keen to see <em>lots</em> of individual academic institutions running Mastodon instances on behalf of their learners and broader educational communities! The cost is fairly trivial (USD30-50/month) for an instance with a thousand or two users, especially for an institution.</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-mastodon">Key variables for you Mastodon</a></p> <ul><li> <p><a href="#get-your-domain-lined-up">Get your Domain lined up</a></p> </li> <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">Outgoing VPS Email</a></p> </li> <li> <p><a href="#docker-compose-and-lets-encrypt">Docker Compose and Let's Encrypt</a></p> </li> <li> <p><a href="#install-the-mastodon-docker-recipe">Install the Mastodon Docker recipe</a></p> </li> <li> <p><a href="#nginx-reverse-proxy-configuration">NGINX reverse proxy configuration</a></p> </li> <li> <p><a href="#build-your-mastodon">Build your Mastodon</a></p> </li> <li> <p><a href="#create-your-admin-user">Create your Admin user</a></p> </li> <li> <p><a href="#basic-instance-configuration">Basic Instance configuration</a></p> </li> <li> <p><a href="#other-considerations">Other considerations</a></p> <ul><li> <p><a href="#backing-up-the-mastodon-database">Backing up the Mastodon Database</a></p> </li> <li> <p><a href="#backing-up-your-vps-files-and-configurations-incuding-your-mastodon">Backing up your VPS' files and configurations, incuding your Mastodon</a></p> </li> <li> <p><a href="#upgrading-your-mastodon-instance-to-newer-versions">Upgrading your Mastodon instance to newer versions</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 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 'agency' you always wanted. Plus, I'll be very impressed by your esprit de corps and tenancity!</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 Mastodon instance. You can run it 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.</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 about 10 times more expensive than commodity hosting options.</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 Mastodon instance of modest size (say up to a few hundred active users) you provision a VPS with the following spec (go as high as you can afford to). 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>8-16 GB RAM (a small instance can be run on 4GB RAM, so you could start with that, as RAM can be upgraded easily)</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://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 - 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 of "ubuntu" user 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:</p> <p><code>sudo apt-get update &amp;&amp; sudo apt-get 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 "do this action as the root (aka the 'Super User', thus 'su' for short) user" - you may 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 and come back to it.</p> <p>At this point, I also like to install some cool software called 'etckeeper' which records configuration changes on your VPS for future reference (and recovering from administrative mess-ups):</p> <p><code>sudo apt-get 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-mastodon" href="#key-variables-for-you-mastodon" name="key-variables-for-you-mastodon" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Key variables for you Mastodon</h2> <p>To set up you Mastodon, 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>[domain name]</strong> - the fully qualified domain name or subdomain by which you want your instance to be accessed. You must have full domain management ability on this domain. Example: mastodon.oeru.org - that's the mastodon subdomain of the oeru.org domain. Authenticating SMTP details - this is required so your web service can send emails to users - crucial things like email address validation and password recovery emails...</li> <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> <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> <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). Either way, best practice is 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><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 [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).</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 you're looking at logs and having to constantly convert the times into your timezone.</p> <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 it inside the backquotes `` means 'replace with the value the script returns', so it sets the value of EDIT to the path of the nano command.</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>Let's configure our firewall. We work on the basis of allowing in <em>only</em> what we want to let in (i.e. 'default deny').</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>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 webserver and reverse-proxy, as well as the Let's Encrypt SSL certificate generator, both of which are crucial for any secure webservices you might want to host. NGINX is a more efficient and flexible alternative to the older Apache webserver you might've seen elsewhere.</p> <p><code>sudo apt-get install nginx-full letsencrypt</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" href="#outgoing-vps-email" name="outgoing-vps-email" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Outgoing VPS Email</h2> <p>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-get 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>, work.fossdle.org, localhost.fossdle.org, 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-docker-compose-and-lets-encrypt" href="#docker-compose-and-lets-encrypt" name="docker-compose-and-lets-encrypt" class="heading-permalink" aria-hidden="true" title="Permalink"></a>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 Mastodon (and anything else you might want to host on this 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>domain name<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-install-the-mastodon-docker-recipe" href="#install-the-mastodon-docker-recipe" name="install-the-mastodon-docker-recipe" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Install the Mastodon Docker recipe</h2> <p>Now we have a place to put the really key bit - the code for running Mastodon via Docker Compose - first we go to the right place to set things up:</p> <p><code>cd /home/docker</code></p> <p>and then we use an amazing tool called '<a href="https://git-scm.com/book/en/v2/Getting-Started-About-Version-Control">git</a>' to download the canonical code created by the Mastodon developers. Of course, replace [domain name] in the following with the one you've selected (for the record, this command should work without <code>sudo</code> because we changed the ownership of the director in the previous step to belong to your unprivileged user).</p> <p><code>git clone https://github.com/tootsuite/mastodon.git [domain name]</code></p> <p>Then you can go into the directory</p> <p><code>cd [domain name]</code></p> <p>Have a look around. Use <code>ls -la</code> to see what files and directories (including 'hidden' ones, whose names start with a '.') the project includes. They key files in this case are the <code>.env.production.sample</code> and <code>docker-compose.yml</code>. You can ignore the rest for the time being.</p> <p>The first thing we want to do is create a <code>.env.production</code> file - this will hold <em>all the crucial details of your Mastodon instance</em>. Do it like this:</p> <p><code>cp .env.production.sample .env.production</code></p> <p>and then edit it (if EDIT isn't defined, because you're in a different session now, you might need to reset it as described above):</p> <p><code>$EDIT .env.production</code></p> <p>The following is what I've got (replace the [tokens] as usual). Make sure you get your SMTP details right - if you don't, you can fix them, but you won't get any emails from the instance until you do! Note that there're five (5) special numbers that you'll need to set later - but don't worry about them for the moment.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span># This is a sample configuration file. You can generate your configuration</span> <span># with the `rake mastodon:setup` interactive setup wizard, but to customize</span> <span># your setup even further, you'll need to edit it manually. This sample does</span> <span># not demonstrate all available configuration options. Please look at</span> <span># https://docs.joinmastodon.org/admin/config/ for the full documentation.</span>   <span># Note that this file accepts slightly different syntax depending on whether</span> <span># you are using `docker-compose` or not. In particular, if you use</span> <span># `docker-compose`, the value of each declared variable will be taken verbatim,</span> <span># including surrounding quotes.</span> <span># See: https://github.com/mastodon/mastodon/issues/16895</span>   <span># Federation</span> <span># ----------</span> <span># This identifies your server and cannot be changed safely later</span> <span># ----------</span> <span>LOCAL_DOMAIN</span>=<span>[</span>domain name<span>]</span> <span>LOCAL_HTTPS</span>=<span>true</span> <span>ALTERNATE_DOMAINS</span>=<span>[</span>second domain name<span>]</span>   <span># Redis</span> <span># -----</span> <span>REDIS_HOST</span>=redis <span>REDIS_PORT</span>=<span>6379</span>   <span># PostgreSQL</span> <span># ----------</span> <span>DB_HOST</span>=db <span>DB_USER</span>=postgres <span>DB_NAME</span>=postgres <span>DB_PASS</span>= <span>DB_PORT</span>=<span>5432</span>   <span># Elasticsearch (optional)</span> <span># ------------------------</span> <span>ES_ENABLED</span>=<span>false</span> <span>#ES_HOST=localhost</span> <span>#ES_PORT=9200</span> <span># Authentication for ES (optional)</span> <span>#ES_USER=elastic</span> <span>#ES_PASS=password</span>   <span># Secrets</span> <span># -------</span> <span># Make sure to use `rake secret` to generate secrets</span> <span># -------</span> <span>PAPERCLIP_SECRET</span>=LongSecretNumberOne <span>SECRET_KEY_BASE</span>=LongSecretNumberTwo <span>OTP_SECRET</span>=LongSecretNumberThree   <span># Web Push</span> <span># --------</span> <span># Generate with `rake mastodon:webpush:generate_vapid_key`</span> <span># --------</span> <span>#VAPID_PRIVATE_KEY=</span> <span>#VAPID_PUBLIC_KEY=</span> <span>VAPID_PRIVATE_KEY</span>=QuiteALotShorterSecretNumberOne <span>VAPID_PUBLIC_KEY</span>=SlightlyShorterSecretNumberTwo     <span># Sending mail</span> <span># ------------</span> <span>SMTP_SERVER</span>=<span>[</span>smtp server<span>]</span> <span>SMTP_PORT</span>=<span>[</span>smtp port<span>]</span> <span>SMTP_LOGIN</span>=<span>[</span>smtp user<span>]</span> <span>SMTP_PASSWORD</span>=<span>[</span>smtp password<span>]</span> <span>SMTP_FROM_ADDRESS</span>=<span>[</span>smtp reply-to-email<span>]</span>   <span># File storage (optional)</span> <span># -----------------------</span> <span># #S3_ENABLED=true</span> <span>#S3_BUCKET=files.example.com</span> <span>#AWS_ACCESS_KEY_ID=</span> <span>#AWS_SECRET_ACCESS_KEY=</span> <span>#S3_ALIAS_HOST=files.example.com</span></pre></div></div> <p>Once you've save that, we can create the <code>docker-compose.yml</code> file. I recommend creating a backup version of the original file in case you want to refer back to it:</p> <p><code>cp docker-compose.yml docker-compose.yml.orig</code></p> <p>Then you can edit the file via</p> <p><code>$EDIT docker-compose.yml</code></p> <p>Replace any [tokens] with your values. Note that the version of Mastodon at the time of this writing is 4.0.1 - to use a different version (e.g. if you are reading this tutorial after Eugen releases Mastodon 4.x (see the <a href="https://github.com/mastodon/mastodon/releases">current release</a>) you'll just want to replace all occurrences of 4.0.1 below with the appropriate version number.</p> <p>Note, the 'build' commands are commented out here as we're using pre-built containers rather than building them locally (as that takes a lot of time and potentially requires a fair bit of disk space, at least temporarily). If you want to do a build, you'll want to uncomment those lines.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>version: <span>'3'</span> services: db: restart: unless-stopped image: postgres:<span>14</span>-alpine shm_size: 256mb networks: - internal_network healthcheck: test: <span>[</span><span>'CMD'</span>, <span>'pg_isready'</span>, <span>'-U'</span>, <span>'postgres'</span><span>]</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>postgres14:<span>/</span>var<span>/</span>lib<span>/</span>postgresql<span>/</span>data environment: - <span>'POSTGRES_HOST_AUTH_METHOD=trust'</span>   redis: restart: unless-stopped image: redis:<span>6</span>-alpine networks: - internal_network healthcheck: test: <span>[</span><span>'CMD'</span>, <span>'redis-cli'</span>, <span>'ping'</span><span>]</span> volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>redis:<span>/</span>data   <span>## this is commented out because most Mastodon admins don't use it.</span> <span>## that's because the disk space it uses grows quickly and without bound...</span> <span># es:</span> <span># restart: unless-stopped</span> <span># image: docker.elastic.co/elasticsearch/elasticsearch-oss:7.10.2</span> <span># environment:</span> <span># - "ES_JAVA_OPTS=-Xms512m -Xmx512m"</span> <span># - "cluster.name=es-mastodon"</span> <span># - "discovery.type=single-node"</span> <span># - "bootstrap.memory_lock=true"</span> <span># networks:</span> <span># - internal_network</span> <span># healthcheck:</span> <span># test: ["CMD-SHELL", "curl --silent --fail localhost:9200/_cluster/health || exit 1"]</span> <span># volumes:</span> <span># - /home/data/[domain name]/elasticsearch:/usr/share/elasticsearch/data</span> <span># ulimits:</span> <span># memlock:</span> <span># soft: -1</span> <span># hard: -1</span>   web: <span>#build: .</span> image: tootsuite<span>/</span>mastodon:v4.0.1 restart: unless-stopped env_file: .env.production command: <span>bash</span> <span>-c</span> <span>"rm -f /mastodon/tmp/pids/server.pid; bundle exec rails s -p 3000"</span> networks: - external_network - internal_network healthcheck: <span># prettier-ignore</span> test: <span>[</span><span>'CMD-SHELL'</span>, <span>'wget -q --spider --proxy=off localhost:3000/health || exit 1'</span><span>]</span> ports: - <span>'127.0.0.1:3000:3000'</span> depends_on: - db - redis volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>public<span>/</span>system:<span>/</span>mastodon<span>/</span>public<span>/</span>system   streaming: <span>#build: .</span> image: tootsuite<span>/</span>mastodon:v4.0.1 restart: unless-stopped env_file: .env.production command: node .<span>/</span>streaming networks: - external_network - internal_network healthcheck: <span># prettier-ignore</span> test: <span>[</span><span>'CMD-SHELL'</span>, <span>'wget -q --spider --proxy=off localhost:4000/api/v1/streaming/health || exit 1'</span><span>]</span> ports: - <span>'127.0.0.1:4000:4000'</span> depends_on: - db - redis   sidekiq: <span>#build: .</span> image: tootsuite<span>/</span>mastodon:v4.0.1 restart: unless-stopped env_file: .env.production command: bundle <span>exec</span> sidekiq depends_on: - db - redis networks: - external_network - internal_network volumes: - <span>/</span>home<span>/</span>data<span>/</span><span>[</span>domain name<span>]</span><span>/</span>public<span>/</span>system:<span>/</span>mastodon<span>/</span>public<span>/</span>system healthcheck: test: <span>[</span><span>'CMD-SHELL'</span>, <span>"ps aux | grep '[s]idekiq\ 6' || false"</span><span>]</span>   <span>## If you need more workers uncomment the following. Add up to 3 more sidekiq containers (up to 5 total)</span> <span># sidekiq2:</span> <span># #build: .</span> <span># image: tootsuite/mastodon:v4.0.1</span> <span># restart: unless-stopped</span> <span># env_file: .env.production</span> <span># command: bundle exec sidekiq</span> <span># depends_on:</span> <span># - db</span> <span># - redis</span> <span># networks:</span> <span># - external_network</span> <span># - internal_network</span> <span># volumes:</span> <span># - /home/data/[domain name]/public/system:/mastodon/public/system</span> <span># healthcheck:</span> <span># test: ['CMD-SHELL', "ps aux | grep '[s]idekiq\ 6' || false"]</span>   networks: external_network: internal_network: internal: <span>true</span></pre></div></div> <p>Note: the port numbers in the <code>docker-compose.yml</code> file need to match those in the NGINX reverse proxy configuration.</p> <p>Once you've got this file configured, you should be ready to 'pull' your Docker containers! To do that, run</p> <p><code>docker-compose pull</code></p> <p>Once that's finished, you'll need to get three secret numbers: SECRET_KEY_BASE, OTP_SECRET, and PAPERCLIP_SECRET. Run the following <em>three times</em> and record the numbers (doesn't matter which one goes where, but once you set them, you <em>don't</em> want to change or lose them!):</p> <p><code>docker-compose run --rm web bundle exec rake secret</code></p> <p>Then you need to get the two VAPID keys - running this will provide both:</p> <p><code>docker-compose run --rm web bundle exec rake mastodon:webpush:generate_vapid_key</code></p> <p>Copy these into your <code>.env.production</code> - I encourage you to record them elsewhere as well - a <a href="/node/25">password manager</a> is a good place!</p> <p><code>$EDIT .env.production</code></p> <p>After that, your Mastodon is ready to roll! But there's one more crucial step - we need to set up the <em>reverse proxy</em> server which provides the secure (encrypted) access to your instant for you and all your users.</p> <h2><a id="user-content-nginx-reverse-proxy-configuration" href="#nginx-reverse-proxy-configuration" name="nginx-reverse-proxy-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>NGINX reverse proxy configuration</h2> <p>For the reverse proxy, we'll use the NGINX web server we installed earlier. It stores all of its configuration in the directoy <code>/etc/nginx</code>.</p> <p>The first thing we'll do is create a couple directories we need:</p> <p>This is where Let's Encrypt will look for a secret code we put here to verify that we own the domain we're requesting an SSL certificate for:</p> <p><code>sudo mkdir /var/www/letsencrypt</code></p> <p>Then we create a place to the Let's Encrypt NGINX configuration details</p> <p><code>sudo mkdir /etc/nginx/includes</code></p> <p>And then we create that configuration file</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>Next we have to create the reverse proxy configuration file for our Mastodon domain name (and any other domain names we might want to use):</p> <p><code>sudo $EDIT /etc/nginx/sites-available/[domain name]</code></p> <p>Again, we have the convention of using the [domain name] to identify this file. It's self-documenting. Copy-and-paste the following into, making [token] substitutions. Note that the port numbers (usually after a ':') need to correspond to the port numbers specified in the <code>docker-compose.yml</code> file.</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>map <span>$http_upgrade</span> <span>$connection_upgrade</span> <span>{</span> default upgrade; <span>''</span> close; <span>}</span>   proxy_cache_path <span>/</span>var<span>/</span>cache<span>/</span>nginx <span>levels</span>=<span>1</span>:<span>2</span> <span>keys_zone</span>=CACHE:10m <span>inactive</span>=7d <span>max_size</span>=1g;   <span># this configuration redirects any attempts to connect to your instance insecurely (via http rather than https)</span> <span># to your preferred [domain name] via https.</span> server <span>{</span> listen <span>80</span>; listen <span>[</span>::<span>]</span>:<span>80</span>; <span># you can optionally include one or more other domains (e.g. [second domain name]) here</span> <span># - just separate by spaces.</span> server_name <span>[</span>domain name<span>]</span>; root <span>/</span>var<span>/</span>www<span>/</span>html;   include includes<span>/</span>letsencrypt.conf;   <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># 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>## optional - if you want your Mastodon to respond to a [second domain name]</span> <span>## this will perform a redirection from this domain to your main domain.</span> <span>##</span> <span>## to use it, uncomment the first # in each line following</span> <span>#server {</span> <span># listen 443 ssl http2;</span> <span># listen [::]:443 ssl http2;</span> <span># server_name [second domain name];</span> <span>#</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>## ssl_certificate /etc/letsencrypt/live/[domain name]/fullchain.pem;</span> <span>## ssl_certificate_key /etc/letsencrypt/live/[domain name]/privkey.pem;</span> <span># ssl_protocols TLSv1 TLSv1.1 TLSv1.2;</span> <span># # from https://0x39b.fr/post/nginx_security/</span> <span># ssl_session_timeout 1d;</span> <span># ssl_session_cache shared:SSL:50m;</span> <span># #ssl_session_tickets off;</span> <span># ssl_prefer_server_ciphers on;</span> <span># 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';</span> <span># # OCSP Stapling ---</span> <span># # fetch OCSP records from URL in ssl_certificate and cache them</span> <span># ssl_stapling on;</span> <span># ssl_stapling_verify on;</span> <span># # to create this, see https://raymii.org/s/tutorials/Strong_SSL_Security_On_nginx.html</span> <span># ssl_dhparam /etc/ssl/certs/dhparam.pem;</span> <span>#</span> <span># # for let's encrypt renewals!</span> <span># include includes/letsencrypt.conf;</span> <span>#</span> <span># keepalive_timeout 70;</span> <span># sendfile on;</span> <span># client_max_body_size 80M;</span> <span>#</span> <span># # change the file name of these logs to include your server name</span> <span># # if hosting many services...</span> <span># access_log /var/log/nginx/[domain name]_access.log;</span> <span># error_log /var/log/nginx/[domain name]_error.log;</span> <span>#</span> <span># # redirect all HTTP traffic to HTTPS.</span> <span># location / {</span> <span># return 302 https://[domain name]$request_uri;</span> <span># }</span> <span>#}</span> <span>## end optional *second* domain name</span>   server <span>{</span> listen <span>443</span> ssl http2; listen <span>[</span>::<span>]</span>:<span>443</span> ssl http2; <span># if you have a [second domain]</span> server_name <span>[</span>domain name<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/[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># from https://0x39b.fr/post/nginx_security/</span> ssl_session_timeout 1d; ssl_session_cache shared:SSL:50m; <span>#ssl_session_tickets off;</span> ssl_prefer_server_ciphers on; ssl_ciphers <span>'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'</span>; <span># OCSP Stapling ---</span> <span># fetch OCSP records from URL in ssl_certificate and cache them</span> ssl_stapling on; ssl_stapling_verify on; <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;   <span># for let's encrypt renewals!</span> include includes<span>/</span>letsencrypt.conf;   keepalive_timeout <span>70</span>; sendfile on; client_max_body_size 80M;   <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># from https://github.com/tootsuite/documentation/blob/master/Running-Mastodon/Production-guide.md</span> <span>gzip</span> on; gzip_vary on; gzip_proxied any; gzip_comp_level <span>6</span>; gzip_buffers <span>16</span> 8k; gzip_http_version <span>1.1</span>; gzip_types text<span>/</span>plain text<span>/</span>css application<span>/</span>json application<span>/</span>javascript text<span>/</span>xml application<span>/</span>xml application<span>/</span>xml+rss text<span>/</span>javascript;     add_header Strict-Transport-Security <span>"max-age=31536000; includeSubDomains"</span>;   location <span>/</span> <span>{</span> try_files <span>$uri</span> <span>@</span>proxy; <span>}</span>   location ~ ^<span>/</span><span>(</span>emoji<span>|</span>packs<span>|</span>system<span>/</span>accounts<span>/</span>avatars<span>|</span>system<span>/</span>media_attachments<span>/</span>files<span>)</span> <span>{</span> add_header Cache-Control <span>"public, max-age=31536000, immutable"</span>; add_header Strict-Transport-Security <span>"max-age=31536000"</span>; try_files <span>$uri</span> <span>@</span>proxy; <span>}</span>   location <span>/</span>sw.js <span>{</span> add_header Cache-Control <span>"public, max-age=0"</span>; add_header Strict-Transport-Security <span>"max-age=31536000"</span>; try_files <span>$uri</span> <span>@</span>proxy; <span>}</span>   location <span>@</span>proxy <span>{</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 https; proxy_set_header Proxy <span>""</span>; proxy_pass_header Server;   proxy_pass http:<span>//</span>127.0.0.1:<span>3000</span>; proxy_buffering off; proxy_redirect off; proxy_http_version <span>1.1</span>; proxy_set_header Upgrade <span>$http_upgrade</span>; proxy_set_header Connection <span>$connection_upgrade</span>;   tcp_nodelay on; <span>}</span>   location <span>/</span>api<span>/</span>v1<span>/</span>streaming <span>{</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 https; proxy_set_header Proxy <span>""</span>;   proxy_pass http:<span>//</span>127.0.0.1:<span>4000</span>; proxy_buffering off; proxy_redirect off; proxy_http_version <span>1.1</span>; proxy_set_header Upgrade <span>$http_upgrade</span>; proxy_set_header Connection <span>$connection_upgrade</span>;   tcp_nodelay on; <span>}</span>   error_page <span>500</span> <span>501</span> <span>502</span> <span>503</span> <span>504</span> <span>/</span><span>500</span>.html; <span>}</span></pre></div></div> <p>We initially comment out the SSL certificate paths for our [domain name] in the above configuration because we haven't yet created those certificates, and NGINX won't run with missing certificates (a trap for young players!). So we temprarily substitute the default (not domain-specific) "snakeoil" certificates that are provided with every Linux installation to act as valid certificate placeholders to run NGINX so that we can then request Let's Encrypt certificates.</p> <p>After we've save the NGINX configuration, we need to make sure it's also in the <code>sites-enabled</code> directory, so NGINX will see it (it ignores those merely in sites-available, which is sort of a holding pen for potential site configurations):</p> <p><code>sudo ln -sf /etc/nginx/sites-available/[domain name] /etc/nginx/sites-enabled/</code></p> <p>before we can test our NGINX confguration for errors, we need to address the file, <code>/etc/ssl/certs/dhparam.pem</code>, our config file references but which doesn't yet exist by creating it like this:</p> <p><code>sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 4096</code></p> <p>Note <strong>this process can take quite a long time</strong>, like 10-40 minutes depending on your VPS and the 'entropy' it generates. If you're short of time, cancel the one running (CTRL+C) and run it again with <code>2048</code> rather than <code>4096</code> specified. It'll make your installation marginally less secure.</p> <p>after which we can ask NGINX to test its configuration for errors:</p> <p><code>sudo nginx -t</code></p> <p>Fix any errors you might find (e.g. typos, missing punctuation, etc.) and after <code>nginx -t</code> tells you you've got valid configurations, reload NGINX to enable the new configuration:</p> <p><code>sudo service nginx reload</code></p> <p>Once that's working, your server is configured to respond to external requests for you site via [domain name] via <code>http://</code> (not encrypted), and via <code>https://</code> (encrypted, although pointing your browser at <code>https://[domain name]</code> right now will get your warnings that you've got a mismatch between your certificate and your domain name.). So, of course, the next step is to generate a certificate for [domain name].</p> <p>Let's Encrypt can award a certificate to you because it can confirm that you (who control [domain name]) also control the server. They verify it by pointing their infrastructure at your [domain name] - recalling that your domain is pointed at your VPS' IPv4 (the A record above) and (if used) IPv6 (the AAAA record above) - and checking in pre-defined location (see the letsencrypt command below) to see if, at that location they can find a secret number that you've asked them to write there. If they find it, they can trust that you control both the [domain name] and the VPS it's pointing to.</p> <p>Here's the command that will request Let's Encrypt's systems to run that check - it will verify that all the domain names specified (with a <code>-d</code> flag are a) pointing at your VPS, and b) their scripts can see the secret number the specified location, in this case in <code>/var/www/letsencrypt</code>):</p> <p><code>letsencrypt certonly --webroot -w /var/www/letsencrypt -d [domain name] -d [second domain name]</code></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> and <span>[</span>second 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>2023</span>-01-<span>31</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 fully 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! :grin:</p> <p>We're in the final stretch now!</p> <h2><a id="user-content-build-your-mastodon" href="#build-your-mastodon" name="build-your-mastodon" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Build your Mastodon</h2> <p>To actually launch your Mastodon instance you first have to create the structure for database in the PostreSQL container. You can do that Virtual</p> <p><code>docker-compose run --rm web rails db:migrate</code></p> <p>This will temporarily spin up your 'web' container (which, in turn depends on your PostgreSQL container, aka 'db') and run the 'migration' script which either updates (or creates, if it's not already there) your database tables.</p> <p>Ok - it's finally time to launch your Mastodon instance. Running this will fire up all of your containers using the values in your <code>.env.production</code> file.</p> <p><code>docker-compose up -d &amp;&amp; docker-compose logs -f --tail=100</code></p> <p>After it initiates the containers with the <code>up -d</code> command, it opens a logging interface which allow you to watch the log messages from all the containers (only including the most recent 100 lines at the time you run it), which is very helpful in the event that something goes wrong - you should be able to see what it is.</p> <p>You can always stop your Mastodon instance via</p> <p><code>docker-compose stop</code> or individual containers via, for example, the 'sidekiq' container, <code>docker-compose stop sidekiq</code>.</p> <p>Now your Mastodon should be running. You can point your browser at <code>https://[domain name]</code> and you should be greeted by a sight similar to the first screenshot above (although it'll have your site's [domain name] rather than social.fossdle,org, which is the site I recently deployed and was the basis for setting up this tutorial). Your login page will just have generic information as it hasn't been configured yet, and doesn't have any users, administrative or otherwise.</p> <p>If your site is missing the "mastodon" image below the Log In form, it might be that your 'assets' haven't been pre-compiled. If that's the case, just run this:</p> <p><code>docker-compose run --rm web rails assets:precompile</code></p> <p>which will re-compile them.</p> <h2><a id="user-content-create-your-admin-user" href="#create-your-admin-user" name="create-your-admin-user" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Create your Admin user</h2> <p>What you now need to do is create a user with [mastodon username] for the administrator of the site. That can be your own user or a dedicated admin user. It's up to you. I tend to make my personal user the admin user. You'll just follow the instructions on the start page to create a new account. All going well, you'll receive an email from the site asking you to verify your email address, and then you'll get a welcome email. At this point, you can log in, although you'll just be a generic user, not an admin. To change your user into an admin user, you'll need to run</p> <p><code>docker-compose exec -e RAILS_ENV=production streaming bin/tootctl accounts modify [mastodon username] --role admin</code></p> <p>If it's successful, you should see</p> <p><code>OK</code></p> <p>At that point, you can do a refresh of your logged in session in your Mastodon instance, and you'll be the Administrator! Congratulations! You've done it.</p> <p>Now, you'll just want to do some basic configuration of your instance... and then you can let others know about it!</p> <h2><a id="user-content-basic-instance-configuration" href="#basic-instance-configuration" name="basic-instance-configuration" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Basic Instance configuration</h2> <p>Once you've got your admin user working, you'll want to go to the "settings" for your user (the little 'gear' near the bottom right of the right column in the default interface, or at the top right of the left column of the advanced interface) and from the resulting menu, you should be able to select the 'Administration' option. The first thing to do is to go to 'Settings' and fill in as much as you need to - I've included a screen shot of our settings page on social.fossdle.org for your reference. You should also set some 'instance rules' - I've included another screenshot showing our instance rules - you can always <a href="https://social.fossdle.org/about/more">check them out for yourself</a> - you're welcome to borrow (any|every)thing from them.</p> <p>After all that's set up, you're ready to go! You can start following other users - one useful trick to know about is that you can go to any other instances you know about via their web address (or go to the <a href="https://joinmastodon.org">main Mastodon help site</a> and look for other instances) and you can explore the 'profile directory' on any other instance. Clicking on a user's name (bold text) will pop up a box in your browser allowing you to elect to follow that user by typing in your Mastodon handle - it'll be [mastodon username]@[domain name] - in the provided form, and then selecting the 'Follow' button. Alternatively, you can copy-and-paste a user handle (below a user's name in their profile directory) into the Search bar in your Mastodon interface - it should show you the same profile info in your own Mastodon's interface window, and you should be able to follow with a single click on the "Follow" button or the little 'follow user' icon (greyed out head with a + after it). If you're already following a user, you'll see a blue 'user being followed' icon (with a blue x next to it) that, if clicked, will <em>unfollow</em> that user.</p> <h2><a id="user-content-other-considerations" href="#other-considerations" name="other-considerations" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Other considerations</h2> <p>Ok, you're in business, but there're a few loose ends to tidy up (after the well-earned euphoria of your new found power and place die down a bit!). These are things that any production web service needs to have, to ensure that you're looking after both your own (and, just as important!, your fellow users') data and generally running a secure, tight ship.</p> <h3><a id="user-content-backing-up-the-mastodon-database" href="#backing-up-the-mastodon-database" name="backing-up-the-mastodon-database" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Backing up the Mastodon Database</h3> <p>The first is that you need to have regular backups of the PostgreSQL database that underlies the whole Mastodon instance. I've <a href="https://git.oeru.org/oeru/docker-compose-dbbackup">published a backup utility</a> that we use at the OER Foundation to back up our Docker containerised PostgreSQL instances every hour! By the way, <a href="https://postgresql.org/">PostgreSQL</a> is an <em>amazing</em> database, both FOSS and world-class, that's used by many of our FOSS services! We would use it over, say, the vastly more expensive proprietary database, MS SQL Server, any day of the week. In many ways, PostgreSQL is more capable, and quite a lot <em>faster</em> besides.</p> <p>I'll aim to write another (shorter!) tutorial on how to implement this system in the next week or two.</p> <h3><a id="user-content-backing-up-your-vps-files-and-configurations-incuding-your-mastodon" href="#backing-up-your-vps-files-and-configurations-incuding-your-mastodon" name="backing-up-your-vps-files-and-configurations-incuding-your-mastodon" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Backing up your VPS' files and configurations, incuding your Mastodon</h3> <p>Also, you'll need to make sure that all the important files (configuration and data) on your VPS are being backed up, and ideally sent somewhere <em>remote</em> to your VPS in case something happens to it (rare though that might be), like it gets cracked by ne'er-do-wells or your hosting company suddenly goes belly-up (haven't heard of it happening, but there's always a first time!)...</p> <p>We use an amazing FOSS tool called <a href="https://restic.net">Restic</a>. It allows us to make automated remote incremental backups of the VPS filesystem (including the frequent database dumps created by the database backup script above), and it even encrypts them on the way to protect your users' data even if the <em>backup</em> server is somehow compromised. This is best practice. I've also <a href="https://git.oeru.org/dave/restic-backup">created a script to deploy Restic</a>, and will need to write another tutorial to provide some more explanation.</p> <p>To use this script, you'll need somewhere (that's accessible from the internet) to which you can send our backups! That's either a server with big hard disks in someone's home (with an externally visible network and a properly configured router), or some other backup location, like a commodity object store (often these are advertised as 'AWS S3-compatible') or a big internet-addressible block storage device. The Restic site will provide some guidance on this.</p> <h3><a id="user-content-upgrading-your-mastodon-instance-to-newer-versions" href="#upgrading-your-mastodon-instance-to-newer-versions" name="upgrading-your-mastodon-instance-to-newer-versions" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Upgrading your Mastodon instance to newer versions</h3> <p>Last, Mastodon's developers seldom rest for long. Eugen and the rest of the Mastodon developer community are constantly looking at how they can improve Mastodon, or fix any issues that might emerge. While I was composing this tutorial, the <a href="https://github.com/mastodon/mastodon/releases">current release</a> version went from 3.5.3 -&gt; 4.0.0. -&gt; 4.0.1 in a matter of a couple hours. You don't need to upgrade your instance <em>every time</em> there's a new release, although you <em>do</em> want to apply any <em>security-specific</em> upgrades as quickly as possible to minimise the window of time that your instance is vulnerable!</p> <p>I'll need to write another tutorial on how to do updates, but you should find basic instructions on each release! If your instance is not current at the time of a new release, make sure you <em>read the intervening release notes, too</em> as there are sometimes special instructions for a given jump from one release to the next.</p> <p>Always make sure you have <em>valid</em> backups, ideally on the same VPS, before you do an upgrade so that you can roll back in the event the upgrade fails for some reason!</p> <p>Well done reading through this screed of techno-text! I hope it was a lot quicker to read than it was to write (sheesh! I think I need a lie-down). All the best with your adventures in the Fediverse as a full-fledged contributing member!!</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-838" class="comment js-comment by-anonymous has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/838#comment-838" class="permalink" rel="bookmark" hreflang="en">I get permission denied as…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1674169339"></span> </h3> <div class="comment__meta"> <div class="comment__submitted"> <span class="comment__author"><span>nick (not verified)</span></span> <span class="comment__pubdate">Mon 21/11/2022 - 04:42</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>I get permission denied as user or root on:<br /> Then we&#039;ll enable forwarding from internal network interfaces as required for Docker containers to be able to talk to the outside world:</p> <p>sudo $EDIT /etc/default/ufw</p> </div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=838&amp;1=default&amp;2=en&amp;3=" token="kWJaop42Jv1ihyvuo9vc5LT9g4fX_2pqT-oUAW_u5P8"></drupal-render-placeholder> </div> </div> </article> <div class="indented"><article data-comment-user-id="1" id="comment-894" class="comment js-comment by-node-author has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/894#comment-894" class="permalink" rel="bookmark" hreflang="en">Did you define an editor …</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1675848918"></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">Wed 08/02/2023 - 22:35</span> </div> </div> <div class="comment__content"> <p class="comment__parent visually-hidden">In reply to <a href="/comment/838#comment-838" class="permalink" rel="bookmark" hreflang="en">I get permission denied as…</a> by <span>nick (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>Did you define an editor (which happens prior to that line, e.g. EDIT=<code>which nano</code> or EDIT=<code>which vim</code>)?</p></div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=894&amp;1=default&amp;2=en&amp;3=" token="oU-0eyYuISB5epwv11RZWi7mH8IgT5aTqWb1nwDi2Ns"></drupal-render-placeholder> </div> </div> </article> </div><article data-comment-user-id="0" id="comment-840" class="comment js-comment by-anonymous has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/840#comment-840" class="permalink" rel="bookmark" hreflang="en">Well written article however…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1674169334"></span> </h3> <div class="comment__meta"> <div class="comment__submitted"> <span class="comment__author"><span>David Field (not verified)</span></span> <span class="comment__pubdate">Tue 20/12/2022 - 02:43</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>Well written article however, i think you ned to set passwords for the Postgres and Elastic in .env.production as docker-compose run --rm web rails db:migrate fails.</p> <p>Also I think if you want the resulting docker containers to work on Ubuntu 22.04 then you&#039;ll need to add</p> <p>sudo iptables -t nat -A POSTROUTING ! -o docker0 -s 172.0.0.0/8 -j MASQUERADE</p> </div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=840&amp;1=default&amp;2=en&amp;3=" token="jIybCl5IfYTeMlT6sfQZdqN_jtKaZthuWjSd8JL0h-w"></drupal-render-placeholder> </div> </div> </article> <div class="indented"><article data-comment-user-id="1" id="comment-893" class="comment js-comment by-node-author has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/893#comment-893" class="permalink" rel="bookmark" hreflang="en">Thanks for that, David. Will…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1675848822"></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">Wed 08/02/2023 - 22:33</span> </div> </div> <div class="comment__content"> <p class="comment__parent visually-hidden">In reply to <a href="/comment/840#comment-840" class="permalink" rel="bookmark" hreflang="en">Well written article however…</a> by <span>David Field (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>Thanks for that, David. Will double check that the next time I do a Mastodon setup - but, for what it's worth, this configuration works for me without the NAT iptables rule, and I didn't have issues with the db:migrate passwords (I generally disable Elastic, for what it's worth)...</p></div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=893&amp;1=default&amp;2=en&amp;3=" token="i511YLYGKPh1Ag6HOqUbFRcxeGDSfkO9h90yYaFEvT8"></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=54&amp;2=field_blog_comments&amp;3=comment" token="B9-JGcAklPJd4Gn3Mi7jVU_kyHopkfC0tQT73ibOWw4"></drupal-render-placeholder> </div> </section> Thu, 03 Nov 2022 21:31:11 +0000 dave 54 at http://tech.oeru.org OERu Web Services as of August 2022 http://tech.oeru.org/oeru-web-services-august-2022 <span class="field field--name-title field--type-string field--label-hidden">OERu Web Services as of August 2022 </span> <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/08/2022 - 16:38</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>In the intervening 17 months since my <a href="/node/34">last update</a> the <a href="https://oerfoundation.org">OER Foundation</a> (OERF) has continued to develop new online services for its global community of learners and educators. All of these services are themselves built with - and hosted on - <a href="https://en.wikipedia.org/wiki/Free_and_open-source_software">Free and Open Source Software</a> or FOSS. Each application is the work and responsibility of its own global developer community. The OERF participates in these communities at various levels as we avail ourselves of this wealth of brilliant software for the benefit of our users.</p> <p>In addition to refining our <a href="https://oeru.org">OERu</a> services over the past year, adding a few and sloughing off a few, we have also begun to assist other organisations by hosting systems on their behalf, usually with the aim of handing them over to their control after a period of system administration tuition.</p> <p>As of this writing, in August 2022, we run the following production systems:</p> <ul><li>a couple of <a href="https://wordpress.org/support/article/create-a-network/">WordPress Multisites</a>: <a id="user-content-our-main-course-delivery-site-where-each-course-is-a-subsite---httpscourseoeruorg" href="#our-main-course-delivery-site-where-each-course-is-a-subsite---httpscourseoeruorg" name="our-main-course-delivery-site-where-each-course-is-a-subsite---httpscourseoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our main course delivery site, where each course is a 'subsite' - <a href="https://course.oeru.org">https://course.oeru.org</a> <a id="user-content-our-train-the-educators-blog-and-sandbox-course-site-each-educator-gets-a-blog-subsite-for-their-own-use-and-a-course-sandbox-subsite---httpsedt4oloeruorg" href="#our-train-the-educators-blog-and-sandbox-course-site-each-educator-gets-a-blog-subsite-for-their-own-use-and-a-course-sandbox-subsite---httpsedt4oloeruorg" name="our-train-the-educators-blog-and-sandbox-course-site-each-educator-gets-a-blog-subsite-for-their-own-use-and-a-course-sandbox-subsite---httpsedt4oloeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our train-the-educators blog and sandbox course site. Each educator gets a 'blog' subsite for their own use, and a 'course sandbox' subsite - <a href="https://edt4ol.oeru.org">https://edt4ol.oeru.org</a> </li> <li>a trio of standalone <a href="https://wordpress.org">WordPress</a> instances for the OERF and related initiatives: <a id="user-content-the-oerfs-own-organisational-website---httpsoerfoundationorg" href="#the-oerfs-own-organisational-website---httpsoerfoundationorg" name="the-oerfs-own-organisational-website---httpsoerfoundationorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the OERF's own organisational website - <a href="https://oerfoundation.org">https://oerfoundation.org</a> <a id="user-content-the-centre-for-open-educational-practice-coep---httpscoepnz" href="#the-centre-for-open-educational-practice-coep---httpscoepnz" name="the-centre-for-open-educational-practice-coep---httpscoepnz" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the Centre for Open Educational Practice (COEP) - <a href="https://coep.nz">https://coep.nz</a> <a id="user-content-the-oerfs-initiative-to-help-educators-in-the-developing-world-cope-with-the-challenges-of-covid19---httpsoer4covidoeruorg" href="#the-oerfs-initiative-to-help-educators-in-the-developing-world-cope-with-the-challenges-of-covid19---httpsoer4covidoeruorg" name="the-oerfs-initiative-to-help-educators-in-the-developing-world-cope-with-the-challenges-of-covid19---httpsoer4covidoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the OERF's initiative to help educators in the developing world cope with the challenges of COVID19 - <a href="https://oer4covid.oeru.org">https://oer4covid.oeru.org</a> </li> <li>a pair of <a href="https://drupal.org">Drupal</a> sites (they've been upgraded to Drupal 9.x) <a id="user-content-the-oerfs-information-tech-site-this-site---httpstechoeruorg-and" href="#the-oerfs-information-tech-site-this-site---httpstechoeruorg-and" name="the-oerfs-information-tech-site-this-site---httpstechoeruorg-and" class="heading-permalink" aria-hidden="true" title="Permalink"></a>the OERF's information tech site (this site!) - <a href="https://tech.oeru.org">https://tech.oeru.org</a>, and <a id="user-content-our-h5p-learning-object-builder-site---httpsh5poeruorg" href="#our-h5p-learning-object-builder-site---httpsh5poeruorg" name="our-h5p-learning-object-builder-site---httpsh5poeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our H5P learning object builder site - <a href="https://h5p.oeru.org">https://h5p.oeru.org</a> </li> <li>a couple <a href="https://www.discourse.org/">Discourse</a> instances - Discourse is the market leading, modern, full-featured (and open source) forum: <a id="user-content-our-collaboration-and-support-site-for-educators-developing-open-educational-resources-oers---httpscommunityoeruorg" href="#our-collaboration-and-support-site-for-educators-developing-open-educational-resources-oers---httpscommunityoeruorg" name="our-collaboration-and-support-site-for-educators-developing-open-educational-resources-oers---httpscommunityoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our collaboration and support site for educators developing Open Educational Resources (OERs) - <a href="https://community.oeru.org">https://community.oeru.org</a> <a id="user-content-our-companion-site-for-folks-taking-oeru-courses-for-both-assignments-and-collaboration-with-other-learners---httpsforumsoeruorg" href="#our-companion-site-for-folks-taking-oeru-courses-for-both-assignments-and-collaboration-with-other-learners---httpsforumsoeruorg" name="our-companion-site-for-folks-taking-oeru-courses-for-both-assignments-and-collaboration-with-other-learners---httpsforumsoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our companion site for folks taking OERu courses for both assignments and collaboration with other learners - <a href="https://forums.oeru.org">https://forums.oeru.org</a> </li> <li>a couple <a href="https://nextcloud.com">NextClouds</a> for file sharing and many other uses: <a id="user-content-our-main-collaborative-document-management-system-and-collaborative-authorship-and-sharing-media-and-documents---httpsdocsoeruorg" href="#our-main-collaborative-document-management-system-and-collaborative-authorship-and-sharing-media-and-documents---httpsdocsoeruorg" name="our-main-collaborative-document-management-system-and-collaborative-authorship-and-sharing-media-and-documents---httpsdocsoeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our main collaborative document management system and collaborative authorship and sharing media and documents - <a href="https://docs.oeru.org">https://docs.oeru.org</a> <a id="user-content-our-internal-collaboration-system---httpshuboeruorg" href="#our-internal-collaboration-system---httpshuboeruorg" name="our-internal-collaboration-system---httpshuboeruorg" class="heading-permalink" aria-hidden="true" title="Permalink"></a>our internal collaboration system - <a href="https://hub.oeru.org">https://hub.oeru.org</a> </li> <li>to support our NextCloud, we have integrated <a href="https://onlyoffice.com">OnlyOffice</a> allowing it to behave like a feature-rich Google Suite, but refreshingly Google-free - <a href="https://onlyoffice.oeru.org">https://onlyoffice.oeru.org</a> </li> <li>a <a href="https://bigbluebutton.org">BigBlueButton</a> instance - a large scale real-time video conferencing and educational collaboration platform - <a href="https://talk.oeru.org">https://talk.oeru.org</a> </li> <li>a <a href="https://github.com/mautic/mautic">Mautic</a> high-powered mailing list automation management system - <a href="https://mautic.oeru.org">https://mautic.oeru.org</a> </li> <li>a <a href="https://rocket.chat">Rocket.Chat</a> instance - a rich chat service comparable to Slack or Discord but fully open source (and we hold all our own data) - <a href="https://chat.oeru.org">https://chat.oeru.org</a> </li> <li>a <a href="https://keycloak.org">Keycloak</a> instance for our still work-in-progress consolidated authentication and identity management system (aka Single Sign-on) - <a href="https://login.oeru.org">https://login.oeru.org</a> </li> <li>a <a href="https://joinmastodon.org">Mastodon</a> instance - Twitter-esque but de-centralised and non-commercial federated open source social network (part of the <a href="https://en.wikipedia.org/wiki/Fediverse">Fediverse</a>) - <a href="https://mastodon.oeru.org">https://mastodon.oeru.org</a> </li> <li>a <a href="https://mailcow.email">Mailcow</a> instance for managing all our email - Mailcow is a full multi-tenanted SMTP/IMAP/POP email system with active spam filtering, user self-service, DKIM configuration/management, webmail, &amp; much more - even virus scanning for Windows clients - <a href="https://about.oerfoundation.org">https://about.oerfoundation.org</a> - it supplies email services for the oeru.org, oerfoundation.org, coep.nz domains, among others!</li> <li>an instance of <a href="https://yourls.org">YourLS</a> - a link shortener - <a href="https://oer.nz">https://oer.nz</a> </li> <li>a <a href="https://matomo.org">Matomo</a> instance - website analytics (like Google Analytics, but without infringing on the EU's <a href="https://gdpr.eu/what-is-gdpr/">GDPR legislation</a>) - <a href="https://stats.oeru.org">https://stats.oeru.org</a> </li> <li>a <a href="https://mahara.org">Mahara</a> instance - online academic portfolio tool - <a href="https://portfolio.oerfoundation.org">https://portfolio.oerfoundation.org</a> </li> <li>a <a href="https://moodle.org">Moodle</a> instance - market leading learning management system which we use primarily for offering quizzes and awarding certificates for participants - <a href="https://moodle.oeru.org">https://moodle.oeru.org</a> </li> <li>a <a href="https://silverstripe.com">SilverStripe</a> instance - our main OERu website - <a href="https://oeru.org">https://oeru.org</a> </li> <li>a <a href="https://mediawiki.org">MediaWiki</a> instance - collaboration platform for OER development built on the same platform on which Wikipedia is built. All our courses are developed here - <a href="https://wikieducator.org">https://wikieducator.org</a> </li> <li>a <a href="https://gitlab.com/rluna-gitlab/gitlab-ce">GitLab</a> - a software developer version control/collaboration tool where we make <em>all</em> our code available - <a href="https://git.oeru.org">https://git.oeru.org</a> </li> <li>a <a href="https://limesurvey.com">LimeSurvey</a> instance - survey tool - <a href="https://survey.oeru.org">https://survey.oeru.org</a> </li> <li>a <a href="https://bitwarden.com">BitWarden</a>/VaultWarden](<a href="https://github.com/dani-garcia/vaultwarden">https://github.com/dani-garcia/vaultwarden</a>) instance - password manager and cross-device sync service - <a href="https://safe.oeru.org">https://safe.oeru.org</a> (here's our <a href="/node/25">howto for hosting your own</a>)</li> <li>an instance of <a href="https://sourceforge.net/projects/semanticscuttle">Semantic Scuttle</a> - a collaborative public website bookmarking service - <a href="https://bookmarks.oeru.org">https://bookmarks.oeru.org</a> </li> </ul><p>This past year, having elected to use OnlyOffice for productivity purposes, we retired our Etherpad-light and CollaboraOffice instances.</p> <p>We also added</p> <ul><li>a Wekan instance - for project planning via the Kanban methodology. Similar to Trello (among other tools) - <a href="https://kanban.oeru.org">https://kanban.oeru.org</a>.</li> <li>a Mobilizon instance - for managing events, and people following, discussing, and signing-up for and attending them - <a href="https://events.oeru.org">https://events.oeru.org</a> </li> <li>a <a href="https://rustdesk.com">RustDesk</a> server instance, allowing us to provide anyone anywhere on just about any computing platform (Linux, Windows, MacOS, iOS, and Android) with live interactive shared desktop support</li> <li>a <a href="https://github.com/PagerTree/prometheus-grafana-alertmanager-example">server monitoring solution</a> based on Grafana, Prometheus, Node-exporter, Alertmanager, cAdvisor, and other tools, which provide a graphical monitoring solution for each of our servers.</li> </ul><p>We also maintain development and testing/staging instances of most of these services. Our services are all hosted on virtual Linux servers (we mostly run Ubuntu Linux) provided by commodity cloud hosting providers via <a href="https://docs.docker.com/engine/install/ubuntu/">Docker</a> and orchestrated via <a href="https://docs.docker.com/compose/install/">Docker Compose</a>. As we reported last year, in time it's likely we'll move to "just-in-time" scaling via Kubernetes, but for now that'd be overkill.</p> <h2><a id="user-content-hosting-on-behalf" href="#hosting-on-behalf" name="hosting-on-behalf" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Hosting on behalf</h2> <p>In the past year, we've started hosting services on behalf of other organisations, including <a href="https://col.org">Commonwealth of Learning</a>, <a href="https://oeglobal.org">Open Education Global</a>, and the government of Samoa, in particular their <a href="https://mesc.ws">Ministry of Education, Sport, and Culture</a>'s Innovative Lifelong Learning Lab (the MiLLL).</p> <p>For the MiLLL project:</p> <ul><li>a pair of WordPress Multisites, one for hosting open educational resource-based course sites - <a href="https://course.milll.ws">https://course.milll.ws</a>- and one hosting individual sites for Samoan primary and secondary schools - <a href="https://schools.milll.ws">https://schools.milll.ws</a> </li> <li>a BigBlueButton instance supporting learners, educators, and government staff's video conferencing requirements - <a href="https://bbb.milll.ws">https://bbb.milll.ws</a> </li> <li>a Mastodon instance especially for Samoans wanting to participate in digital social media - <a href="https://mastodon.milll.ws">https://mastodon.milll.ws</a> </li> <li>an instance of Rocket.Chat - <a href="https://chat.milll.ws">https://chat.milll.ws</a> </li> <li>an instance of Discourse - <a href="https://forum.milll.ws">https://forum.milll.ws</a> </li> <li>an instance of Moodle - <a href="https://moodle.milll.ws">https://moodle.milll.ws</a> </li> <li>an instance of BitWardern/VaultWarden - <a href="https://safe.milll.ws">https://safe.milll.ws</a> </li> </ul><p>For the <a href="https://col.org">Commonwealth of Learning</a>:</p> <ul><li>a WordPress Multisite for hosting OER-based course sites for the <a href="https://pacificpartnership.col.org/">Pacific Partnership in Open Distance and Flexible Learning initiative</a> - <a href="https://pacificopencourses.col.org">https://pacificopencourses.col.org</a> </li> </ul><p>For <a href="https://oeglobal.org">Open Education Global</a>:</p> <ul><li>a WordPress Multisite for hosting OER-based course sites - <a href="https://course.oeglobal.org">https://course.oeglobal.org</a> </li> </ul><h2><a id="user-content-costs-and-usage" href="#costs-and-usage" name="costs-and-usage" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Costs and Usage</h2> <p>In the past year we have served many thousands of registered users and over 200,000 anonymous learners access our courses (the full content of which are open to all without requiring authentication).</p> <p>It's also important to note that all of these services can be provided at no cost to our collaborators and learners as there are <i>no</i> per-seat license fees (nor <i>any</i> license fees) associated with any of the services. Our only costs are related to the fairly generic 'cloud'-based virtual servers (all running the FOSS Linux operating system) we hire from a host of competing commodity hosting providers.</p> <p>Our entire annual IT infrastructure cost, including for our 'on behalf' hosting partners, was comfortably less than USD10,000. What's more our usage monitoring suggests that we were operating below 10% capacity, meaning that we have a <i>lot</i> of additional headroom.</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=53&amp;2=field_blog_comments&amp;3=comment" token="7V4E5uyf0g3obVq8V89F0_43uNHUEiYYPXC9rAs6xDs"></drupal-render-placeholder> </div> </section> Mon, 08 Aug 2022 04:38:07 +0000 dave 53 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 Upgrading a Docker-based Discourse Forum instance http://tech.oeru.org/upgrading-docker-based-discourse-forum-instance <span class="field field--name-title field--type-string field--label-hidden">Upgrading a Docker-based Discourse Forum instance</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--discourse"> <span class="field__item-wrapper"><a href="/taxonomy/term/19" hreflang="en">discourse</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> </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 19/05/2022 - 14:45</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/Discourse-admin_page_upgrade_link.png?itok=SmTm00O2" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A sample of the admin page showing a Discourse administrator that an upgrade is pending, showing the web-upgrade link.&quot;}" role="button" title="A sample of the admin page showing a Discourse administrator that an upgrade is pending, showing the web-upgrade link." data-colorbox-gallery="gallery-field_image-6Xp_Sree6uw" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A sample of the admin page showing a Discourse administrator that an upgrade is pending, showing the web-upgrade link.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Discourse-admin_page_upgrade_link.png?itok=iBEbWRyb" width="220" height="144" alt="A sample of the admin page showing a Discourse administrator that an upgrade is pending, showing the web-upgrade link." 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/Discourse-upgrade_email_alert.png?itok=VBs3FoqS" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A sample of the email sent to Discourse admins by the Discourse system alerting them to a new pending upgrade.&quot;}" role="button" title="A sample of the email sent to Discourse admins by the Discourse system alerting them to a new pending upgrade." data-colorbox-gallery="gallery-field_image-6Xp_Sree6uw" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A sample of the email sent to Discourse admins by the Discourse system alerting them to a new pending upgrade.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Discourse-upgrade_email_alert.png?itok=M4tLaS3O" width="220" height="140" alt="A sample of the email sent to Discourse admins by the Discourse system alerting them to a new pending upgrade." 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/Discourse-backup_admin_page.png?itok=Lv0Hz0-Y" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A sample Discourse system backup administration page.&quot;}" role="button" title="A sample Discourse system backup administration page." data-colorbox-gallery="gallery-field_image-6Xp_Sree6uw" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A sample Discourse system backup administration page.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Discourse-backup_admin_page.png?itok=KZaf9Y0c" width="220" height="144" alt="A sample Discourse system backup administration page." 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/Discourse-web_upgrade.png?itok=xyJIWWoq" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A Discourse web upgrade status page, prior to initiating the upgrade.&quot;}" role="button" title="A Discourse web upgrade status page, prior to initiating the upgrade." data-colorbox-gallery="gallery-field_image-6Xp_Sree6uw" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A Discourse web upgrade status page, prior to initiating the upgrade.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Discourse-web_upgrade.png?itok=-NQk807A" width="220" height="124" alt="A Discourse web upgrade status page, prior to initiating the upgrade." 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/Discourse-web_upgrade_progress.png?itok=eAf0UUVc" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A Discourse web upgrade underway. &quot;}" role="button" title="A Discourse web upgrade underway. " data-colorbox-gallery="gallery-field_image-6Xp_Sree6uw" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A Discourse web upgrade underway. &quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Discourse-web_upgrade_progress.png?itok=Q6y7yjKe" width="220" height="136" alt="A Discourse web upgrade underway. " 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/Dicscourse-require_cli_upgrade.png?itok=VKj1mQll" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Sample of the message you see if a particular Discourse upgrade is too complicated for the web interface.&quot;}" role="button" title="Sample of the message you see if a particular Discourse upgrade is too complicated for the web interface." data-colorbox-gallery="gallery-field_image-6Xp_Sree6uw" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Sample of the message you see if a particular Discourse upgrade is too complicated for the web interface.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Dicscourse-require_cli_upgrade.png?itok=dVBM-OW2" width="220" height="94" alt="Sample of the message you see if a particular Discourse upgrade is too complicated for the web interface." 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><a href="https://discourse.org">Discourse</a> is the world-leading online web-based forum. It's a superb, extremely mature-and-yet-cutting edge platform. It also happens to be Free and Open Source Software, which is why we, at the <a href="https://oerfoundation.org">OER Foundation</a>, use it.</p> <p>Like all good software, it's undergoing continuous improvement by its developer community who release fairly frequent - perhaps every couple weeks - updates. Luckily, keeping your Discourse forum up-to-date isn't particularly onerous.</p> <p>Upgrading a Discourse instance deployed via Docker (as all of ours are) follows one of two possible workflows. Both usually start with either a Discourse administrator logging into the Discourse site (which she/he might not do very often) and noticing on the default admin dashboard page (see the first attached screenshot) that there is a pending upgrade, usually with a 'click here to upgrade' link. Alternatively, if an administrator is not logged in (the system is aware of this) the system sends administrators for the site an email (see the second attached screenshot for a sample) alerting them all that an upgrade is pending, with a link to the upgrade page in the Discourse administration system.</p> <h2><a id="user-content-always-backup-first" href="#always-backup-first" name="always-backup-first" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Always backup first!</h2> <p>As any good system administrator knows, their users' data is of utmost importance. So, before proceeding with any upgrade, it's crucial to make a backup of your system to ensure that if everything goes pear-shaped, you can recover and get back to where you were.</p> <p>Normally, I consider it sufficient to take a Discourse 'backup' via the admin backup interface 'immediately before doing the upgrade' (see attached screenshot), leaving ''off'' the uploaded content, as this is unlikely to be affected or removed via the upgrade process, to save space. Doing this immediately prior to the upgrade minimises the chances that any users lose data (e.g. a recent post, in the space of time between the backup and the actual upgrade or even a post they're working on when the upgrade commences - if the latter happens, as long as they don't leave the page and wait for the forum to 'come back' after the upgrade, they should be able to proceed without losing anything... but there's a small chance they could be unlucky - be mindful of this and try to a) announce upgrades on the site prior to running them, and b) try to do so outside of your busiest hours, which should be easy to determine thanks to Discourse's superb analytics (also a feature of the default admin dashboard).</p> <p>After conducting the backup, you can do the upgrade.</p> <h2><a id="user-content-upgrade-workflows" href="#upgrade-workflows" name="upgrade-workflows" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Upgrade Workflows</h2> <p>You will ''usually'' have a straightforward 'point-and-click' web browser based upgrade process, but on occasion, you will click on the 'upgrade link' and end up on a page like 'upgrade is too complicated for the web interface' message (see attached screenshot). In that latter case, things get a little more complicated. Also, such upgrades can result in somewhat more downtime for the forum, so only conduct them when you have sufficient time.</p> <h3><a id="user-content-web-based-upgrade" href="#web-based-upgrade" name="web-based-upgrade" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Web-based upgrade</h3> <p>In most cases, when you click on the "Click here to upgrade" link, you'll be taken to a page that assesses the various components (the Discourse codebase, the Docker subsystems, and any additional functional plugins) making up your Discourse install and alerts you which of those components have a pending upgrade. This can sometimes take a while (the system is having to compare what you have on your server to the official up-to-date versions of those things on the web), but when done, the page will show you 'upgrade' buttons for the components with upgrades pending as per the attached sample upgrade pages screenshot.</p> <p>Once you've got those showing, you can click the 'Upgrade' button that's highlighted (if any - that one needs to be done first, prior to the others) and then the 'Start upgrade" operation for each one. Or, sometimes, you'll see an "Upgrade all" button at the top of the page, which applies all the pending upgrades in a single operation.</p> <p>Usually, in practice, this renders your Discourse forum unusable (it'll be in 'maintenance mode' from a user's perspective) for a few seconds to a few minutes, depending on the upgrade.</p> <h3><a id="user-content-command-line-launcher-upgrade" href="#command-line-launcher-upgrade" name="command-line-launcher-upgrade" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Command line 'launcher' upgrade</h3> <p>When the upgrade is more fundamental, usually involving the Docker subsystem, you sometimes don't have the option of a web-based upgrade. Instead of the upgrade web pages from the previous section, you get message like that shown in the next screenshot, which talks about running the upgrade from the command line using the <code>launcher</code> command and doing a 'rebuild' of the system.</p> <p>In that case, you need to be able to log into the host server (usually a remote cloud-based "Virtual Private Server" or VPS) - I always use SSH to do that <a href="/node/49">see these instructions</a> from a terminal on my Linux desktop - with suitable permissions (usually the ability to run 'sudo' commands, i.e. act as the administrative or 'root' user of that server).</p> <p>Once you're logged in as a user with 'sudo' privileges, you need to go to the Docker configuration directory for that instance of Discourse - in the OER Foundation's case, I have the convention of having the installation in the <code>/home/docker/</code> directory in a subdirectory with the same name as the as the site, e.g. forum.oeru.org. In that case, I'd enter</p> <p><code>cd /home/docker/forum.oeru.org</code></p> <p>Note that we don't use the default path for our Discourse installation (<code>/var/discourse</code>), so we're being rebels here. Doing an</p> <p><code>ls -l</code></p> <p>here should give a directory that looks a bit like this (you might not have the 'oeru'-related files) when listed:</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre><span>-rw-rw-r--</span> <span>1</span> dave dave <span>1099</span> Nov <span>29</span> <span>2019</span> LICENSE <span>-rw-r--r--</span> <span>1</span> dave root <span>8285</span> Jul <span>24</span> <span>2021</span> README.md <span>-rw-rw-r--</span> <span>1</span> dave dave <span>258</span> Dec <span>2</span> <span>2019</span> README.oeru drwxrwxr-x <span>1</span> dave dave <span>16</span> Nov <span>29</span> <span>2019</span> bin drwxrwxr-x <span>1</span> dave dave <span>16</span> Apr <span>15</span> <span>23</span>:<span>36</span> cids drwxrwxr-x <span>1</span> dave dave <span>30</span> May <span>19</span> 06:<span>47</span> containers <span>-rwxrwxr-x</span> <span>1</span> dave dave <span>11955</span> Apr <span>15</span> <span>23</span>:<span>22</span> discourse-doctor <span>-rwxr-xr-x</span> <span>1</span> dave root <span>27132</span> Oct <span>21</span> <span>2021</span> discourse-setup drwxrwxr-x <span>1</span> dave dave <span>192</span> Dec <span>22</span> 04:<span>43</span> image <span>-rwxrwxr-x</span> <span>1</span> dave dave <span>23538</span> Apr <span>15</span> <span>23</span>:<span>22</span> launcher drwxrwxr-x <span>1</span> dave dave <span>120</span> Nov <span>16</span> <span>2021</span> samples drwxrwxr-x <span>1</span> dave dave <span>22</span> Nov <span>29</span> <span>2019</span> scripts drwxrwxr-x <span>1</span> dave dave <span>16</span> Nov <span>29</span> <span>2019</span> shared drwxrwxr-x <span>1</span> dave dave <span>866</span> Apr <span>15</span> <span>23</span>:<span>22</span> templates drwxr-xr-x <span>1</span> dave root <span>130</span> Jan <span>16</span> <span>22</span>:<span>42</span> tests</pre></div></div> <p>The script we're going to use is the <code>launcher</code> script, and we're going to use the site's configuration which, by our convention, will reside in <code>containers/app.yml</code>(you can see its contents by typing <code>less containers/app.yml</code> assuming your user has sufficient permissions to view it directly. If not, prepend <code>sudo</code> to your command.</p> <p>To run the upgrade, you have to complete two steps as suggested by the page you saw when you clicked on the upgrade link:</p> <p>First, ensure that the 'git' repository that was used to provide this set of files to begin with is up-to-date - if your user owns these files (as my 'dave' user does in the example above) then run this to avoid changing the ownership of the files:</p> <p><code>git pull</code></p> <p>If you don't own the files you'll need to run</p> <p><code>sudo git pull</code></p> <p>which should result in a fairly quick (a few seconds) request to the git server, pulling down files that have been changed by the developers since the last time you did a <code>git pull</code>... Note, if the pull fails, you may find that you've altered one or more files for some reason and that the change you've made would be clobbered by the pull. The easiest thing to do in that case is - for each file - to (prepending with <code>sudo</code> if required):</p> <p><code>cp [filename] [filename].bak</code></p> <p>so you have a backup copy of your changed file, and then issue (again, with <code>sudo</code> prepended if necessary):</p> <p><code>git checkout [filename]</code></p> <p>which reverts [filename] to its original git-distributed version. The <code>git pull</code> should now work.</p> <p>Second, simply type (again, prepending with <code>sudo</code> if required:</p> <p><code>./launcher rebuild app</code></p> <p>This will initiate what can be a rather long process (perhaps 1 to as many as, say, 30 minutes, depending on the upgrade, the amount that needs to be downloaded, your server's network bandwidth, and a variety of other variables) which will involve downloading new Docker containers with Discourse code, as well as possibly a new PostgreSQL database container among others. Your Discourse will continue running as long as it can safely do so, but then the <code>launcher</code> script will shut down the containers and restart them. It might also be 'recompiling' aggregated resources used by Discourse, like various Ruby On Rails gems, icon files, style sheets (CSS), or Javascript files...</p> <p>Once it's done, you'll get the command prompt back. You're done! Have a look at your site and you should see you're up-to-date!</p> <p>You can log out of the server vai <code>CTRL-D</code> or typing <code>exit</code>.</p> <h3><a id="user-content-problems" href="#problems" name="problems" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Problems?</h3> <p>I have, on a few occasions, had an upgrade fail for some reason. If that happens, you should see an indicator of has caused the failure (in a few cases for me, it was the fact that the PostgreSQL database didn't have enough time to restart before the Discourse app tried to access it to run data schema upgrades, and failed as a result. Changing the 'timeout' value from 5 seconds to 60 seconds fixed it - looking on the <a href="https://meta.discourse.org">Meta Discourse</a> - Discourse's own Discourse used to discuss running Discourse - is a very good resource when trying to solve system-related problems. It's worth creating an account for yourself on it if you're going to be responsible for a Discourse instance!</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-1049" class="comment js-comment by-anonymous has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/1049#comment-1049" class="permalink" rel="bookmark" hreflang="en">I had to use the command `…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1686109077"></span> </h3> <div class="comment__meta"> <div class="comment__submitted"> <span class="comment__author"><span>Peter N Lewis (not verified)</span></span> <span class="comment__pubdate">Thu 23/03/2023 - 22:28</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>I had to use the command `./launcher rebuild app` (no `.yml`) for this to work.</p> </div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=1049&amp;1=default&amp;2=en&amp;3=" token="PNviJRTGbHkCAoilRGpDUNUQC6hl89VkaEuOf-3u9hI"></drupal-render-placeholder> </div> </div> </article> <div class="indented"><article data-comment-user-id="1" id="comment-1119" class="comment js-comment by-node-author has-title clearfix"> <div class="comment__container"> <h3 class="comment__title"> <a href="/comment/1119#comment-1119" class="permalink" rel="bookmark" hreflang="en">Well spotted, Peter, and…</a> <span class="comment__new marker marker--success hidden" data-comment-timestamp="1686109268"></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">Wed 07/06/2023 - 15:41</span> </div> </div> <div class="comment__content"> <p class="comment__parent visually-hidden">In reply to <a href="/comment/1049#comment-1049" class="permalink" rel="bookmark" hreflang="en">I had to use the command `…</a> by <span>Peter N Lewis (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>Well spotted, Peter, and thanks for letting me know! I've corrected the tutorial.</p></div> </div> </div> <drupal-render-placeholder callback="comment.lazy_builders:renderLinks" arguments="0=1119&amp;1=default&amp;2=en&amp;3=" token="25VArLyKXIddj7W-MfTtlFiVQlNmnrz26WLth5t4i6o"></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=51&amp;2=field_blog_comments&amp;3=comment" token="sEqniUwHCHUWHihRflAm8YgWK0q2UAy7k0fiHG3-09Y"></drupal-render-placeholder> </div> </section> Thu, 19 May 2022 02:45:12 +0000 dave 51 at http://tech.oeru.org Creating Linux shell users with sudo and SSH access http://tech.oeru.org/creating-linux-shell-users-sudo-and-ssh-access <span class="field field--name-title field--type-string field--label-hidden">Creating Linux shell users with sudo and SSH access</span> <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 10/05/2022 - 11:33</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/DigitalOceanConsoleScreenshot.png?itok=KO2YM-KO" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Screenshot of Digital Ocean&#039;s browser-based console, logged into VPS as the root user.&quot;}" role="button" title="Screenshot of Digital Ocean&#039;s browser-based console, logged into VPS as the root user." data-colorbox-gallery="gallery-field_image-AreByFWOE2o" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Screenshot of Digital Ocean&#039;s browser-based console, logged into VPS as the root user.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/DigitalOceanConsoleScreenshot.png?itok=4qAWYCEw" width="220" height="158" alt="Screenshot of Digital Ocean&#039;s browser-based console, logged into VPS as the root user." 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/Screenshot%202022-05-10%20at%2013-44-16%20mesc.oerfoundation.org%20-%20DigitalOcean-modified.png?itok=FuNxpcjc" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;Screenshot of Digital Ocean browser interface, viewing a VPS, showing the link for launching the &#039;console&#039;.&quot;}" role="button" title="Screenshot of Digital Ocean browser interface, viewing a VPS, showing the link for launching the &#039;console&#039;." data-colorbox-gallery="gallery-field_image-AreByFWOE2o" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;Screenshot of Digital Ocean browser interface, viewing a VPS, showing the link for launching the &#039;console&#039;.&quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/Screenshot%202022-05-10%20at%2013-44-16%20mesc.oerfoundation.org%20-%20DigitalOcean-modified.png?itok=F1e41LUo" width="220" height="155" alt="Screenshot of Digital Ocean browser interface, viewing a VPS, showing the link for launching the &#039;console&#039;." 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/default-login-prompt-screenshot.png?itok=_AfI5XKT" aria-controls="colorbox" aria-label="{&quot;alt&quot;:&quot;A default VPS login screen. Note, colours and fonts and sizes of things are likely to vary. &quot;}" role="button" title="A default VPS login screen. Note, colours and fonts and sizes of things are likely to vary. " data-colorbox-gallery="gallery-field_image-AreByFWOE2o" class="colorbox" data-cbox-img-attrs="{&quot;alt&quot;:&quot;A default VPS login screen. Note, colours and fonts and sizes of things are likely to vary. &quot;}"><img src="/sites/default/files/styles/medium/public/2022-05/default-login-prompt-screenshot.png?itok=gS6CCN9A" width="220" height="115" alt="A default VPS login screen. Note, colours and fonts and sizes of things are likely to vary. " 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>With Linux servers, both local and in "the Cloud" (aka a Virtual Private Server or VPS), access is generally remote via a command line interface. This tutorial covers the process of creating normal shell (command line) users who have the ability to perform administrative tasks (via the 'sudo' - Super User DO - mechanism), as well as configuring that user to be able to log in from their workstation via SecureShell (SSH) without needing to enter a password (i.e. using public-private key pairs).</p> <p>To begin with, we need a Linux server to which we have administrative access. This can be direct access to a local Linux server on our own infrastructure, or a remote VPS for which we have the 'root' (administrative user) access, either via SSH (to a default root or unprivileged user created by the VPS provider) or via a browser-based console (which simulates sitting down at monitor/keyboard at the actual server itself).</p> <p>You'll log into your server either via your web browser (see attached images of the Digital Ocean VPS control page, with console link, and a browser-based console session, logged in as the 'root' user), or via SSH from your workstation. If you are on Linux or MacOS, you can use the in-built SSH client (you'll want to create an SSH key-pair for yourself first - on <a href="https://www.digitalocean.com/community/tutorials/how-to-set-up-ssh-keys-on-ubuntu-20-04">Linux</a>, on a <a href="https://pagely.com/quickstart/firehose/ssh/mac/generating-key-pairs/">Mac</a>, or on <a href="https://www.howtogeek.com/762863/how-to-generate-ssh-keys-in-windows-10-and-windows-11/">Windows</a>. Note: I normally leave the 'passphrase' blank - it's slightly less secure especially if your SSH keys are on a computer you share with other people, but it's a <strong>lot</strong> more convenient, and a trade-off I'm willing to make!</p> <p>You can address your server either by its IP address (either IPv4, e.g. 137.184.84.159, or if you're one of the rare far-sighted people with an IPv6 address, e.g. 2604:a880:4:1d0::4a2:c000, you can use that instead).</p> <p>If you'd like to create your own VPS using a cloud infrastructure provider like Digital Ocean, and perhaps even configure a domain name to point to it (so you can use that domain to address your server rather than an IP address, see the <a href="/node/42">first couple sections of this tutorial</a>.</p> <h2><a id="user-content-logging-in" href="#logging-in" name="logging-in" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Logging in</h2> <p>Logging into your VPS will depend on whether you're using a console or logging in via SSH. If via a console, you'll just need to enter the username (possibly 'root') and password you set when creating the VPS. If via SSH, you'll run</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). If your VPS is configured for passwordless logins, you shouldn't need a password. If not, you'll need the password you created when provisioning the VPS. Usually, it's either one or the other. If you're running Putty, you'll enter the same details into the Putty interface.</p> <p>In most cases, if you've never logged into that VPS via SSH previously, you're likely to see a notification like this right after entering the command (with your VPS' hostname and IP address and its own ECDSA key fingerprint - it'll be unique to each server):</p> <div class="geshifilter"><div class="bash geshifilter-bash"><pre>The authenticity of host <span>'sandbox.milll.ws (163.12.232.115)'</span> can<span>'t be established. ECDSA key fingerprint is SHA256:0doRcAm9kAZjraohX+CeCwv1Yv6bIbzAYUSTrWGc9Y4. Are you sure you want to continue connecting (yes/no/[fingerprint])? </span></pre></div></div> <p>Type 'yes' and ENTER. You should only ever see this once for any IP or domain name you use to access this particular VPS.</p> <p>Once you're logged in, you'll see a command line prompt, probably somewhat similar to that of the attached screenshot. The name of the VPS you've logged into and your username will normally (by convention) be specified in your command prompt. In the attached screenshot, you can see my prompt is <code>dave@mesc:~$</code>. That means I'm logged in as user <code>dave</code>, the VPS's short-name is <code>mesc</code> (it's long name is <code>mesc.oerfoundation.org</code> which is too long to include in a prompt) and my current location is <code>~</code>, the 'tilde' character which is short-hand on a UNIX shell for the user's 'home directory'. On a Linux VPS, it's likely to be in <code>/home/[username]</code> or <code>/home/dave</code> in my user's case.</p> <p>Note, on a UNIX system, directories (aka folders) are delineated with a forward slash, <code>/</code>. Windows is the only system that uses a backslash <code>\</code> to delineate directories.</p> <p>The <code>$</code> at the end of the prompt tells you you're an 'unprivileged user'. If you were logged in as an admin user, usually 'root', you'd see a <code>#</code> in your prompt to remind you that you've got unlimited power within the VPS, and to use it carefully!</p> <p>If your prompt doesn't show your username, you can find out what user you're logged in as by typing</p> <p><code>whoami</code></p> <h2><a id="user-content-creating-a-regular-user" href="#creating-a-regular-user" name="creating-a-regular-user" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Creating a regular user</h2> <p>Let's say you're creating a user with the username of 'fiafia' - <em>by convention, usernames on Linux systems are all lower case letters, without spaces or punctuation</em>. Assuming your user has 'sudo' privileges, you can create the user like this (you will be asked to enter your user's password the first time you use <code>sudo</code>, and again after not using it for a while, usually 10 minutes):</p> <p><code>sudo adduser fiafia</code></p> <p>That will request that you enter additional information, like the user's full name, a phone number, and even an office room number (you can leave out any unnecessary information), and it will create a home directory for the user, <code>/home/fiafia</code> in this case.</p> <p>It will also require you to set a password for the new user. If you're not sure how to generate a good random password, you can user our <a href="/node/43">short tutorial</a>. Should you want to change it you can set it like this - your user will need to know their password in order to run <code>sudo</code> commands:</p> <p><code>sudo passwd $U</code></p> <p>which will instruct you to enter a strong password twice (to guard against typos).</p> <p>To give fiafia the ability to log in via SSH and to allow her to run administrative commands, her user needs to be added to several 'groups', members of which get additional privileges:</p> <p><code>sudo adduser fiafia admin</code><br /><code>sudo adduser fiafia sudo</code></p> <p>Those groups, <code>ssh</code>, <code>admin</code>, and <code>sudo</code> will confer those abilities on your new user.</p> <p>If you're working with VPS running a version of Ubuntu prior to 22.04 (like 20.04) or perhaps a Debian variant, you might also need to add the user to the 'ssh' group (it appears that with 22.04, Ubuntu have decided the ssh group was unnecessary and have removed it).</p> <p>Just to be safe, if you run this on 22.04 too, it might fail to complete (saying something like "there's no 'ssh' group"), but that's not a problem.</p> <p><code>sudo adduser fiafia ssh</code></p> <p>If you wanted to create several users, you could avoid typing in each user's name so many times by instead setting a variable in the shell, say <code>U</code>, to the username and then reference the value of U, which you do by putting a '$' in front of it, like <code>$U</code>. So to create another new user, say 'masina', you could do the same thing like this, and copy and paste the following into your terminal window (If pasting via 'CTRL-V' doesn't work, try 'SHIFT+CTRL-V', similarly copy via 'SHIFT+CTRL-C'):</p> <p><code>U=masina</code><br /><code>sudo adduser $U</code><br /><code>sudo adduser $U admin</code><br /><code>sudo adduser $U sudo</code></p> <p>(Note: to repeat these and the following commands for another user, just assign a new username to U, <code>U=[newusername]</code> and re-paste the same commands with <code>$U</code>.)</p> <p>Because, by following this tutorial, your user is likely to log in without using their password, it makes sense to put it somewhere safe where they can find it. I usually do it this way (replace [password] with the same password you used for the previous command):</p> <p><code>sudo echo "[password]" &gt; /home/$U/.password</code></p> <p>which, in this case, will put the password in a 'hidden' file (files or directories with names starting with a '.' are called hidden files or directories - they won't appear in a normal directory listing, e.g. <code>ls</code> or <code>ls -l</code>. If you want to see them, try <code>ls -la</code> where the 'a' means 'all') called <code>.password</code> in the user masina's home directory, <code>/home/masina</code>.</p> <p>You will also want to make sure that your new user has permission to remove that file. The right way to do that is to transfer ownership of the file to that user:</p> <p><code>sudo chown $U /home/$U/.password</code></p> <h2><a id="user-content-creating-ssh-keys-on-your-vps" href="#creating-ssh-keys-on-your-vps" name="creating-ssh-keys-on-your-vps" class="heading-permalink" aria-hidden="true" title="Permalink"></a>Creating SSH keys on your VPS</h2> <p>Now we can create an SSH key pair for this new user on this VPS. Helpfully, in the process, the <code>.ssh</code> (hidden) directory for the file into which to put that user's <em>workstation</em> public SSH key to allow them passwordless logins from that workstation:</p> <p><code>sudo su $U</code></p> <p>That command allows your user to "become" the user $U and execute the following commands (the 'sudo' isn't needed here as that user is working in her/his own space with her/his own files):</p> <p><code>ssh-keygen -t rsa -b 2048</code></p> <p>This command uses the SSH keygen utility to create a 2048bit keypair: a public and a private key which provide the means for engaging in full encrypted (and extremely secure!) interactions with a remote server via the SSH protocol. You'll have to hit "ENTER" three times to complete the process (as I said above, I normally leave the passphrase blank).</p> <p>Once that is done, you can create and edit a new file:</p> <p><code>nano ~/.ssh/authorized_keys</code></p> <p>In that file, you can copy and paste (without spaces on either end) that user's <em>workstation's</em> <strong>public</strong> ssh key (<em>never publish</em> a private key anywhere!), save and close the file (use CTRL-X).</p> <p>The quickest way to do that on a Linux computer is by running the command (back on your workstation!):</p> <p><code>cat ~/.ssh/id_rsa.pub</code></p> <p>which will print the contents of your public key file below it, so you can easily highlight and copy &amp; paste it (remember, a copy from a Linux terminal is SHIFT+CTRL+C).</p> <p>When you're done pasting the public key into your <code>authorized_keys</code> file, save and exit the editor (CTRL-X) and type</p> <p><code>exit</code></p> <p>The <code>exit</code> will return you to your own user. You can use CTRL-D (slightly faster to type) to do the same thing.</p> <p>From that point, that user should be able to SSH to the VPS 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></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=49&amp;2=field_blog_comments&amp;3=comment" token="36KGpyt9VtFQVtBToeZLjHkiDNTzg-D6hmycl2L8HYQ"></drupal-render-placeholder> </div> </section> Mon, 09 May 2022 23:33:58 +0000 dave 49 at http://tech.oeru.org