<?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>Forem: Robert Zhu</title>
    <description>The latest articles on Forem by Robert Zhu (@robzhu).</description>
    <link>https://forem.com/robzhu</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%2F223166%2Ffa165a5b-2ea8-4fce-9f3c-a72e3b0f3498.png</url>
      <title>Forem: Robert Zhu</title>
      <link>https://forem.com/robzhu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/robzhu"/>
    <language>en</language>
    <item>
      <title>How to build a Pocket Platform-as-a-Service</title>
      <dc:creator>Robert Zhu</dc:creator>
      <pubDate>Tue, 03 Sep 2019 16:55:53 +0000</pubDate>
      <link>https://forem.com/robzhu/how-to-build-a-pocket-platform-as-a-service-2k38</link>
      <guid>https://forem.com/robzhu/how-to-build-a-pocket-platform-as-a-service-2k38</guid>
      <description>&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--KcPYEN1B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/2000/0%2AHZBBO9srk3RLjDqY.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--KcPYEN1B--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/2000/0%2AHZBBO9srk3RLjDqY.jpg" alt="construction site scaffolding"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Prerequisite knowledge: Linux, SSH, SSL, Docker, Nginx, HTTP, DNS&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--GtGAFxcd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1600/0%2A7cLM0a0n-c3WUp2M" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--GtGAFxcd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1600/0%2A7cLM0a0n-c3WUp2M" alt="project requirements spectrum"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What kind of infrastructure do we need for new projects? I would prioritize simplicity, flexibility, value, and on-demand capacity. For new web-based projects, I find myself quickly hitting the following requirements:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DNS configuration&lt;/li&gt;
&lt;li&gt;SSL support&lt;/li&gt;
&lt;li&gt;Subdomain to a service&lt;/li&gt;
&lt;li&gt;SSL reverse proxy to localhost (similar to ngrok and serveo)&lt;/li&gt;
&lt;li&gt;Automatic deployment after a commit to the source repo (nice to have)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://aws.amazon.com/lightsail/?&amp;amp;trk=el_a131L0000057ybbQAA&amp;amp;trkCampaign=pac-edm-2019-Pocket-PaaS-Medium&amp;amp;sc_channel=el&amp;amp;sc_campaign=pac-edm-2019-Pocket-PaaS-Medium-blog&amp;amp;sc_outcome=Enterprise_Digital_Marketing"&gt;Amazon Lightsail&lt;/a&gt; is perfect for building a simple “Pocket Platform” that provides all these features. It’s cheap and easy for beginners, and provides a friendly interface for managing virtual machines and DNS. Let’s assemble our Pocket Platform together on Lightsail, step-by-step.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;If you prefer to follow along with videos instead: &lt;a href="https://youtu.be/iqOq8LM3lXE"&gt;part 1&lt;/a&gt;, &lt;a href="https://youtu.be/tn9snFMXa0M"&gt;part 2&lt;/a&gt;, &lt;a href="https://www.youtube.com/watch?v=5Y5h0RT1cnA"&gt;part 3&lt;/a&gt;&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Domain Name &amp;amp; Static IP
&lt;/h2&gt;

&lt;p&gt;First, we’ll need a domain name for our project. You can register your domain with any domain name registration service, such as Amazon &lt;a href="https://aws.amazon.com/route53/"&gt;Route53&lt;/a&gt;. Once your domain is registered, open the &lt;a href="https://lightsail.aws.amazon.com/ls/webapp"&gt;Lightsail console&lt;/a&gt;, click the Networking tab and click Create static IP:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--vVZd6Zfa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1002/0%2APIhqYmiojWoHlnDH" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--vVZd6Zfa--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1002/0%2APIhqYmiojWoHlnDH" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Give it a name you can remember and don’t worry about attaching it to an instance just yet. Next, click “Create DNS Zone”:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--SnC7dV3I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1046/0%2AeXq82GPvkXpm4KFE" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--SnC7dV3I--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1046/0%2AeXq82GPvkXpm4KFE" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;On the Create a DNS zone page, enter your domain name, and click Create DNS zone. For the remainder of this guide, I’m using one of my domains “raccoon.news”.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--0d3LBkYj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/687/0%2AuRKEYRGeoQBi6xB7" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--0d3LBkYj--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/687/0%2AuRKEYRGeoQBi6xB7" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Create two A records, “@.raccoon.news” and “dev.raccoon.news”, both resolving to the static IP address you created earlier. Then copy the values for the Lightsail name servers at the bottom of the page. Go back to your domain name provider, and edit the name servers to point to the Lightsail name servers. Since I registered my domain with Route53, here’s what it looks like:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--nGs8IYJy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/506/0%2A2NzdPTZ30sh1IP8u" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--nGs8IYJy--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/506/0%2A2NzdPTZ30sh1IP8u" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Note: if you registered your domain with Route53, make sure you are changing the name server values under “domain registration”, not hosted zones. In fact, if you registered your domain with Route53, you’ll need to delete the hosted zone that Route53 automatically creates for your domain.&lt;/p&gt;

