<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0" xmlns:media="http://search.yahoo.com/mrss/"><channel><title><![CDATA[cloudflare - fe84]]></title><description><![CDATA[> notes to self _]]></description><link>https://blog.foureight84.com/</link><image><url>https://blog.foureight84.com/favicon.png</url><title>cloudflare - fe84</title><link>https://blog.foureight84.com/</link></image><generator>Ghost 4.8</generator><lastBuildDate>Sat, 11 Apr 2026 04:21:16 GMT</lastBuildDate><atom:link href="https://blog.foureight84.com/tag/cloudflare/rss/" rel="self" type="application/rss+xml"/><ttl>60</ttl><item><title><![CDATA[Pi-Hole Recursive DNS with Unbound]]></title><description><![CDATA[Deploying Unbound recursive DNS server locally with Pi-Hole using Docker]]></description><link>https://blog.foureight84.com/pi-hole-recursive-dns/</link><guid isPermaLink="false">61bb93cf0a679a00014c664d</guid><category><![CDATA[adblocking]]></category><category><![CDATA[pihole]]></category><category><![CDATA[unbound]]></category><category><![CDATA[recursive dns]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[swarm]]></category><category><![CDATA[traefik]]></category><dc:creator><![CDATA[foureight84]]></dc:creator><pubDate>Thu, 16 Dec 2021 20:42:08 GMT</pubDate><media:content url="https://blog.foureight84.com/content/images/2021/12/Unbound_FC_Shaded_cropped.svg" medium="image"/><content:encoded><![CDATA[<img src="https://blog.foureight84.com/content/images/2021/12/Unbound_FC_Shaded_cropped.svg" alt="Pi-Hole Recursive DNS with Unbound"><p>In the previous post, I wrote an extensive guide on <a href="https://blog.foureight84.com/swarm-your-pihole">deploying Cloudflare as the upstream DNS for Pi-Hole over HTTPS</a>. This is a follow-up where Cloudflare is replaced with <a href="https://www.nlnetlabs.nl/projects/unbound/about/">Unbound</a> as the upstream DNS server. </p><p>Unbound is a recursive DNS that sits between Pi-Hole and authoritative DNS servers. Cloudflare&apos;s 1.1.1.1 and Google&apos;s 8.8.8.8 are examples of recursive DNS services. By making Unbound the upstream DNS server for Pi-Hole, you&apos;re cutting out other third parties from tracking your web presence. A more detailed read-up of this setup can be found on the official <a href="https://docs.pi-hole.net/guides/dns/unbound/">Pi-Hole Unbound guide</a>.</p><p>Back on June 11, 2021, Cloudflare DNS experienced outages in the Los Angeles and Chicago area. The result was over an hour of downtime with its DNS service. I was able to avoid that outage by switching over to Unbound and letting it handle domain resolution directly with authoritative DNS servers. This method can be a little slow but DNS caching in Pi-Hole becomes beneficial on subsequent lookups.</p><h2 id="deploying-unbound-with-pi-hole">Deploying Unbound with Pi-Hole</h2><p>In the previous post, Pi-Hole and Cloudflare DNS were deployed using Docker Swarm and managed through Portainer with Traefik as the reverse proxy. This will follow the previous guide closely. Let&apos;s start by cloning the project:</p><pre><code class="language-bash">git clone https://github.com/foureight84/traefik-pihole-doh.git &amp;&amp; cd traefik-pihole-doh</code></pre><p>This guide assumes that your Docker Swarm, Portainer, and Traefik have been properly configured. If not, <a href="https://blog.foureight84.com/swarm-your-pihole/#getting-started">follow this guide</a>.</p><p>Go to your Portainer web portal and click on App Templates -&gt; Custom Templates and click on the &quot;+ Add Custom Template&quot; button.</p><!--kg-card-begin: markdown--><blockquote>
<p>This stack uses Unbound Docker image created by Kyle Harding (<a href="https://github.com/klutchell">https://github.com/klutchell</a>)<br>
Image: <a href="https://hub.docker.com/r/klutchell/unbound">https://hub.docker.com/r/klutchell/unbound</a><br>
Github: <a href="https://github.com/klutchell/unbound-docker">https://github.com/klutchell/unbound-docker</a></p>
</blockquote>
<!--kg-card-end: markdown--><p>You&apos;ll need to fill in a relevant title for the template. I called mine <code>recursive_dns</code>. Add a description - <code>Pi-hole and Unbound</code>. Make sure the template Type is set to <code>Swarm</code>. Then click on the Upload option and choose the <code>docker-compose.yaml</code> in the <code>dns-unbound</code> folder in the cloned project.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/12/Pi-hole-Unbound-Portainer.PNG" class="kg-image" alt="Pi-Hole Recursive DNS with Unbound" loading="lazy" width="1406" height="1002" srcset="https://blog.foureight84.com/content/images/size/w600/2021/12/Pi-hole-Unbound-Portainer.PNG 600w, https://blog.foureight84.com/content/images/size/w1000/2021/12/Pi-hole-Unbound-Portainer.PNG 1000w, https://blog.foureight84.com/content/images/2021/12/Pi-hole-Unbound-Portainer.PNG 1406w" sizes="(min-width: 720px) 720px"><figcaption>Uploading dns-unbound/docker-compose.yaml as a Swarm template</figcaption></figure><!--kg-card-begin: markdown--><p><s>After it has been uploaded, find the newly created custom template in the list of templates and click edit.<br>
We will need to check that <code>PIHOLE_DNS_=172.18.0.1#5053</code> environment variable matches your <code>docker_gwbridge</code> IPV4 IPAM Gateway address. Once verified, deploy the stack. That&apos;s it!</s></p>
<p>Deploy the stack once the custom template has been uploaded. The <code>klutchell/unbound</code> Docker image now listens on port 53 by default. Setting the <code>PIHOLE_DNS</code> environment variable to the <code>unbound</code> service name is all that&apos;s needed.</p>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><aside class="note">Make sure you&apos;re only running one instance of Pi-Hole. If you are running Pi-Hole with Cloudflare from the previous guide, be sure to remove that stack before deploying this stack.</aside><!--kg-card-end: html--><p>While uncached DNS queries may be slower than using Google&apos;s public DNS (8.8.8.8) we can see that Pi-Hole&apos;s caching outpaces all other public DNS services by far. Plus the millisecond differences in uncached queries are not noticeable in a real use case scenario. It&apos;s actually faster than using Cloudflare!</p><figure class="kg-card kg-image-card"><img src="https://blog.foureight84.com/content/images/2021/12/image.png" class="kg-image" alt="Pi-Hole Recursive DNS with Unbound" loading="lazy" width="596" height="928"></figure><pre><code>  192.168.  1.  4 |  Min  |  Avg  |  Max  |Std.Dev|Reliab%|
  ----------------+-------+-------+-------+-------+-------+
  + Cached Name   | 0.000 | 0.001 | 0.001 | 0.000 | 100.0 |
  + Uncached Name | 0.015 | 0.060 | 0.187 | 0.052 | 100.0 |
  + DotCom Lookup | 0.015 | 0.049 | 0.081 | 0.022 | 100.0 |
  ---&lt;O-OO----&gt;---+-------+-------+-------+-------+-------+
                     pihole.home
                Local Network Nameserver


    1.  1.  1.  1 |  Min  |  Avg  |  Max  |Std.Dev|Reliab%|
  ----------------+-------+-------+-------+-------+-------+
  - Cached Name   | 0.012 | 0.013 | 0.018 | 0.001 | 100.0 |
  - Uncached Name | 0.014 | 0.069 | 0.355 | 0.077 | 100.0 |
  - DotCom Lookup | 0.014 | 0.022 | 0.048 | 0.009 | 100.0 |
  ---&lt;--------&gt;---+-------+-------+-------+-------+-------+
                     one.one.one.one
                    CLOUDFLARENET, US
                    
    8.  8.  8.  8 |  Min  |  Avg  |  Max  |Std.Dev|Reliab%|
  ----------------+-------+-------+-------+-------+-------+
  - Cached Name   | 0.012 | 0.015 | 0.023 | 0.002 | 100.0 |
  - Uncached Name | 0.014 | 0.038 | 0.158 | 0.040 | 100.0 |
  - DotCom Lookup | 0.014 | 0.016 | 0.025 | 0.002 | 100.0 |
  ---&lt;--------&gt;---+-------+-------+-------+-------+-------+
                       dns.google
                       GOOGLE, US</code></pre>]]></content:encoded></item><item><title><![CDATA[Swarm Your Pi-hole]]></title><description><![CDATA[Deploy Pi-hole with DNS-over-HTTPS using Docker Swarm and load balance using Traefik]]></description><link>https://blog.foureight84.com/swarm-your-pihole/</link><guid isPermaLink="false">60ef44ce941c380001abbe8c</guid><category><![CDATA[docker]]></category><category><![CDATA[swarm]]></category><category><![CDATA[pihole]]></category><category><![CDATA[adblocking]]></category><category><![CDATA[dns-over-https]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[traefik]]></category><category><![CDATA[load balance]]></category><dc:creator><![CDATA[foureight84]]></dc:creator><pubDate>Fri, 16 Jul 2021 20:20:22 GMT</pubDate><media:content url="https://blog.foureight84.com/content/images/2021/07/91841822-4a3a5900-ec53-11ea-92fe-4bde2acccac4-1.png" medium="image"/><content:encoded><![CDATA[<img src="https://blog.foureight84.com/content/images/2021/07/91841822-4a3a5900-ec53-11ea-92fe-4bde2acccac4-1.png" alt="Swarm Your Pi-hole"><p>I&apos;ve been using Pi-hole as a whole network ad blocker for a while now and it&apos;s been great. The only mistake I made the first time was using it as a standalone docker instance. Whenever I perform a manual update to a newer release, there would be a few minutes of internet downtime. Personally, it&apos;s not a big deal and I could just force a DHCP-renew. But I share my internet connection with my parents living next door and an internet outage immediately leads to a phone call.</p><p>There are various ways to update a running Docker container and minimize downtime, and I&apos;ve chosen to update my stack to use Docker Swarm to accomplish this goal. Yes, the setup is overkill for a home network, but making things causal is always better down the road when you need to make changes.</p><p>I am currently using a J5005 Dell Wyse 5070 thin client as my server, but this article should apply to a Raspberry Pi. You should take a look at the Docker images used and swap them for ARM-compatible.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/dns_stack_topology.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="2000" height="732" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/dns_stack_topology.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/dns_stack_topology.png 1000w, https://blog.foureight84.com/content/images/size/w1600/2021/07/dns_stack_topology.png 1600w, https://blog.foureight84.com/content/images/2021/07/dns_stack_topology.png 2220w" sizes="(min-width: 720px) 720px"><figcaption>DNS Typology</figcaption></figure><!--kg-card-begin: markdown--><blockquote>
