<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Pierre Carion</title>
    <description>The latest articles on DEV Community by Pierre Carion (@pcarion).</description>
    <link>https://dev.to/pcarion</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F213914%2F9ebff5c2-2b48-46bf-998e-14ec0f445dd2.jpeg</url>
      <title>DEV Community: Pierre Carion</title>
      <link>https://dev.to/pcarion</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/pcarion"/>
    <language>en</language>
    <item>
      <title>HTTPS for your local endpoints for less than $1 a month</title>
      <dc:creator>Pierre Carion</dc:creator>
      <pubDate>Tue, 04 Feb 2020 00:13:13 +0000</pubDate>
      <link>https://dev.to/pcarion/https-for-your-local-endpoints-for-less-than-1-a-month-4gb</link>
      <guid>https://dev.to/pcarion/https-for-your-local-endpoints-for-less-than-1-a-month-4gb</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://pcarion.com/article/ssh-tunnel"&gt;https://pcarion.com/article/ssh-tunnel&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;There are quite a few instances when you want a public URL to hit your development machine:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you want to expose a webserver running on your local machine to the internet so that a colleague or a customer may have a look at it.&lt;/li&gt;
&lt;li&gt;you are using a service, like &lt;a href="https://www.twilio.com/"&gt;twillio&lt;/a&gt;, which allows you to setup webhooks URL: the service will call those URLs to notify you when &lt;em&gt;something&lt;/em&gt; happens.&lt;/li&gt;
&lt;li&gt;you want to test an OAuth integration, with Facebook or Twitter, and you want to provide public https callback URLs.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;An alternative option is to install your code on a public server and have those requests hit this server. During development, it is much more convenient to have those requests hit your local machine so that you can debug and see the logs in real time.&lt;/p&gt;

&lt;p&gt;You can also use a service like &lt;a href="https://ngrok.com/"&gt;ngrok&lt;/a&gt;: this is a tool, developed by &lt;a href="https://twitter.com/inconshreveable"&gt;Alan Shreve&lt;/a&gt;, very easy to use and &lt;a href="https://www.twilio.com/blog/2015/09/6-awesome-reasons-to-use-ngrok-when-testing-webhooks.html"&gt;perfect for webhooks&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;We'll describe here how you can setup your own solution on ec2.&lt;/p&gt;

&lt;p&gt;Such a home made solution has several advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can have persistent URLs - ngrok gives a different domain each time you use it unless you become a paying customer.&lt;/li&gt;
&lt;li&gt;you can automate the process (see the end of this post) meaning that you can quickly tear down your EC2 instance and restart it quickly when you need the URLs again.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;At &lt;code&gt;$0.0069&lt;/code&gt; per hour for a nano instance, the service will cost you $1 per month if you use it for about 144 hours. This is more than enough for a "night and weekend" project.&lt;/p&gt;

&lt;h2&gt;
  
  
  Description of the solution
&lt;/h2&gt;

&lt;p&gt;The solution looks like this:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--fCxkTBhr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/general-a44f005457e576124d5987409e4a80e9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--fCxkTBhr--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/general-a44f005457e576124d5987409e4a80e9.png" alt="reverse proxy setup"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;A machine (C) on the public internet will be able to access multiple URLs, on the same public URL, to access one or more services on your local machine (A).&lt;/p&gt;

&lt;p&gt;For instance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the URL &lt;code&gt;https://api.mydomain.com&lt;/code&gt; will access &lt;code&gt;http://localhost:3000&lt;/code&gt; on your dev machine&lt;/li&gt;
&lt;li&gt;the URL &lt;code&gt;https://api.mydomain.com&lt;/code&gt; will access &lt;code&gt;http://localhost:3000&lt;/code&gt; on your dev machine&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Warning: a custom made solution is definitely more complex than using &lt;code&gt;ngrok&lt;/code&gt;, so you should think twice before taking that route. You need to be comfortable with bash scripting and DNS setup to implement that solution.&lt;/p&gt;