&lt;p&gt;While we wait for our DNS changes to propagate, let’s set up our Lightsail instance.&lt;/p&gt;

&lt;h2&gt;
  
  
  Basic Server Setup
&lt;/h2&gt;

&lt;p&gt;In the Lightsail console, create a new instance and select Ubuntu 18.04. For the purposes of this guide, you can use the cheapest instance, but once you’re running anything in production, choose an instance that has enough capacity for your workload.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EdMPnG74--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/949/0%2ASZjxznQdFwW8EaP3" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EdMPnG74--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/949/0%2ASZjxznQdFwW8EaP3" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Once the instance has launched, select manage and open two additional TCP ports: 443 and 2222. Then, under instance/networking, attach the static IP we allocated earlier.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--EQo_en8T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/751/0%2AwvC7kmr40zj_RnqH" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--EQo_en8T--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/751/0%2AwvC7kmr40zj_RnqH" alt="IP Address and Firewall settings"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;SSH to the Lightsail instance, download the SSH key, and save it to a friendly filepath, for example: &lt;strong&gt;~/ls_ssh_key.pem&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oNZHorqJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/620/0%2Axp5HZxsebJLh8qEP" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oNZHorqJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/620/0%2Axp5HZxsebJLh8qEP" alt="Manage your SSH keys"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Restrict permissions for your SSH key and SSH to the instance:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# restrict permissions for your SSH key&lt;/span&gt;
&lt;span class="nb"&gt;chmod &lt;/span&gt;400 ~/ls_ssh_key.pem

&lt;span class="c"&gt;# SSH to the instance&lt;/span&gt;
ssh &lt;span class="nt"&gt;-i&lt;/span&gt; ls_ssh_key.pem ubuntu@STATIC_IP
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Once we’re connected to the instance install Docker to help us manage deployment and configuration:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get update &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sudo &lt;/span&gt;apt-get &lt;span class="nb"&gt;install &lt;/span&gt;docker.io
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl start docker
&lt;span class="nb"&gt;sudo &lt;/span&gt;systemctl &lt;span class="nb"&gt;enable &lt;/span&gt;docker
docker run hello-world
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;After docker is installed, we’re going to set up a gateway using called the &lt;a href="https://github.com/jwilder/nginx-proxy"&gt;nginx-proxy&lt;/a&gt; container. This container lets us route traffic to other containers by providing the “VIRTUAL_HOST” environment variable. Conveniently, nginx-proxy comes with an SSL companion, &lt;a href="https://github.com/JrCs/docker-letsencrypt-nginx-proxy-companion/blob/master/docs/Basic-usage.md"&gt;nginx-proxy-letsencrypt&lt;/a&gt;, which uses &lt;a href="https://letsencrypt.org/"&gt;Let’s Encrypt&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# start the reverse proxy container&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; nginx-proxy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--publish&lt;/span&gt; 80:80 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--publish&lt;/span&gt; 443:443 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--volume&lt;/span&gt; /etc/nginx/certs &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--volume&lt;/span&gt; /etc/nginx/vhost.d &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--volume&lt;/span&gt; /usr/share/nginx/html &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--volume&lt;/span&gt; /var/run/docker.sock:/tmp/docker.sock:ro &lt;span class="se"&gt;\&lt;/span&gt;
    jwilder/nginx-proxy

&lt;span class="c"&gt;# start the letsencrypt companion&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; nginx-proxy-letsencrypt &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--volumes-from&lt;/span&gt; nginx-proxy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--volume&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock:ro &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"DEFAULT_EMAIL=YOUREMAILHERE"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    jrcs/letsencrypt-nginx-proxy-companion