<p><strong>Disclaimer</strong>: This is meant for home use only. DO NOT deploy this in a Production environment. Pi-hole uses SQLite and concurrent write is not supported. While DNS resolution and blocking won&apos;t have issues, there will be loss of log data if two or more swarm nodes are writing to DB at the same time. Also, make sure your router is properly configured to not expose your DNS to the public internet. It will crash and burn.</p>
</blockquote>
<!--kg-card-end: markdown--><h3 id="stack-overview">Stack Overview</h3><!--kg-card-begin: markdown--><ul>
<li>Management Stack (mgmt)
<ul>
<li>Traefik - reverse proxy and load balancer</li>
<li>Portainer - Web UI Docker management tool</li>
<li>Whoami - Tiny Go webserver that prints os information and HTTP request to output</li>
</ul>
</li>
<li>DNS Stack (dns)
<ul>
<li>Cloudflared DoH Proxy</li>
<li>Pi-hole DNS adblocker and DHCP server</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><h3 id="a-few-details">A few Details</h3><p><a href="https://doc.traefik.io/traefik/">Traefik</a> is a GoLang reverse proxy and load-balancer with built-in support for Let&apos;s Encrypt. In my earlier post about using <a href="https://blog.foureight84.com/deploying-ghost-on-linode-with-cheap-remote-backup-using-terraform/">Terraform to deploy this Ghost blog</a>, I used <a href="https://hub.docker.com/r/jwilder/nginx-proxy">jwilder/nginx-proxy</a> along with <a href="https://hub.docker.com/r/jrcs/letsencrypt-nginx-proxy-companion/">jrcs/letsencrypt-nginx-proxy-companion</a> to serve as a reverse-proxy and automatic SSL certificate handling. However, that setup is on a Docker standalone deployment using Docker Compose. While it is possible to use an Nginx reverse-proxy with Docker Swarm, Traefik is a much better fit with Docker Swarm with fewer configurations involved.</p><p><a href="https://www.portainer.io/products/community-edition">Portainer</a> is a container management tool with a web UI. It&apos;s used in this project to manage the DNS swarm, which makes scaling and service updates more casual. I personally prefer this to keep the project simple and doesn&apos;t require looking up Docker CLI commands.</p><p><a href="https://pi-hole.net/">Pi-hole</a>, the main reason for this project, is a Linux based DNS server with built-in DHCP support. The main use for Pi-hole is to block advertisement domains, but it also has additional benefits in blocking known malware and phishing domains as well. As it is a DNS server, it can be used to apply to an entire network.</p><h3 id="getting-started">Getting Started</h3><p>We&apos;ll first need to clone the project</p><pre><code class="language-bash">git clone https://github.com/foureight84/traefik-pihole-doh.git &amp;&amp; cd traefik-pihole-doh</code></pre><p>You&apos;ll need to enable Docker swarm on your host if not already.</p><pre><code class="language-bash">docker swarm init</code></pre><p>We will need to create a Docker overlay network called <code>traefik</code>. Additional Docker services you choose to add later that require the use of Traefik will need to be attached to this network. </p><pre><code class="language-bash">docker network create --driver=overlay --attachable traefik</code></pre><p>Now we spawn the <code>mgmt</code> stack and use Portainer to deploy and maintain swarms from here on out.</p><pre><code class="language-bash">docker stack deploy -c mgmt/docker-compose.yaml mgmt</code></pre><p>If your server is running Ubuntu &gt;= 18.04, chances are, systemd-resolved is probably occupying port 53. <a href="#q-how-do-i-deal-with-the-port-53-conflict-error-with-traefik-container">We&apos;ll need to turn that off.</a></p><!--kg-card-begin: html--><aside class="note">Hostnames are tagged under the &apos;label:&apos; properties in the docker-compose files.</aside><!--kg-card-end: html--><p>Update your <code>hosts</code> file with the hostnames for the running web services. Windows: <code>C:\Windows\System32\drivers\etc\hosts</code>, Linux/Mac: <code>/etc/hosts/</code>. For my setup, my home network has the domain of <code>.home</code>. You will need to change this for your setup. <code>192.x.x.x</code> denotes the server this docker stack is currently running on.</p><figure class="kg-card kg-code-card"><pre><code>192.x.x.x	portainer.home
192.x.x.x	traefik.home
192.x.x.x	whoami.home
192.x.x.x	pihole.home</code></pre><figcaption>Add these static hostnames to your router once verified that everything is working</figcaption></figure><p>Traekfik dashboard is available via <code>http://traefik.home/local/dashboard/</code> don&apos;t forget the <code>/</code> at the end. You should see entry points for port 80 as well as 53 tcp and udp.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-3.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1402" height="1058" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-3.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-3.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-3.png 1402w" sizes="(min-width: 720px) 720px"><figcaption>Traefik dashboard</figcaption></figure><p>Go to Portainer via <code>http://portainer.home</code> and create your admin account. Once logged in, we will need to create a Docker Secret that will hold the web password for Pi-Hole web ui. The secret needs to be named <em><code>pihole_webpw</code>.</em></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/portainer_secrets.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="988" height="743" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/portainer_secrets.png 600w, https://blog.foureight84.com/content/images/2021/07/portainer_secrets.png 988w" sizes="(min-width: 720px) 720px"><figcaption>Create a Docker Secret named pihole_webpw with your Pi-hole web password.</figcaption></figure><p>Make note of the <code>docker_gwbridge</code> network&apos;s gateway IPV4. This is used to forward DNS requests to the upstream Cloudflared DoH docker container. We can&apos;t use the IP the container gets assigned on the traefik network since that is not static.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/portainer_network_list.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1203" height="747" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/portainer_network_list.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/portainer_network_list.png 1000w, https://blog.foureight84.com/content/images/2021/07/portainer_network_list.png 1203w" sizes="(min-width: 720px) 720px"><figcaption>Portainer network list</figcaption></figure><p>Open the <code>docker-compose.yaml</code> in the <code>dns</code> folder with a text editor and update the <code>DNS1</code> field with your <code>docker_gwbridge</code>&apos;s IP address.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">environment:
      - TZ=America/Los_Angeles
      - DNS1=172.18.0.1#5054 #replace with docker_gwbridge&apos;s gateway ip
      - DNS2=no
      - REV_SERVER=true
      - REV_SERVER_CIDR=192.168.1.0/24 #Update these fields to match your environment
      - REV_SERVER_TARGET=192.168.1.1
      - REV_SERVER_DOMAIN=home
      - WEBPASSWORD_FILE=/run/secrets/pihole_webpw</code></pre><figcaption>dns/docker-compose.yaml pihole service environment variables</figcaption></figure><p>The <code>REV_*</code> environment variables should also be updated to match your setup as well. <code>REV_SERVER</code> and related fields allow Pi-Hole to perform a reverse DNS lookup against the router. I&apos;m currently using my router&apos;s DHCP server (<code>REV_SERVER_TARGET</code>). Static ip and hostnames are kept on the router. Requests for my local NAS for example, will go through Pi-Hole and that will get forwarded to my router.</p><p>After making changes to the dns/docker-compose.yaml file, we will need to upload this to Portainer as a docker-swarm template and deploy the swarm.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/portainer_custom_template.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1209" height="1050" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/portainer_custom_template.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/portainer_custom_template.png 1000w, https://blog.foureight84.com/content/images/2021/07/portainer_custom_template.png 1209w" sizes="(min-width: 720px) 720px"><figcaption>Uploading dns/docker-compose.yaml as a Swarm template</figcaption></figure><figure class="kg-card kg-image-card"><img src="https://blog.foureight84.com/content/images/2021/07/portainer_custom_template_uploaded.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1215" height="531" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/portainer_custom_template_uploaded.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/portainer_custom_template_uploaded.png 1000w, https://blog.foureight84.com/content/images/2021/07/portainer_custom_template_uploaded.png 1215w" sizes="(min-width: 720px) 720px"></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/portianer_deploy_pihole.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1210" height="894" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/portianer_deploy_pihole.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/portianer_deploy_pihole.png 1000w, https://blog.foureight84.com/content/images/2021/07/portianer_deploy_pihole.png 1210w" sizes="(min-width: 720px) 720px"><figcaption>Deploying dns stack</figcaption></figure><p>It should take a few minutes for Pi-Hole deployment to complete. Update the DNS on your computer with your server&apos;s IP and test before applying it as the DNS to use on your router. To check if DoH is working properly, head to <a href="https://1.1.1.1/help">https://1.1.1.1/help</a></p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-2.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="529" height="828"><figcaption><a href="https://1.1.1.1/help">https://1.1.1.1/help</a></figcaption></figure><h3 id="update-services">Update Services</h3><p>Since we are running in Docker Swarm mode, we can take advantage of rolling updates to minimize downtime. Of course, you need more than one instance of a service running in order to perform a rolling update. </p><p>We first need to change the <code>--update-delay</code> flag on the following services (or any that you want to apply a rolling update):</p><ul><li>Traefik</li><li>Pi-hole</li><li>Cloudflared</li></ul><p>We will use <code>120s</code> (2 minutes) as the delay from one service instance to the next. Pi-Hole and Cloudflare services can take up to 30 seconds to reach ready-state and process new requests. 120 seconds should give us ample time for a new Docker image to download and update the first instance while the other instances remain unchanged and continue to process new requests.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-10.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1252" height="822" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-10.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-10.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-10.png 1252w" sizes="(min-width: 720px) 720px"><figcaption>Service list view. Click on a service to view details and update behavior</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-11.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1255" height="820" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-11.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-11.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-11.png 1255w" sizes="(min-width: 720px) 720px"><figcaption>Service details / Update Configuration section. Change &apos;Update Delay&apos; to 2m or 120s</figcaption></figure><p>In the Service details view, click on <code>Update configuration</code> on the right-hand menu to scroll to the section shown in the image above. <code>Update Delay</code> should be entered as <code>2m</code> or <code>120s</code>.</p><p>To update a service, scroll to the top and click on <code>Update the service</code> and toggle <code>Pull latest image version</code> on the confirmation screen. Now Docker will update each running instance one-by-one 120 seconds apart.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-12.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1250" height="821" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-12.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-12.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-12.png 1250w" sizes="(min-width: 720px) 720px"><figcaption>Updating service</figcaption></figure><p>If you&apos;ve noticed from the Service list view screenshot, I am only running 1 instance for all my services since it&apos;s a home network and I don&apos;t need the redundancy or load balancing at the moment (although I might scale Pi-hole and Cloudflared to 2 instances each as these containers have halted in the past). These services will be scaled to 2 or 3 before performing a service update in order to utilize the rolling update. Once all instances are updated, I will downscale them back to the initial scale.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-13.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1251" height="822" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-13.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-13.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-13.png 1251w" sizes="(min-width: 720px) 720px"><figcaption>Scaling services on the fly before performing a service update in order to take advantage of the rolling update feature of Docker Swarm.</figcaption></figure><p></p><p>Happy adblocking!</p><h2 id="faq">FAQ</h2><h3 id="q-where-can-get-more-blocklists">Q: Where can get more blocklists?</h3><p>Filterlists is a good place to start (<a href="https://filterlists.com/">https://filterlists.com/</a>)</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><h3 id="q-what-the-hell-are-you-on-about">Q: What the hell are you on about?</h3><p>For anyone starting from scratch, the initial steps not mentioned in this guide are:</p><ol><li>Installing Ubuntu server (<a href="https://ubuntu.com/download/server">https://ubuntu.com/download/server</a>), for Raspberry Pi (<a href="https://ubuntu.com/download/raspberry-pi">https://ubuntu.com/download/raspberry-pi</a>)</li><li>Enable SSH after booting into Ubuntu: <code>sudo apt install openssh-server</code>. This will allow remote terminal login to your server via ssh. e.g. <code>ssh your-username@your-server-ip-address</code> from another computer with an SSH client. Windows 10 has SSH built-in or you can use <a href="https://www.putty.org/">PuTTY</a>.</li><li>Installing Docker-CE: <a href="https://docs.docker.com/engine/install/ubuntu/">https://docs.docker.com/engine/install/ubuntu/</a></li></ol><p>You do not need to install Ubuntu for the Rasberry Pi, there&apos;s also Raspian OS. Just make sure to do steps 2 and 3.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><h3 id="q-how-do-i-deal-with-the-port-53-conflict-error-with-the-traefik-container">Q: How do I deal with the port 53 conflict error with the Traefik container?</h3><p>There are two approaches to this. I will guide based on Ubuntu. You can use this as a guide for your system.<br>First, we need to find out what&apos;s using port 53 and determine if we can stop, or disable it. If you can&apos;t then read further for the alternative.</p><pre><code class="language-bash">sudo netstat -pna | grep 53</code></pre><p>If you&apos;re on Ubuntu 18.04 (I believe) and newer, it will most likely be caused by systemd-resolved.</p><p>If it is then:</p><!--kg-card-begin: markdown--><ol>