&lt;p&gt;This solution requires:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;a domain name: this domain, and subdomains, will be used to configure public URL(s) to access your local machine&lt;/li&gt;
&lt;li&gt;a public DNS: as you want to have public URL to access your server, a DNS is required.&lt;/li&gt;
&lt;li&gt;a machine on the public internet: this machine will act as a bridge between the public internet and your local machine. You can use Digital ocean or AWS EC2, with full root access&lt;/li&gt;
&lt;li&gt;a &lt;em&gt;sshd&lt;/em&gt; daemon running on that server: SSH is the swiss army knife for that kind of setup, and you need to be able to fully configure the SSH server, especially to setup a reverse tunnel&lt;/li&gt;
&lt;li&gt;a SSL certificate. To protect your connection, you need to setup SSL certificates so that the public URL can be available only through https. We will use &lt;a href="https://letsencrypt.org/"&gt;Let's encrypt&lt;/a&gt; for that.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The Gateway server (B) is the machine on the public internet and you need to configure a set of services on that server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;configure the SSD service to allow TCP port forwarding&lt;/li&gt;
&lt;li&gt;install a reverse proxy - &lt;code&gt;HAProxy&lt;/code&gt; - to forward different subdomain to different ssh tunnels&lt;/li&gt;
&lt;li&gt;configure let's encrypt to allow SSL traffic over https&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;In the rest of this document, we will use EC2 to install this gateway server.&lt;/p&gt;

&lt;h2&gt;
  
  
  EC2 instance configuration
&lt;/h2&gt;

&lt;p&gt;The setup is not intended for a production service and the traffic on that machine should be very low. For that reason, you can use a very small instance to run that server : I am using a &lt;code&gt;t2.nano&lt;/code&gt; instance with an ubuntu OS.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3DA76ntx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-43234f546cc3fbc0bb6ab085f7c12de4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3DA76ntx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-43234f546cc3fbc0bb6ab085f7c12de4.png" alt="ubuntu instance"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  security group
&lt;/h3&gt;

&lt;p&gt;The only setup to pay attention to is the network/security group definition.&lt;/p&gt;

&lt;p&gt;By default, the setup would allow only the port 22 for your ssh access:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--3DA76ntx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-43234f546cc3fbc0bb6ab085f7c12de4.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--3DA76ntx--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-43234f546cc3fbc0bb6ab085f7c12de4.png" alt="aws default security group"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;You need more inbound ports for your server:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;22&lt;/code&gt; for your SSH access&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;80&lt;/code&gt; for your incoming http access&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;443&lt;/code&gt; for your incoming https access&lt;/li&gt;
&lt;li&gt;different ssh tunnels ports, like &lt;code&gt;8080&lt;/code&gt;, &lt;code&gt;8085&lt;/code&gt;, &lt;code&gt;8090&lt;/code&gt;, etc... The number of ports depend on the number of services you want to expose through the ssh tunnel&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can create a security group with those inbound ports:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--tPRq-QoA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-466fa35b8fc40dc33b2defc8e999a832.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--tPRq-QoA--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-466fa35b8fc40dc33b2defc8e999a832.png" alt="aws security group"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  private key file
&lt;/h3&gt;

&lt;p&gt;To use the SSH connection, you need to create a &lt;code&gt;key pair&lt;/code&gt; . Download the associated &lt;code&gt;.pem&lt;/code&gt; file, copy it in a safe place and do a &lt;code&gt;chmod 400&lt;/code&gt; on that file to avoid the error:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
@         WARNING: UNPROTECTED PRIVATE KEY FILE!          @
@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
Permissions 0644 for 'ssh_tunnel.pem' are too open.
It is required that your private key files are NOT accessible by others.
This private key will be ignored.
Load key "ssh_tunnel.pem": bad permissions
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  server configuration
&lt;/h2&gt;

&lt;p&gt;At the end of this document, we will show how to automate entirely the setup process. &lt;/p&gt;

&lt;p&gt;The following description is useful if you want to have a better understanding of what the automated script will be doing.&lt;/p&gt;

&lt;p&gt;Using your pem file, you can &lt;code&gt;ssh&lt;/code&gt; to the server and proceed with the configuration.&lt;/p&gt;

&lt;p&gt;First, make sure that all the packages are up to date&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo apt-get update
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  &lt;strong&gt;setup the sshd server for the reverse ssh tunnel&lt;/strong&gt;
&lt;/h2&gt;

&lt;p&gt;You need to make sure that the sshd server is running and has the proper configuration.&lt;/p&gt;

&lt;p&gt;This command will check the sshd configuration:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sshd -T | grep -E 'gatewayports|allowtcpforwarding'
gatewayports no
allowtcpforwarding yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;For the ssh tunnels to work, you need both parameters to be set to &lt;code&gt;yes&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Open the file &lt;code&gt;/etc/ssh/sshd_config&lt;/code&gt; and add or set those 2 lines:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;AllowTcpForwarding yes
GatewayPorts yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;Then, you need to restart the ssh daemon for those parameters to be taken into account:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;service sshd restart
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;And then you can check that all is in order:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ sshd -T | grep -E 'gatewayports|allowtcpforwarding'
gatewayports yes
allowtcpforwarding yes
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  T*&lt;em&gt;est of the reverse ssh tunnel setup&lt;/em&gt;*
&lt;/h2&gt;