&lt;span class="c"&gt;# start a demo web server under a subdomain&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;docker run &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; nginx &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"VIRTUAL_HOST=test.EXAMPLE.COM"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"LETSENCRYPT_HOST=test.EXAMPLE.COM"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    nginx
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;Pay special attention to setting a valid email for the DEFAULT_EMAIL environment variable on the proxy companion or else you will need to specify the email whenever you start a new container. If everything went well, you should be able to navigate to &lt;a href="https://test.EXAMPLE.COM"&gt;https://test.EXAMPLE.COM&lt;/a&gt; and see the nginx default content with a valid SSL certificate that has been auto-generated by Let’s Encrypt:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--YMcGfcZ_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/665/0%2AEdPeEhfjiReDWDvf" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--YMcGfcZ_--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/665/0%2AEdPeEhfjiReDWDvf" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Two common mistakes to watch out for: &lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Make sure Port 443 open in the Lightsail console&lt;/li&gt;
&lt;li&gt;Let’s encrypt rate limiting: &lt;a href="https://letsencrypt.org/docs/rate-limits/"&gt;https://letsencrypt.org/docs/rate-limits/&lt;/a&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Localhost Proxy with SSL
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--VYJ496Z2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/550/0%2AIobqPd8T21Hnp1Wx" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--VYJ496Z2--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/550/0%2AIobqPd8T21Hnp1Wx" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Most developers prefer to code on a dev machine (laptop or desktop) because they can access the file system, use their favorite IDE, recompile, debug, and more. Unfortunately, developing on a dev machine can introduce bugs due to differences from the production environment. In addition, certain services (e.g. Alexa Skills, GitHub Webhooks) require SSL in order to work, which can be annoying to configure on your local machine. We can use an SSL reverse proxy to make our local dev environment resemble production from the browser’s point of view. This technique also helps allow our test application to make API requests to production endpoints with &lt;a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS"&gt;Cross-Origin Resource Sharing&lt;/a&gt; restrictions. While it’s not a perfect solution, it takes us one step closer toward a frictionless dev/test feedback loop.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;You may have used services like &lt;a href="http://ngrok.com/"&gt;ngrok&lt;/a&gt; and &lt;a href="https://serveo.net/"&gt;serveo&lt;/a&gt; for this purpose. By running a reverse proxy ourselves, we won’t need to spread our domain and SSL settings across multiple services.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;To run a reverse proxy, we’ll create an &lt;a href="https://www.maketecheasier.com/reverse-ssh-tunnel-allow-external-connections/"&gt;SSH reverse tunnel&lt;/a&gt;. Once the reverse tunnel SSH session is initiated, all network requests to the specified port on the host will be proxied to our dev machine. However, since our Lightsail instance is already using port 22 for VPS management, we need a different SSH port (2222 from above). To keep everything organized, we’ll also run the SSH server for port 2222 inside a special proxy container. Here’s a diagram:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Qla127n9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1548/0%2Axl9Fxm5B-39DaRCk" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Qla127n9--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/1548/0%2Axl9Fxm5B-39DaRCk" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Using &lt;a href="https://docs.docker.com/engine/examples/running_ssh_service/"&gt;Dockerize an SSH service&lt;/a&gt; as a starting point, I’ve created a &lt;a href="https://github.com/robzhu/nginx-local-tunnel"&gt;repository&lt;/a&gt; with a working Dockerfile and nginx config for reference. Here are the summary steps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/robzhu/nginx-local-tunnel
&lt;span class="nb"&gt;cd &lt;/span&gt;nginx-local-tunnel
docker build &lt;span class="nt"&gt;-t&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;DOCKERUSER&lt;span class="o"&gt;}&lt;/span&gt;/dev-proxy &lt;span class="nb"&gt;.&lt;/span&gt; &lt;span class="nt"&gt;--build-arg&lt;/span&gt; &lt;span class="nv"&gt;ROOTPW&lt;/span&gt;&lt;span class="o"&gt;={&lt;/span&gt;PASSWORD&lt;span class="o"&gt;}&lt;/span&gt;

&lt;span class="c"&gt;# start the proxy container&lt;/span&gt;
&lt;span class="c"&gt;# Note, 2222 is the port we opened on the instance earlier.&lt;/span&gt;
docker run &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; 2222:22 &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; dev-proxy &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"VIRTUAL_HOST=dev.EXAMPLE.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"LETSENCRYPT_HOST=dev.EXAMPLE.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="o"&gt;{&lt;/span&gt;DOCKERUSER&lt;span class="o"&gt;}&lt;/span&gt;/dev-proxy

&lt;span class="c"&gt;# Ports explained:&lt;/span&gt;
&lt;span class="c"&gt;# 3000 refers to the port that your app is running on localhost.&lt;/span&gt;
&lt;span class="c"&gt;# 2222 is the forwarded port on the host that we use to directly SSH into the container.&lt;/span&gt;
&lt;span class="c"&gt;# 80 is the default HTTP port, forwarded from the host&lt;/span&gt;
ssh &lt;span class="nt"&gt;-R&lt;/span&gt; :80:localhost:3000 &lt;span class="nt"&gt;-p&lt;/span&gt; 2222 root@dev.EXAMPLE.com