<li><code>sudo systemctl stop systemd-resolved</code> to stop the service</li>
<li><code>sudo nano /etc/systemd/resolved.conf</code></li>
<li>Uncomment <code>#DNS=</code> and replace it with our DNS of choice. It shouldn&apos;t matter as this is for your host. I chose cloudflare. It should then look like this <code>DNS=1.1.1.1</code></li>
<li>Uncomment <code>#DNSStubListener=no</code> or add the line if it&apos;s not there.</li>
<li>Save and exit the text editor.</li>
<li><code>sudo ln -sf /run/systemd/resolve/resolv.conf /etc/resolv.conf</code> creates a symbolic link and replaces the current link in the existing path.</li>
<li>Check that DNS still works by doing a <code>ping github.com</code></li>
<li>If you see an error about <code>sudo: unable to resolve host ubuntu: Name or service not known</code> then edit your <code>hosts</code> file and add <code>127.0.0.1 your-machine-name</code> below <code>127.0.0.1 localhost</code>.</li>
</ol>
<!--kg-card-end: markdown--><p>If you cannot stop the service that&apos;s currently using port 53 then you can update the mgmt stack docker-compose.yaml to use docker&apos;s loopback.</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">traefik:
  image: traefik:latest
  ports:
    - target: 53
      published: 53
      protocol: tcp
      mode: host
    - target: 53
      published: 53
      protocol: udp
      mode: host
    - target: 80
      published: 80
      protocol: tcp
      mode: host</code></pre><figcaption>mgmt/docker-compose.yaml ports expressed in long format</figcaption></figure><p>Unfortunately, IP address format is not supported in long-form port declaration. It will need to change to:</p><figure class="kg-card kg-code-card"><pre><code class="language-yaml">traefik:
  image: traefik:latest
  ports:
    - 0.0.0.0:53:53/udp
    - 0.0.0.0:53:53/tcp
    - target: 80
      published: 80
      protocol: tcp
      mode: host</code></pre><figcaption>mgmt/docker-compose.yaml updated ports to listen on docker&apos;s loopback</figcaption></figure><p>A caveat of using this method is that Traefik won&apos;t be able to forward the real IP of the requestor in the request header for <code>X-Forwarded-For</code> and <code>X-Real-Ip</code>. That IP will be a docker NAT IP (test this out using <code>http://whoami.test.local</code>). It&apos;s not important for our purpose, but if you were to deploy another web service that utilizes this then it could be an issue.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><h3 id="q-pi-hole-client-id-in-the-query-log-only-shows-traefiks-hostname">Q: Pi-Hole client id in the query log only shows Traefik&apos;s hostname</h3><p>When a request comes in through a reverse proxy via HTTP, the request&apos;s originating IP is passed along in the request headers as <code>X-Forwarded-For</code> and <code>X-Real-Ip</code>. However, with DNS, there&apos;s no such flag. There&apos;s been a proposal for DNS <code>X-Proxied-For</code> but that&apos;s not a standard. <a href="https://tools.ietf.org/id/draft-bellis-dnsop-xpf-03.html">https://tools.ietf.org/id/draft-bellis-dnsop-xpf-03.html</a><br>Pi-hole only makes notes of which immediate client sent the request, and in this case, it&apos;s Traefik. If you really want to make this work then you can implement a <a href="https://docs.docker.com/network/macvlan/">macvlan</a> network and attach your Pi-Hole instance(s). This would allow a Docker container to receive an IP on your router&apos;s subnet and allow direct communication.<br>If pi-hole was not in swarm mode then this would not be an issue (this was my previous setup). In swarm mode and behind a load balancer, you would need to allocate a block of IPs to dedicate to the swarm (IP assignment is not supported in swarm mode) while the load balancer would only apply to port 80 traffic to pi-hole&apos;s web UI.</p><p></p><h3 id="q-how-do-i-test-my-adblocking-capability">Q: How do I test my adblocking capability?</h3><p><a href="https://d3ward.github.io/toolz/src/adblock.html"><a href="https://d3ward.github.io/toolz/src/adblock.html">https://d3ward.github.io/toolz/src/adblock.html</a></a> is a good place to start. It should show you whether you are blocking major players on the DNS level.</p><figure class="kg-card kg-image-card"><img src="https://blog.foureight84.com/content/images/2021/07/image-4.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1387" height="1020" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-4.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-4.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-4.png 1387w" sizes="(min-width: 720px) 720px"></figure><p>You could also turn off your browser ad block extension and go to a known site with embedded ads. <a href="https://tweaktown.com">https://tweaktown.com</a> is a good place to test.</p><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-5.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1386" height="1055" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-5.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-5.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-5.png 1386w" sizes="(min-width: 720px) 720px"><figcaption>Without Pi-hole and browser adblocker extension</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-6.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1389" height="1057" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-6.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-6.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-6.png 1389w" sizes="(min-width: 720px) 720px"><figcaption>After Pi-hole is enabled with proper blocklists added</figcaption></figure><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/image-7.png" class="kg-image" alt="Swarm Your Pi-hole" loading="lazy" width="1390" height="891" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/image-7.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/07/image-7.png 1000w, https://blog.foureight84.com/content/images/2021/07/image-7.png 1390w" sizes="(min-width: 720px) 720px"><figcaption>Pi-hole DNS blocking with browser adblocking extension</figcaption></figure><p>Without a browser Adblock extension, it doesn&apos;t look pretty but it saves on your bandwidth usage since ads do not load at all. Use this in combination with a browser-based adblocker to remove those HTML elements. Without Pi-Hole ads will be hidden with browser adblockers but they would still load.</p>]]></content:encoded></item><item><title><![CDATA[Terraforming Ghost on Linode with Google Drive Backup Using Rclone]]></title><description><![CDATA[Using Terraform to setup Ghost on Linode with auto daily backup to Google Drive using Rclone.]]></description><link>https://blog.foureight84.com/deploying-ghost-on-linode-with-cheap-remote-backup-using-terraform/</link><guid isPermaLink="false">60d92ea4883663000189ce70</guid><category><![CDATA[ghost]]></category><category><![CDATA[blog]]></category><category><![CDATA[terraform]]></category><category><![CDATA[hcl]]></category><category><![CDATA[docker]]></category><category><![CDATA[cloudflare]]></category><category><![CDATA[linode]]></category><category><![CDATA[automation]]></category><category><![CDATA[rclone]]></category><category><![CDATA[backup]]></category><dc:creator><![CDATA[foureight84]]></dc:creator><pubDate>Thu, 01 Jul 2021 08:20:58 GMT</pubDate><media:content url="https://blog.foureight84.com/content/images/2021/07/ghost_network_diagram.png" medium="image"/><content:encoded><![CDATA[<!--kg-card-begin: markdown--><h3 id="table-of-contents">Table of contents</h3>
<ul>
<li><a href="#terraform">Terraform</a>
<ul>
<li><a href="#linode">Linode</a></li>
<li><a href="#cloudflare">Cloudflare</a></li>
<li><a href="#variables-and-definitions">Variables &amp; Definitions</a></li>
</ul>
</li>
<li><a href="#docker">Docker</a>
<ul>
<li><a href="#ghost-stack">Ghost Stack</a></li>
<li><a href="#rclone">Rclone</a></li>
</ul>
</li>
<li><a href="#deployment">Deployment</a></li>
</ul>
<!--kg-card-end: markdown--><hr><img src="https://blog.foureight84.com/content/images/2021/07/ghost_network_diagram.png" alt="Terraforming Ghost on Linode with Google Drive Backup Using Rclone"><p>At the beginning of the pandemic, I decided to build a custom keyboard (post on this later). The project led to me wanting to add a Blackberry trackball and this would require a special way to mount it to the keyboard PCB. After weeks of trying to jury-rigged a mount from common parts to attach the trackball to the keyboard, I realized that it would be much easier to design and 3D print the necessary part. </p><p>I also needed a better way to document my projects and starting a blog is better for visibility and accessibility than a markdown README file on Github. Ghost is a good candidate, offering a lightweight straightforward writing platform (with markdown support). There&apos;s also a robust list of third-party integrations which would be good down the road if I needed to scale.</p><p>I also wanted to be able to easily backup my data in case I needed to move to a different host. At first, I was eyeing an AWS S3 bucket or perhaps Linode&apos;s offering called &quot;Object Storage.&quot; At the moment, I don&apos;t need that much backup space. A free option would be best. This is where Rclone is handy as it would allow me to tarball essential ghost files and have a daily backup. I could also deploy Rclone on another server, such as a local NAS, as an added backup destination. </p><p>I chose Terraform in order to automate the setup tasks so that I can easily switch hosts in the future. The one plus side with Terraform is that it uses declarative language to define tasks and this is, to me, a lot easier to read and understand in the future. If you haven&apos;t looked at your code in over six months, it may as well been written by someone else.</p><!--kg-card-begin: markdown--><h3 id="project-overview">Project Overview</h3>
<ul>
<li>Terraform deployment</li>
<li>Cloudflare proxied DNS</li>
<li>Docker
<ul>
<li>Nginx reverse proxy</li>
<li>Let&apos;s Encrypt for automatic SSL</li>
<li>Ghost 4</li>
<li>Rclone backup blog data to Google Drive</li>
</ul>
</li>
</ul>
<!--kg-card-end: markdown--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/06/ghost_network_diagram.png" class="kg-image" alt="Terraforming Ghost on Linode with Google Drive Backup Using Rclone" loading="lazy" width="1661" height="1252" srcset="https://blog.foureight84.com/content/images/size/w600/2021/06/ghost_network_diagram.png 600w, https://blog.foureight84.com/content/images/size/w1000/2021/06/ghost_network_diagram.png 1000w, https://blog.foureight84.com/content/images/size/w1600/2021/06/ghost_network_diagram.png 1600w, https://blog.foureight84.com/content/images/2021/06/ghost_network_diagram.png 1661w" sizes="(min-width: 720px) 720px"><figcaption>Network Diagram</figcaption></figure><p>This project can be found on my <a href="https://github.com/foureight84/ghost-linode-terraform">github</a>. Clone and follow along:</p><pre><code class="language-bash">git clone https://github.com/foureight84/ghost-linode-terraform.git &amp;&amp; cd ghost-linode-terraform</code></pre><hr><h2 id="terraform">Terraform</h2><p>A declarative configuration-based infrastructure as code API wrapper. There are a few pros and cons to consider. Keep in mind that this is my first time using Terraform these are surface-level points observed.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><p><strong>Pros:</strong></p><ul><li>Quick and straightforward (standardized) configuration syntax declaring what tasks to perform.</li><li>Easy to read and double-check my work. Terraform&apos;s &apos;plan&apos; feature to check for syntax errors and preview the final result.</li><li>Configurations can be changed and applied quickly without having to go through service consoles.</li><li>All pieces in my stack can be managed through Terraform (see cons).</li></ul><p><strong>Cons:</strong></p><ul><li>Providers often offer web APIs to perform the same tasks, and at best, there will be feature parity. But there is a chance that feature-parity on Terraform is a second priority to a provider and support lags behind.</li><li>Not all services may have Terraform support, which could lead to the additional complexity of having to manage multiple tools. This is often the case in a real-world scenario.</li></ul><!--kg-card-begin: html--><aside class="note">Linode does allow for changing a node&apos;s root password after it has been shut down. This is done through their web console.</aside><!--kg-card-end: html--><ul><li>Applying my Terraform configuration update doesn&apos;t always behave as expected. The functionality limitation depends greatly on the provider. For example, after a Linode is created, applying changes to certain properties such as root password will result in Terraform destroying the instance to create a new node using updated configurations.</li></ul><p>Before diving into specific files in the project, here is a brief overview and explanation of the project structure:</p><pre><code class="language-Treeview">[..root]
&#x2502;  cloudflare.tf                   //resource module for CloudFlare rules
&#x2502;  data.tf                         //templates ex. docker-compose.yaml, bash scripts, etc...
&#x2502;  linode.tf                       //resource module for Linode instance setup
&#x2502;  outputs.tf                      //handle specific data to display after terraforming
&#x2502;  providers.tf                    //tokens, auth keys, etc required by service providers
&#x2502;  terraform.tfvars.example        //example answers for input prompts 
&#x2502;  variables.tf                    //defined inputs required for terraforming
&#x2502;  versions.tf                     //declaration of providers and versions to use
&#x2502;
&#x2514;&#x2500;[scripts]
  &#x251C;&#x2500;[linode]
  &#x2502;       docker-compose.yaml      //main stack. nginx proxy, letsencrypt, ghost
  &#x2502;       stackscript.sh           //boot time script specific to Linode to setup env
  &#x2502;
  &#x2514;&#x2500;[rclone]
    &#x2502;     backup.sh                //cron script for backing up ghost blog directory
    &#x2502;     docker-compose.yaml      //rclone docker application
    &#x2502;
    &#x2514;&#x2500;&#x2500;[config]
            rclone.conf.example    //rclone configuration for cloud storage</code></pre><hr><h3 id="linode">Linode</h3><p><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/linode.tf">linodes.tf</a>, <a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/scripts/linode/stackscript.sh">stackscript.sh</a>, <a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/data.tf">data.tf</a></p><p>This is a straightforward configuration to create a Linode with the use of Linode&apos;s Stackscript. Which is essentially a run-once bash script that gets executed on the first boot. </p><p>The stackscript.sh lives in the <code>ghost-linode-terraform/scripts/linode/</code> directory and is parsed by Terraform as a data template. Terraform&apos;s templating syntax needs to be taken into consideration when parsing text files. Most notable are variables and their escape characters. Any $string or ${string} notation will be regarded as a template variable, while $$string and $${string} are escaped bash variables. <a href="https://www.terraform.io/docs/language/expressions/strings.html">More about strings and templates</a>.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><figure class="kg-card kg-code-card"><pre><code class="language-HCL">script = &quot;${data.template_file.stackscript.rendered}&quot;</code></pre><figcaption>linode.tf, example of data template being referenced</figcaption></figure><p>Alongside the Stackscript, the &quot;linode_instance&quot; resource block also includes <code>stackscript_data</code> property. This is a way of providing data to the one-time boot script. The key-value assignments within this block correspond with the &apos;User-defined fields&apos; at the top of stackscript.sh.</p><figure class="kg-card kg-code-card"><pre><code class="language-bash">#!/bin/sh
# &lt;UDF name=&quot;DOCKER_COMPOSE&quot; label=&quot;Docker compose file&quot; default=&quot;&quot; /&gt;
# &lt;UDF name=&quot;ENABLE_RCLONE&quot; label=&quot;(Bool) Flag to turn on RClone Support&quot; default=&quot;false&quot; /&gt;
# &lt;UDF name=&quot;RCLONE_DOCKER_COMPOSE&quot; label=&quot;RClone docker compose file&quot; default=&quot;&quot; /&gt;
# &lt;UDF name=&quot;RCLONE_CONFIG&quot; label=&quot;RClone configuration file&quot; default=&quot;&quot; /&gt;
# &lt;UDF name=&quot;BACKUP_SCRIPT&quot; label=&quot;Backup script&quot; default=&quot;&quot; /&gt;</code></pre><figcaption>stackscript.sh, user-defined fields declaration</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-HCL">stackscript_data = {
    &quot;DOCKER_COMPOSE&quot; = &quot;${data.template_file.docker_compose.rendered}&quot;
    &quot;ENABLE_RCLONE&quot; = var.enable_rclone
    &quot;RCLONE_DOCKER_COMPOSE&quot; = &quot;${data.template_file.rclone_docker_compose.rendered}&quot;
    &quot;RCLONE_CONFIG&quot; = &quot;${data.template_file.rclone_config.rendered}&quot;
    &quot;BACKUP_SCRIPT&quot; = &quot;${data.template_file.backup_script.rendered}&quot;
  }</code></pre><figcaption>linode.tf, assigning data template string to UDF</figcaption></figure><figure class="kg-card kg-code-card"><pre><code class="language-HCL">data &quot;template_file&quot; &quot;docker_compose&quot; {
  template = &quot;${file(&quot;${path.module}/scripts/linode/docker-compose.yaml&quot;)}&quot;

  vars = {
    &quot;ghost_blog_url&quot; = &quot;${var.ghost_blog_url}&quot;
    &quot;letsencrypt_email&quot; = &quot;${var.letsencrypt_email}&quot;
  }
}</code></pre><figcaption>data.tf, example of parsing a file into a data template</figcaption></figure><p>In the snippet above, docker-compose.yaml for the Ghost stack is parsed as a data template and <code>ghost_blog_url</code> and <code>letsencrypt_email</code> variables get evaluated and then passed as a string to the Stackscript at runtime.</p><p>At the time of deployment, the Stackscript will be created before the Linode instance. Terraform allows direct referencing named values. This can be seen in the &quot;linode_instance&quot; block where <code>linode_stackscript.ghost_deploy.id</code> is assigned as the stackscript ID to include when creating the Linode instance. Finally, other parsed data templates such the docker-compose.yaml are assigned to stackscript user-defined fields and sent as <code>stackscript_data</code>.</p><h3 id="cloudflare">Cloudflare</h3><p><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/cloudflare.tf">cloudflare.tf</a></p><p>The Cloudflare configuration is relatively straightforward. Since the foureight84.com domain is already managed by Cloudflare, a lookup is performed and the named value is referenced with each resource call as <code>zone_id</code> property.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><figure class="kg-card kg-code-card"><pre><code class="language-HCL">data &quot;cloudflare_zones&quot; &quot;ghost_domain_zones&quot; {
  filter {
    name   = var.cloudflare_domain
    status = &quot;active&quot;
  }
}</code></pre><figcaption>cloudflare.tf managed domain (foureight84.com)</figcaption></figure><p>An &apos;A&apos; record is created for the blog (blog.foureight84.com) and end-to-end HTTPS encryption is enforced (<code>ssl=&quot;strict&quot;</code>) along with requiring all HTTPS origin pull requests only come from Cloudflare. Nginx-proxy-companion will generate a validation file under the path blog.foureight84.com/.well-known/&lt;some random string&gt; for SSL certification requests. Let&apos;s Encrypt validation of this generated file accepts both HTTP and HTTPS (ports 80 and 443), a page rule is created to ensure that the request does not get blocked.</p><h3 id="variables-and-definitions">Variables and Definitions</h3><p><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/variables.tf">variables.tf</a></p><p>As seen in <code>linode.tf</code>, <code>cloudflare.tf</code>, and especially <code>data.tf</code>, <code>${var.&lt;some string&gt;}</code> are used throughout. These are references to declared variables in <code>variables.tf</code> such as API tokens for our services, domain names, and etc. These variables show up as input prompts when performing Terraform plan, apply, or destroy actions. Variables contain properties such as data type, descriptions, default values, and custom validation rules. <a href="https://www.terraform.io/docs/language/values/variables.html">More about input variables</a>.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><p><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/terraform.tfvars.example">terraform.tfvars.example</a></p><p>This Terraform deployment requires 17 data inputs in order to perform its tasks and that&apos;s 17 possible chances to introduce error. Luckily, Terraform supports dictionary referencing in the form of <code>.tfvars</code> files. Tfvars are key-value files where the key is the variable name. A <code>.tfvars</code> file is referenced during run-time in order to provide required inputs. For example:</p><pre><code class="language-bash">terraform plan -var-file=&quot;defined.tfvars&quot;</code></pre><hr><h2 id="docker">Docker</h2><p>This project has two separate docker-compose environments. The first is our blog stack, and the second is Rclone to perform data backup. I decided to separate the Rclone docker service from the primary Ghost stack for two reasons:</p><ol><li>I want to be able to mount the cloud storage prior to running the Ghost stack so that data restore can be performed should a backup exists (see <code><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/scripts/linode/stackscript.sh#L51">stackscript.sh</a></code>). </li><li>Cloud drive mount should always stay active. Changes to my Ghost stack should not impact Rclone&apos;s availability.</li></ol><h3 id="ghost-stack">Ghost Stack</h3><p><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/scripts/linode/docker-compose.yaml">docker-compose.yaml</a></p><p>This runs 3 containers using the following images:</p><ul><li>jwilder/nginx-proxy</li><li>jrcs/letsencrypt-nginx-proxy-companion</li><li>ghost:4-alpine</li></ul><p>The default directory is <code>/root/ghost</code>. This can be changed via Terraform during deployment from the tfvars file. Since this is a set default value, Terraform does not prompt for the input value.</p><h4 id="jwildernginx-proxy"><em>jwilder/nginx-proxy</em></h4><p>This is the &quot;front&quot; facing container on the origin server (Linode) sitting behind Cloudflare. Visitors&apos; incoming requests for <em>https://blog.foureight84.com</em> will go through Cloudflare, which is forwarded to our reverse-proxy, where the request is relayed to the running Docker service matching the <code>VIRTUAL_HOST</code> value. Finally, nginx-proxy will collect the served content from the Ghost service and send it back to the visitor.</p><p>The reverse proxy will be listening to exposed ports 80 and 443.</p><h4 id="jrcsletsencrypt-nginx-proxy-companion"><em>jrcs/letsencrypt-nginx-proxy-companion</em></h4><p>Nginx-proxy-companion handles automatic SSL registration for the running Docker services. The important environment variables to keep in mind are:</p><pre><code class="language-YAML">environment:
  - LETSENCRYPT_HOST=${ghost_blog_url}
  - LETSENCRYPT_EMAIL=${letsencrypt_email}</code></pre><!--kg-card-begin: html--><aside class="note">${ghost_blog_url} is an example of a Terraform templating variable. These variables will be replaced with proper values during deployment.</aside><!--kg-card-end: html--><p>The above environment variables are added to Docker services that require SSL certificates (Ghost container in this example). In the future, if I need to add additional subdomains, such as <em>www.foureight84.com</em>, then that service will require <code>LETSENCRYPT_HOST=www.foureight84.com</code>. I can avoid having to repeatedly define <code>LETSENCRYPT_EMAIL</code> by setting the <code>DEFAULT_EMAIL</code> environment variable in the nginx-proxy-companion instead.</p><p>By default, a production certificate will be requested. Let&apos;s Encrypt has a limit of 10 cert requests every 7 days for normal users. All requested certs are stored in the path <code>/etc/acme.sh</code> which is mounted to the <code>acme</code> Docker volume.</p><!--kg-card-begin: html--><aside class="note">The current setup creates a full end-to-end encryption path. SSL requests from visitors to Cloudflare as well as communication between origin and Cloudflare.</aside><!--kg-card-end: html--><p>The nginx-proxy and nginx-proxy-companion share <code>certs</code>, <code>vhost.d</code>, and <code>nginx.html</code> volumes. If I need to move to a different host, then the files store in <code>certs</code> and <code>acme</code> docker volumes will need to be backed up. The former is where the generated private key used for SSL certificate requests is stored, whereas the latter is contains the generated certificates and checked against upon service startup. Without the original private key then the SSL certificate cannot be renewed and without SSL certificate would force a new request. </p><!--kg-card-begin: html--><aside class="note">Let&apos;s Encrypt certificates have a 90-day lifespan, which has pros and cons. Read more about the discussion on their <a href="https://community.letsencrypt.org/t/pros-and-cons-of-90-day-certificate-lifetimes/4621">forum thread</a> relating to the matter. This should be evaluated carefully for production usage.</aside><!--kg-card-end: html--><p>Avoid performing a <code>docker volume prune</code> or <code>docker system prune</code> without proper backup. They are technically not essential unless the weekly certificate request limit has been reached. To get the path to these volumes, run the terminal command <code>docker volume inspect &lt;volume name&gt;</code>.</p><h4 id="ghost4-alpine"><em>ghost:4-alpine</em></h4><p>This is an all-in-one image. I believe prior versions used MySQL. With Ghost v4, SQLite is now the recommended DB. This works out better for my requirement as it is easier to backup.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><pre><code class="language-YAML">ports:
  - 127.0.0.1:8080:2368</code></pre><p>I am just remapping the default port 2358 to 8080. This will be internal and not required as the Ghost service will be not directly exposed to public traffic. As mentioned earlier this will be handled by the reverse proxy where incoming requests will be directed to the matching <code>VIRTUAL_HOST</code>.</p><h3 id="rclone">Rclone</h3><p><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/scripts/rclone/docker-compose.yaml">docker-compose.yaml</a></p><p>The default directory is <code>/root/rclone</code> but can be changed using Terraform tfvars at runtime.</p><p>Rclone is a command-line application that enables cloud storage to be mounted on the host filesystem. I believe it supports over 30 well-known services such as Google Drive, Dropbox, AWS S3, Amazon Drive, etc. I decided to stick with Google Drive since I have 100GB of underutilized storage. I plan on incorporating Rclone in a <a href="https://www.truenas.com/truenas-scale/">TrueNAS SCALE</a> self-built NAS as a redundant backup in the future.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><p>Here are the steps to setting up Google Drive with Rclone:</p><!--kg-card-begin: markdown--><ul>