&lt;p&gt;Before moving forward with the server configuration, you can already check that the ssh setup is working properly.&lt;/p&gt;

&lt;p&gt;In order to test that a local service can be reached from the public internet, you need to start some sort of local server on your development machine.&lt;/p&gt;

&lt;p&gt;A simple HTTP server will do the trick: from a directory containing no sensitive data, you can start a simple python server:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;python -m SimpleHTTPServer 8000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You can check from your local browser from &lt;code&gt;http:127.0.0.1:8000&lt;/code&gt; that the server is working, and ... not exposing sensitive data.&lt;/p&gt;

&lt;p&gt;Now is the time to invoke the SSH port forwarding voodoo incantation, from your local machine:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -i "tunnel.pem" ubuntu@ec2-a-b-c-d.us-west-1.compute.amazonaws.com -N -R 8080:localhost:8000
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;This SSH command is much simpler than it looks like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ubuntu@ec2-a-b-c-d.us-west-1.compute.amazonaws.com&lt;/code&gt; this is the public DNS Address of your server and you can find the value in your aws/ec2 console&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-N&lt;/code&gt;: by default, ssh will create a shell on the remote machine. We don't need that here&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;-R&lt;/code&gt;: with this option you are asking ssh to answer on the remote side (your gateway)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;8080:localhost:8000&lt;/code&gt;: any connection on port &lt;code&gt;8080&lt;/code&gt; on the gateway will be tunneled to the the port &lt;code&gt;8000&lt;/code&gt; on your local machine (where the webserver we started previously is listening on).&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all work as expected, you can open your browser at: &lt;code&gt;http://ec2-a-b-c-d.us-west-1.compute.amazonaws.com:8080&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;and you should see the file served by your local server!&lt;/p&gt;

&lt;p&gt;Your reverse ssh tunnel is working.&lt;/p&gt;

&lt;p&gt;More information about reverse ssh tunneling can be found &lt;a href="https://unix.stackexchange.com/questions/46235/how-does-reverse-ssh-tunneling-work"&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;h2&gt;
  
  
  DNS setup
&lt;/h2&gt;

&lt;p&gt;The next step is that you want a &lt;em&gt;nicer&lt;/em&gt; URL to access your service right?&lt;/p&gt;

&lt;p&gt;You need to configure the DNS for your domain &lt;em&gt;yourdomain.com&lt;/em&gt; and create a &lt;code&gt;A record&lt;/code&gt; for &lt;em&gt;api.yourdomain.com&lt;/em&gt; , with a TTL of 600, with a value of &lt;code&gt;a.b.c.d&lt;/code&gt; which is the IP address of your gateway.&lt;/p&gt;

&lt;p&gt;I may take time for the DNS configuration to propagate, but once it is done, you can then access your local web server through the URL: &lt;code&gt;http://api.yourdomain.com:8080&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Better, but can still be improved: you may want to setup multiple subdomains which would allow you to host multiple local services, or have multiple machines using this tunnel (each using a specific subdomain).&lt;/p&gt;

&lt;p&gt;To do that, you need a reverse proxy on your gateway.&lt;/p&gt;
&lt;h2&gt;
  
  
  HAProxy setup
&lt;/h2&gt;

&lt;p&gt;Let's install HAProxy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;$ install haproxy
$ sudo apt-get install -y haproxy
$ haproxy -v
HA-Proxy version 1.8.8-1ubuntu0.9 2019/12/02
Copyright 2000-2018 Willy Tarreau &amp;lt;willy@haproxy.org&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You then need to configure its main configuration file: &lt;code&gt;/etc/haproxy/haproxy.cfg&lt;/code&gt;&lt;/p&gt;

&lt;p&gt;The setup is pretty basic: base on the domain being accessed (like &lt;code&gt;api.yourdomain.com&lt;/code&gt;), you serve data from a local server (&lt;code&gt;127.0.0.1:8080&lt;/code&gt;), which, through the ssh reverse tunnel will connect back to your local machine&lt;/p&gt;

&lt;p&gt;Example of subdomain setting in &lt;code&gt;haproxy.cfg&lt;/code&gt;&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;frontend account
    bind *:80
    mode http
    acl host_api hdr(host) -i api.yourdomain.com
    use_backend account if host_api