&lt;span class="c"&gt;# Start sample app on localhost&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;node-hello &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; npm i
nodemon main.js
&lt;span class="c"&gt;# Point browser to https://dev.EXAMPLE.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;The reverse proxy subdomain will only work as long as the reverse proxy SSH connection remains open. If there is no SSH connection, you should see an nginx gateway error:&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--oK64x2oC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/271/0%2Aok8kFNskEYCjtptO" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--oK64x2oC--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/271/0%2Aok8kFNskEYCjtptO" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;While this solution is handy, be extremely careful as it could expose your work-in-progress to the internet. Consider adding additional authorization logic and &lt;a href="https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-tcp/"&gt;settings for allowing/denying specific IPs&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Automatic Deployment
&lt;/h2&gt;

&lt;p&gt;Finally, let’s build an automation workflow that watches for commits on a source repository, builds an updated container image, and re-deploys the container on our host. There are many ways to do this, but here’s the combination I’ve selected for simplicity:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;GitHub&lt;/li&gt;
&lt;li&gt;&lt;a href="https://docs.docker.com/docker-hub/builds/"&gt;Docker automated builds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://containrrr.github.io/watchtower/"&gt;Watchtower&lt;/a&gt;
First, create a GitHub repository that will host your application source code. For demo purposes, you can clone my &lt;a href="https://github.com/robzhu/express-hello"&gt;express hello-world example&lt;/a&gt;. On the docker hub page, create a new repository, click the GitHub icon, and select your repository from the dropdown:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--K7q4vXxJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/686/0%2AiZiN6JdLFCQJ7uhc" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--K7q4vXxJ--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/686/0%2AiZiN6JdLFCQJ7uhc" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Now docker will watch for commits to the repo and build a new image with the “latest” tag in response. Once the image is available, start the container like so:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;--detach&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; app &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"VIRTUAL_HOST=app.raccoon.news"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--env&lt;/span&gt; &lt;span class="s2"&gt;"LETSENCRYPT_HOST=app.raccoon.news"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    robzhu/express-hello
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;p&gt;&lt;em&gt;Note: you'll need to add app.raccoon.news as an A record in your DNS settings&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Lastly, let’s use &lt;a href="https://containrrr.github.io/watchtower/"&gt;Watchtower&lt;/a&gt; to poll dockerhub and update the “app” container whenever a new image is detected:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight"&gt;&lt;pre class="highlight shell"&gt;&lt;code&gt;docker run &lt;span class="nt"&gt;-d&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--name&lt;/span&gt; watchtower &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;-v&lt;/span&gt; /var/run/docker.sock:/var/run/docker.sock &lt;span class="se"&gt;\&lt;/span&gt;
    containrrr/watchtower &lt;span class="se"&gt;\&lt;/span&gt;
    &lt;span class="nt"&gt;--interval&lt;/span&gt; 10 &lt;span class="se"&gt;\&lt;/span&gt;
    APPCONTAINERNAME
&lt;/code&gt;&lt;/pre&gt;&lt;/div&gt;



&lt;h2&gt;
  
  
  Summary
&lt;/h2&gt;

&lt;p&gt;Our Pocket PaaS is now complete! As long as we deploy new containers and add the VIRTUAL_HOST and LETSENCRYPT_HOST environment variables, we get automatic subdomain routing and SSL termination. With SSH reverse tunneling, we can develop on our local dev machine using our favorite IDE and test/share our app at &lt;a href="https://dev.EXAMPLE.COM"&gt;https://dev.EXAMPLE.COM&lt;/a&gt;. And since it’s a public URL with SSL, we can test Alexa Skills, GitHub Webhooks, CORS settings, PWAs, and anything else that requires SSL. Once we’re happy with our changes, a git commit will trigger an automated rebuild of our docker image, which gets automatically redeployed by Watchtower.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s---EKdOYLz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/480/0%2AWpb5ebvAys7OL9Fl" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s---EKdOYLz--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://miro.medium.com/max/480/0%2AWpb5ebvAys7OL9Fl" alt=""&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;I hope this was useful. Thoughts? Leave a comment or direct-message me on twitter: &lt;a href="https://twitter.com/rbzhu"&gt;@rbzhu&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>docker</category>
      <category>nginx</category>
      <category>ubuntu</category>
    </item>
  </channel>
</rss>