<li>Start with this: <a href="https://rclone.org/drive/#making-your-own-client-id">https://rclone.org/drive/#making-your-own-client-id</a>
<ul>
<li>Don&apos;t forget to submit app for verification. It will be an automatic approval. The generated auth token will not renew if the app is in development mode.</li>
</ul>
</li>
<li>Then follow this guide to attach the Google Drive account to Rclone: <a href="https://rclone.org/drive/">https://rclone.org/drive/</a></li>
</ul>
<!--kg-card-end: markdown--><!--kg-card-begin: html--><br><!--kg-card-end: html--><figure class="kg-card kg-code-card"><pre><code class="language-YAML">volumes:
  - ${rclone_dir}/config:/config/rclone
  - ${rclone_dir}/mount:/data:shared
  - /etc/passwd:/etc/passwd:ro
  - /etc/group:/etc/group:ro</code></pre><figcaption>Rclone docker-compose.yaml</figcaption></figure><!--kg-card-begin: html--><aside class="note">One caveat to using Rclone is that backups will count against a VPS&apos;s monthly traffic quota. Whereas, using block storage from the same host usually does not.</aside><!--kg-card-end: html--><p><code>/etc/passwd</code> and <code>/etc/group</code> mounts are required for <a href="https://en.wikipedia.org/wiki/Filesystem_in_Userspace">FUSE</a> to work properly inside the container. Additionally, a premade configuration from another Rclone instance is needed. This can be done by completing a Rclone setup wizard for Google Drive. Make sure to copy the configuration to the project&apos;s folder: <code>ghost-linode-terraform/scripts/rclone/config/</code></p><p>In my setup, the Google Drive configuration is called &apos;gdrive.&apos; This needs to reflect in the docker-compose&apos;s command block:</p><pre><code class="language-YAML">command: &quot;mount gdrive: /data&quot;</code></pre><p>The default mount path is <code>/root/rclone/mount</code>.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><p><a href="https://github.com/foureight84/ghost-linode-terraform/blob/a7fe63d1945162968eeb88a85a4452e3a3b640fc/scripts/rclone/backup.sh">backup.sh</a></p><p>This script is responsible for creating tarballs from the ghost blog directory on the host machine. By default, the script maintains a rolling 7-day backup with <code>latest.tgz</code> being the last run archive.</p><p>A crontab is added through Linode&apos;s Stackscript and is set to run daily at 11PM (system time). Keep in mind that UTC is the default system time zone.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><figure class="kg-card kg-image-card kg-card-hascaption"><img src="https://blog.foureight84.com/content/images/2021/07/Rclone_backup_gdrive_view.JPG" class="kg-image" alt="Terraforming Ghost on Linode with Google Drive Backup Using Rclone" loading="lazy" width="647" height="445" srcset="https://blog.foureight84.com/content/images/size/w600/2021/07/Rclone_backup_gdrive_view.JPG 600w, https://blog.foureight84.com/content/images/2021/07/Rclone_backup_gdrive_view.JPG 647w"><figcaption>Google Drive view of blog backups</figcaption></figure><hr><h2 id="deployment">Deployment</h2><p>If you have not done so, install Terraform CLI on your localhost. <a href="https://learn.hashicorp.com/tutorials/terraform/install-cli">Follow this installation guide</a>.</p><!--kg-card-begin: html--><br><!--kg-card-end: html--><p><strong>Clone project</strong></p><pre><code class="language-bash">git clone https://github.com/foureight84/ghost-linode-terraform.git &amp;&amp; cd ghost-linode-terraform</code></pre><p><strong>Initialize Terraform workspace</strong></p><figure class="kg-card kg-code-card"><pre><code class="language-bash">terraform init</code></pre><figcaption>This will download required provider modules</figcaption></figure><p><strong>Create your tfvars definition file</strong></p><pre><code class="language-bash">cp terraform.tfvars.example defined.tfvars</code></pre><p><strong>Open <code>defined.tfvars</code> and fill in all required values</strong></p><pre><code class="language-HCL">