backend api
    mode http
    server node1 127.0.0.1:8080
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;With that configuration, you can then verify that the URL &lt;a href="http://api.yourdomain.com"&gt;&lt;code&gt;http://api.yourdomain.com&lt;/code&gt;&lt;/a&gt; serves also your local HTTP server data.&lt;/p&gt;

&lt;p&gt;We won't go any further yet as the configuration of HAProxy is very dependent on the next step.&lt;/p&gt;
&lt;h1&gt;
  
  
  SSL - letsencrypt
&lt;/h1&gt;

&lt;p&gt;Nowadays, https is almost always required to access a server.&lt;/p&gt;

&lt;p&gt;You can easily get a free SSL certificate using &lt;a href="https://letsencrypt.org/"&gt;Let's encrypt&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;The installation of the required tool is easy:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo add-apt-repository -y ppa:certbot/certbot
sudo apt-get update
sudo apt-get install -y certbot
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;There is a rate limiting with let's encrypt services so... you are limited in the number of trial and errors to configure your certificates.&lt;/p&gt;

&lt;p&gt;There are 2 conditions to ensure before starting the let's encrypt setup:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you need to make sure that the DNS setup is done and propagated for &lt;code&gt;http://yourdomain.com&lt;/code&gt; domain name - the let's encrypt server relies on that to ensure that you are the rightful owner of the domain&lt;/li&gt;
&lt;li&gt;you must stop &lt;code&gt;haproxy&lt;/code&gt; or any service using the port &lt;code&gt;80&lt;/code&gt; as this port will be used by let's encrypt, using their own &lt;code&gt;cerbot&lt;/code&gt; server to retrieve the certificate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Once those conditions are met, you can start the certificate retrieval process:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo certbot certonly --standalone -d yourdomain.com -d api.yourdomain.com -d www.yourdomain.com --non-interactive --agree-tos --email you@email.com
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;You need to change the command line above with:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the list of subdomains you want to have a certificate for in the &lt;code&gt;-d&lt;/code&gt; arguments&lt;/li&gt;
&lt;li&gt;your email address as the &lt;code&gt;-email&lt;/code&gt; argument&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If all goes well, you should see this kind of output:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Saving debug log to /var/log/letsencrypt/letsencrypt.log
Plugins selected: Authenticator standalone, Installer None
Obtaining a new certificate
Performing the following challenges:
http-01 challenge for yourdomain.com
http-01 challenge for api.yourdomain.com
http-01 challenge for www.yourdomain.com
Waiting for verification...
Cleaning up challenges

IMPORTANT NOTES:
- Congratulations! Your certificate and chain have been saved at:
   /etc/letsencrypt/live/yourdomain.com/fullchain.pem
   Your key file has been saved at:
   /etc/letsencrypt/live/yourdomain.com/privkey.pem
   Your cert will expire on 2020-04-25. To obtain a new or tweaked
   version of this certificate in the future, simply run certbot
   again. To non-interactively renew *all* of your certificates, run
   "certbot renew"
 - If you like Certbot, please consider supporting our work by:

   Donating to ISRG / Let's Encrypt:   https://letsencrypt.org/donate
   Donating to EFF:                    https://eff.org/donate-le
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  import certificates to HAProxy
&lt;/h2&gt;

&lt;p&gt;The certificates generated above will be used by HAProxy and they need some massaging to be usable there:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;sudo mkdir /etc/haproxy/certs
DOMAIN='yourdomain.com' sudo -E bash -c 'cat /etc/letsencrypt/live/$DOMAIN/fullchain.pem /etc/letsencrypt/live/$DOMAIN/privkey.pem &amp;gt; /etc/haproxy/certs/$DOMAIN.pem'
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;p&gt;The last step is to configure HAProxy, with all your domain and the https setup.&lt;/p&gt;

&lt;p&gt;We will cover that in the next section&lt;/p&gt;
&lt;h2&gt;
  
  
  Automation
&lt;/h2&gt;

&lt;p&gt;As you can see, there are a lot of sets involved to set up a server but there is a way to automate the entire process.&lt;/p&gt;
&lt;h3&gt;
  
  
  ec2 template
&lt;/h3&gt;

&lt;p&gt;You can speed up the creation of the ec2 instance by using a template.&lt;/p&gt;

&lt;p&gt;You can create a template from the instance you just created and make sure that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you are using the right security group with all the inbound ports you want to use&lt;/li&gt;
&lt;li&gt;you use the key-pair previously created&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;With that template, the creation of the instance becomes very easy with basically one click.&lt;/p&gt;
&lt;h3&gt;
  
  
  automation steps
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;create an instance using the ec2 instance template&lt;/li&gt;
&lt;li&gt;once the instance is booted, connect to it using your pem file to make sure your ssh setup is correct&lt;/li&gt;
&lt;li&gt;retrieve the IP address of this new instance and update your DNS setup. Confirm with a DNS lookup that the IP address has been updated&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;you can confirm that the DNS is correct ifyou can connect to your instance with your domain name:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ssh -i "tunnel.pem" ubuntu@yourdomain.com&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;update the following script (see below) and run it with:&lt;/p&gt;

&lt;p&gt;&lt;code&gt;ssh -i "tunnel.pem" ubuntu@yourdomain.com 'bash -s' &amp;lt; setup_tunnel_host.sh&lt;/code&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  automation script - setup_tunnel_host.sh
&lt;/h3&gt;

&lt;p&gt;Here is the script that you can run to automate all the steps describe above...&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;You need to set a couple of variables at the top of that script&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;this script will setup a SSH tunnel for you domain: &lt;code&gt;BASE_DOMAIN_NAME&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;your email address for let's encrypt: &lt;code&gt;LETE_EMAIL&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;the script setup 2 subdomains with 2 ports (it's easy to update the script to use more ports/subdomains):

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;SUBDOMAIN1&lt;/code&gt;/ &lt;code&gt;REMOTE_REDIRECT_PORT1&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;SUBDOMAIN2&lt;/code&gt;/ &lt;code&gt;REMOTE_REDIRECT_PORT2&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;On your local machine, you can then initiates both tunnels with:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;ssh -i "tunnel.pem" [ubuntu@yourdomain.com](mailto:ubuntu@chirloute.com) -N -R 8080:localhost:8000 
ssh -i "tunnel.pem" [ubuntu@yourdomain.com](mailto:ubuntu@chirloute.com) -N -R 8090:localhost:3000 
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You must update the local port to match the server running on your local machine.&lt;/p&gt;

&lt;p&gt;Enjoy!&lt;/p&gt;

</description>
      <category>haproxy</category>
      <category>letsencrypt</category>
      <category>ec2</category>
      <category>ssl</category>
    </item>
    <item>
      <title>How to create a resume with Gatsbyjs</title>
      <dc:creator>Pierre Carion</dc:creator>
      <pubDate>Sun, 26 Jan 2020 19:36:45 +0000</pubDate>
      <link>https://dev.to/pcarion/how-to-create-a-resume-with-gatsbyjs-533g</link>
      <guid>https://dev.to/pcarion/how-to-create-a-resume-with-gatsbyjs-533g</guid>
      <description>&lt;p&gt;&lt;em&gt;Originally published at &lt;a href="https://pcarion.com/article/gatsby-resume"&gt;https://pcarion.com/article/gatsby-resume&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

&lt;h1&gt;
  
  
  Online resume
&lt;/h1&gt;

&lt;p&gt;There are a lot of reasons to use a static site generator to generate your resume online:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is always nice to have a fast loading time&lt;/li&gt;
&lt;li&gt;you don't want to take any chance with the site being down when a prospective employer accesses your resume&lt;/li&gt;
&lt;li&gt;it is cheap to deploy: the load on the page should be low, so a free hosting solution like &lt;a href="https://www.netlify.com"&gt;Netlify&lt;/a&gt; makes sense&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For all those reasons, &lt;a href="https://www.gatsbyjs.org/"&gt;Gatsby&lt;/a&gt; makes a good choice as a platform to publish your resume online.&lt;/p&gt;

&lt;p&gt;This article will do exactly that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;the resume we will build is deployed with Netlify at &lt;a href="https://resume-with-gatsby.netlify.com/"&gt;https://resume-with-gatsby.netlify.com/&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;you can find the source code on GitHub at &lt;a href="https://github.com/pcarion/resume-with-gatsby"&gt;https://github.com/pcarion/resume-with-gatsby&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Resume definition format
&lt;/h1&gt;

&lt;p&gt;You will update the content of your resume regularly, so you really don't want to sprinkle this content inside a static HTML template.&lt;/p&gt;

&lt;p&gt;The &lt;a href="https://jsonresume.org/"&gt;JSON Resume&lt;/a&gt; is an open-source project to create a JSON-based standard for resumes.&lt;/p&gt;

&lt;p&gt;Personally, I find that a JSON file is more difficult to update than a yaml file, so we'll use the format defined by the JSON resume project, but in a YAML format.&lt;/p&gt;

&lt;p&gt;The project comes with a sample resume, from the fictional character Richard Hendriks from the excellent series &lt;a href="https://www.hbo.com/silicon-valley"&gt;"Silicon Valley" on HBO&lt;/a&gt;: &lt;/p&gt;