//project_dir = &quot;&quot;                                // default /root/ghost
//rclone_dir = &quot;&quot;                                 // default /root/rclone

linode_api_token = &quot;&quot;
linode_label = &quot;&quot;
linode_image = &quot;linode/ubuntu20.04&quot;               // see terraform linode provider documentation for these values
linode_region = &quot;us-west&quot;                         // see terraform linode provider documentation for these values
linode_type = &quot;g6-nanode-1&quot;                       // see terraform linode provider documentation for these values
linode_authorized_users = [&quot;&quot;]                    // user profile created on linode with associated ssh pub key. https://cloud.linode.com/profile/keys
linode_group = &quot;blog&quot;
linode_tags = [ &quot;ghost&quot;, &quot;docker&quot; ]

linode_root_password = &quot;&quot;

cloudflare_domain = &quot;&quot;                           // requires that your domain is already managed by cloudflare. value ex: foureight84.com
cloudflare_email = &quot;&quot;
cloudflare_api_key = &quot;&quot;                          // not to be mistaken with cf api token

letsencrypt_email = &quot;&quot;

ghost_blog_url = &quot;&quot;                              // ex. blog.foureight84.com

enable_rclone =                                  // boolean (default false). change to true if using rclone. see README.md in rclone directory on how to setup config beforehand</code></pre><p><strong>Double-check that everything is correct and get a deployment preview</strong></p><pre><code class="language-bash">terraform plan -var-file=&quot;defined.tfvars&quot;</code></pre><p><strong>Apply Terraform changes to production</strong></p><pre><code class="language-bash">terraform apply -var-file=&quot;defined.tfvars&quot;</code></pre><p></p><p></p><p></p><p></p>]]></content:encoded></item></channel></rss>