&lt;p&gt;&lt;a href="https://github.com/jsonresume/resume-schema/blob/v1.0.0/examples/valid/complete.json"&gt;https://github.com/jsonresume/resume-schema/blob/v1.0.0/examples/valid/complete.json&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using a YAML format, the resume looks like this:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;---
basics:
  name: Richard Hendriks
  label: Programmer
  image: ''
  email: richard.hendriks@mail.com
  phone: "(912) 555-4321"
  url: http://richardhendricks.example.com
  summary: Richard hails from Tulsa. He has earned degrees from the University of
    Oklahoma and Stanford. (Go Sooners and Cardinal!) Before starting Pied Piper,
    he worked for Hooli as a part time software developer. While his work focuses
    on applied information theory, mostly optimizing lossless compression schema of
    both the length-limited and adaptive variants, his non-work interests range widely,
    everything from quantum computing to chaos theory. He could tell you about it,
    but THAT would NOT be a “length-limited” conversation!
  location:
    address: 2712 Broadway St
    postalCode: CA 94115
    city: San Francisco
    countryCode: US
    region: California
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1&gt;
  
  
  Gatsby setup
&lt;/h1&gt;

&lt;p&gt;To install Gatsby, you start with installing the 3 required packages&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;**yarn init -y
yarn add gatsby react react-dom**
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can then add the usual scripts in your &lt;code&gt;package.json&lt;/code&gt; file to start the Gatsby server:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;"scripts": {
  "build": "gatsby build",
  "clean": "gatsby clean",
  "develop": "gatsby develop"
},
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;At this stage, you can already start Gatsby with &lt;code&gt;yarn develop&lt;/code&gt;, but there is nothing available for rendering.&lt;/p&gt;

&lt;h1&gt;
  
  
  Loading the resume in Gatsby
&lt;/h1&gt;

&lt;p&gt;The &lt;code&gt;gatsby-source-filesystem&lt;/code&gt; plugin allows you to load any file in Gatsby.&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add gatsby-source-filesystem
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;To configure this plugin, you create a &lt;code&gt;gatsby-config.js&lt;/code&gt; file where you tell the plugin which directory to parse to read files.&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;module.exports = {
  plugins: [
    {
      resolve: `gatsby-source-filesystem`,
      options: {
        path: `./resume`,
      },
    },
  ],
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;You can then start Gatsby (&lt;code&gt;yarn run develop&lt;/code&gt;) and load the Graphiql server, usually at that URL: &lt;a href="http://localhost:8000/___graphql"&gt;&lt;code&gt;http://localhost:8000/___graphql&lt;/code&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;gatsby-source-filesystem&lt;/code&gt;will populate the &lt;code&gt;allFile&lt;/code&gt; root with the files it found, as shown by this query:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query MyQuery {
  allFile {
    edges {
      node {
        absolutePath
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;the results will show your resume file:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "data": {
    "allFile": {
      "edges": [
        {
          "node": {
            "absolutePath": "&amp;lt;path to&amp;gt;/resume/resume.yaml"
          }
        }
      ]
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1&gt;
  
  
  Exploration
&lt;/h1&gt;

&lt;p&gt;The step here is not required, but it shows how you could write your own plugin to parse the resume.&lt;/p&gt;

&lt;p&gt;In the &lt;code&gt;gatsby-node.js&lt;/code&gt; file,  you could write a very basic plugin to read the content of the resume, using the &lt;code&gt;onCreateNode&lt;/code&gt; method to later create new nodes to expose your resume in the GraphQL API:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;async function onCreateNode({ node, loadNodeContent }) {
  if (node.internal.mediaType !== `text/yaml`) {
    return;
  }

  const content = await loadNodeContent(node);
  console.log(`File: ${node.absolutePath}:`);
  console.log(content);
}

exports.onCreateNode = onCreateNode;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;When you restart the server, you can see the content of the resume file on screen:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;success onPreBootstrap - 0.009s
success createSchemaCustomization - 0.002s
File: &amp;lt;path to&amp;gt;/resume/resume.yaml:
---
basics:
  name: Richard Hendriks
  label: Programmer
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Interesting but there are existing plugins who already do that ... and much more.&lt;/p&gt;

&lt;h1&gt;
  
  
  Transformer plugin: gatsby-transformer-yaml
&lt;/h1&gt;

&lt;p&gt;This plugin is a good candidate for us:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it reads the yaml file&lt;/li&gt;
&lt;li&gt;parses it&lt;/li&gt;
&lt;li&gt;makes it available if the GraphQL API&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You install that plugin with:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add gatsby-transformer-yaml
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and the magic happens when you restart the server: 2 new root types appear in the Graphiql interface, &lt;code&gt;allResumeYaml&lt;/code&gt; and &lt;code&gt;resumeYaml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;If we suppose that the site will contain only one resume, we can get the content of the resume  using that query:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query MyQuery {
  resumeYaml {
    basics {
      email
      name
      phone
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and, we get back:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;{
  "data": {
    "resumeYaml": {
      "basics": {
        "email": "richard.hendriks@mail.com",
        "name": "Richard Hendriks",
        "phone": "(912) 555-4321"
      }
    }
  }
}
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h1&gt;
  
  
  Time to render some data
&lt;/h1&gt;

&lt;p&gt;Let's create our first page:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;you can delete the &lt;code&gt;gatsby-node.js&lt;/code&gt; file containing our exploration plugin.&lt;/li&gt;
&lt;li&gt;create a &lt;code&gt;src/pages/index.js&lt;/code&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;We'll start with the most basic data extraction (name and email) to make sure that all is in order.&lt;/p&gt;

&lt;p&gt;In &lt;code&gt;src/pages/index.js&lt;/code&gt; :&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import React from 'react';

const Resume = ({ data }) =&amp;gt; {
  const resume = data.resumeYaml;
  const { basics } = resume;
  return (
    &amp;lt;React.Fragment&amp;gt;
      &amp;lt;h1&amp;gt;{basics.name}&amp;lt;/h1&amp;gt;
      &amp;lt;h2&amp;gt;{basics.email}&amp;lt;/h2&amp;gt;
    &amp;lt;/React.Fragment&amp;gt;
  );
};

export default Resume;

export const query = graphql`
  query MyQuery {
    resumeYaml {
      basics {
        email
        name
      }
    }
  }
`;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;This will give us our first page:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--_0_RJUd8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-7d499c186d7eba460e5b21920d9c4170.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--_0_RJUd8--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://pcarion.com/static/Untitled-7d499c186d7eba460e5b21920d9c4170.png" alt="First view"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Not quite enough yet to get you a job at Hooli, but it's a promising start.&lt;/p&gt;

&lt;h1&gt;
  
  
  Resume rendering
&lt;/h1&gt;

&lt;p&gt;The JSON resume project contains also &lt;a href="https://jsonresume.org/themes/"&gt;a library of themes&lt;/a&gt; to render resume following the specification.&lt;/p&gt;

&lt;p&gt;For the purpose of this presentation, I have chosen the &lt;a href="https://themes.jsonresume.org/theme/flat"&gt;flat theme&lt;/a&gt; by &lt;a href="https://github.com/erming"&gt;Mattias Erming&lt;/a&gt;, because&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;it is pretty well designed&lt;/li&gt;
&lt;li&gt;it is open source (MIT) - &lt;a href="https://github.com/erming/jsonresume-theme-flat"&gt;https://github.com/erming/jsonresume-theme-flat&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;the styling is easy to extract: one &lt;code&gt;style.css&lt;/code&gt; file and one template file&lt;/li&gt;
&lt;/ul&gt;

&lt;h1&gt;
  
  
  Process to build the resume
&lt;/h1&gt;

&lt;p&gt;I won't describe all the steps to port that template to Gatsby, just the major steps&lt;/p&gt;

&lt;h2&gt;
  
  
  extra package: gatsby-react-helmet
&lt;/h2&gt;

&lt;p&gt;Helmet is nice addition because it makes it very easy to:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;to set page title&lt;/li&gt;
&lt;li&gt;add external css dependencies&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You install that package with:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;yarn add gatsby-plugin-react-helmet react-helmet
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and add it to your &lt;code&gt;gatsby-config.js&lt;/code&gt; file.&lt;/p&gt;

&lt;h2&gt;
  
  
  issue with CSS ordering
&lt;/h2&gt;

&lt;p&gt;The way you add a specif css document (the one coming with the flat theme example), is by importing if in the &lt;code&gt;gatsby-browser.js&lt;/code&gt; document:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import "./src/styles/style.css"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The style requires 2 other CSS loaded from a CDN, and you can use Helmet to do that:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;Helmet&amp;gt;
  &amp;lt;title&amp;gt;{basics.name}&amp;lt;/title&amp;gt;
  &amp;lt;link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.2.0/css/bootstrap.min.css"
  /&amp;gt;
  &amp;lt;link
    rel="stylesheet"
    href="https://cdnjs.cloudflare.com/ajax/libs/octicons/2.0.2/octicons.min.css"
  /&amp;gt;

&amp;lt;/Helmet&amp;gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;The problem is that the local style loaded by &lt;code&gt;gatsby-browser.js&lt;/code&gt; is loaded &lt;em&gt;before&lt;/em&gt; the styles loaded by Helmet and that broke the rendering: for example the H1..4 styles were the ones defined by bootstrap, and not the local styles file.&lt;/p&gt;

&lt;p&gt;The only solution I found, was to load bootstrap as a dependency and load it from &lt;code&gt;gatsby-browser.js&lt;/code&gt;, before the local style:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;import 'bootstrap/dist/css/bootstrap.css'
import "./src/styles/style.css"
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;
&lt;h2&gt;
  
  
  porting process
&lt;/h2&gt;

&lt;p&gt;The process involved updating the GraphQL query to import more and more data (the minimal theme only omits the &lt;code&gt;projects&lt;/code&gt; section of the resume).&lt;/p&gt;

&lt;p&gt;The final query asks for all the data:&lt;/p&gt;
&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;query MyQuery {
    resumeYaml {
      basics {
        email
        name
        label
        phone
        url
        summary
        profiles {
          network
          url
          username
        }
      }
      work {
        description
        endDate(formatString: "MMM, YYYY")
        highlights
        location
        name
        startDate(formatString: "MMM, YYYY")
        position
        summary
        url
      }
      volunteer {
        endDate(formatString: "MMM, YYYY")
        highlights
        organization
        position
        startDate(formatString: "MMM, YYYY")
        summary
        url
      }
      education {
        area
        courses
        endDate(formatString: "MMM, YYYY")
        gpa
        institution
        startDate(formatString: "MMM, YYYY")
        studyType
      }
      awards {
        awarder
        date(formatString: "MMM, YYYY")
        summary
        title
      }
      publications {
        name
        publisher
        releaseDate(formatString: "MMM, YYYY")
        summary
        url
      }
      skills {
        keywords
        level
        name
      }
      languages {
        fluency
        language
      }
      interests {
        keywords
        name
      }
      references {
        name
        reference
      }
    }
  }
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;and each section of the resume has its own component:&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight plaintext"&gt;&lt;code&gt;const Resume = ({ resume }) =&amp;gt; {
  const { basics, work, volunteer, education, awards, publications, skills, languages, interests, references } = resume;
  return (
&amp;lt;React.Fragment&amp;gt;
  &amp;lt;Helmet&amp;gt;
    &amp;lt;title&amp;gt;{basics.name}&amp;lt;/title&amp;gt;
    &amp;lt;link
      rel="stylesheet"
      href="https://cdnjs.cloudflare.com/ajax/libs/octicons/2.0.2/octicons.min.css"
    /&amp;gt;
  &amp;lt;/Helmet&amp;gt;
  &amp;lt;Header basics={basics} /&amp;gt;
  &amp;lt;div id="content" className="container"&amp;gt;
    &amp;lt;Contact basics={basics} /&amp;gt;
    &amp;lt;About basics={basics} /&amp;gt;
    &amp;lt;Profiles profiles={basics.profiles||[]} /&amp;gt;
    &amp;lt;Work works={work} /&amp;gt;
    &amp;lt;Volunteer volunteers={volunteer} /&amp;gt;
    &amp;lt;Education educations={education} /&amp;gt;
    &amp;lt;Awards awards={awards} /&amp;gt;
    &amp;lt;Publications publications={publications} /&amp;gt;
    &amp;lt;Skills skills={skills} /&amp;gt;
    &amp;lt;Languages languages={languages} /&amp;gt;
    &amp;lt;Interests interests={interests} /&amp;gt;
    &amp;lt;References references={references} /&amp;gt;
  &amp;lt;/div&amp;gt;
&amp;lt;/React.Fragment&amp;gt;
);
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;

&lt;p&gt;Pretty easy to read and update.&lt;/p&gt;

&lt;h1&gt;
  
  
  Deployment
&lt;/h1&gt;

&lt;p&gt;We won't describe here the actual deployment as there are a lot of articles out there describing how to deploy a Gatsby site.&lt;/p&gt;

&lt;p&gt;For instance, the site &lt;a href="https://resume-with-gatsby.netlify.com/"&gt;https://resume-with-gatsby.netlify.com/&lt;/a&gt; was deployed on &lt;a href="https://www.netlify.com"&gt;Netlify&lt;/a&gt; with a couple of clicks.&lt;/p&gt;

</description>
      <category>gatsby</category>
      <category>resume</category>
      <category>netlify</category>
    </item>
  </channel>
</rss>
