<?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: Henry Wu</title>
    <description>The latest articles on Forem by Henry Wu (@hwupu).</description>
    <link>https://forem.com/hwupu</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%2F726072%2F3317bbf1-11cf-47db-b919-a6d2b926635b.jpg</url>
      <title>Forem: Henry Wu</title>
      <link>https://forem.com/hwupu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/hwupu"/>
    <language>en</language>
    <item>
      <title>Easier way to run SSR websites (Node.js) on Windows IIS</title>
      <dc:creator>Henry Wu</dc:creator>
      <pubDate>Sat, 09 Mar 2024 14:20:50 +0000</pubDate>
      <link>https://forem.com/hwupu/easier-way-to-run-ssr-websites-nodejs-on-windows-iis-m3o</link>
      <guid>https://forem.com/hwupu/easier-way-to-run-ssr-websites-nodejs-on-windows-iis-m3o</guid>
      <description>&lt;p&gt;&lt;em&gt;Sometimes you have no choice, but to use less-than-ideal tech stack the team familiar with, or perhaps the only tech stack the team knows.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  It begins as...
&lt;/h2&gt;

&lt;p&gt;Here, I have a website. While it is a simple website, but does require server-side rendering (SSR) functionality. It may not pose an issue to any &lt;em&gt;modern&lt;/em&gt; web developer, but it becomes one when handing off to old-school Windows engineer.&lt;/p&gt;

&lt;p&gt;Before it sounds like a complaint, it's worth noting that modern tech stacks and practices are generally cross-platform. There's no reason why I can't run my website under Windows IIS, regardless of how cumbersome it might be to hook up with the JavaScript runtime.&lt;/p&gt;

&lt;p&gt;After conducting research and experimenting with &lt;a href="https://learn.microsoft.com/en-us/iis/configuration/system.webserver/fastcgi/"&gt;FastCGI&lt;/a&gt; and &lt;a href="https://www.iis.net/downloads/microsoft/url-rewrite"&gt;URL Rewrite&lt;/a&gt;, I discovered another convenient module that seems to be under-discussed.&lt;/p&gt;

&lt;p&gt;I'm not an expert in Windows IIS, so I can't speak to the differences under the hood, but from my experience, I find that &lt;strong&gt;&lt;a href="https://learn.microsoft.com/en-us/iis/extensions/httpplatformhandler/httpplatformhandler-configuration-reference"&gt;httpPlatformHandler&lt;/a&gt; is easier to setup&lt;/strong&gt;.&lt;/p&gt;




&lt;p&gt;Before I share the walkthrough of IIS setup, we'll need the website running properly under JavaScript runtime (Node.js in this case).&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup httpPlatformHandler
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Download and install &lt;a href="https://www.iis.net/downloads/microsoft/httpplatformhandler"&gt;httpPlatformHandler&lt;/a&gt; from Microsoft.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Open the site under IIS, and navigate to: Handler Mapping &amp;gt; Add Module Mapping...&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter the follow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Request path&lt;/td&gt;
&lt;td&gt;*&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Module&lt;/td&gt;
&lt;td&gt;httpPlatformHandler&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Uncheck the "Request Restrictions..."&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigate to: Configuration Editor &amp;gt; system.webService/httpPlatform&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Enter the follow:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;arguments&lt;/td&gt;
&lt;td&gt;.\.output\server\index.mjs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;processPath&lt;/td&gt;
&lt;td&gt;C:\Program Files\nodejs\node.exe&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;starupTimeLimit&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Under the environmentVariables, enter the following:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Key&lt;/th&gt;
&lt;th&gt;Value&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;PORT&lt;/td&gt;
&lt;td&gt;%HTTP_PLATFORM_PORT%&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;NODE_ENV&lt;/td&gt;
&lt;td&gt;Production&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;
&lt;/li&gt;
&lt;/ol&gt;




&lt;p&gt;🎉 Let's it. As of my understanding, all http request will now be handled by Node instead.&lt;/p&gt;




&lt;p&gt;The final &lt;code&gt;web.config&lt;/code&gt; should looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="cp"&gt;&amp;lt;?xml version="1.0" encoding="UTF-8"?&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;configuration&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;system.webServer&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;handlers&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;add&lt;/span&gt; 
                &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"httpPlatformHandler"&lt;/span&gt; 
                &lt;span class="na"&gt;path=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; 
                &lt;span class="na"&gt;verb=&lt;/span&gt;&lt;span class="s"&gt;"*"&lt;/span&gt; 
                &lt;span class="na"&gt;modules=&lt;/span&gt;&lt;span class="s"&gt;"httpPlatformHandler"&lt;/span&gt; 
                &lt;span class="na"&gt;resourceType=&lt;/span&gt;&lt;span class="s"&gt;"Unspecified"&lt;/span&gt; 
                &lt;span class="na"&gt;requireAccess=&lt;/span&gt;&lt;span class="s"&gt;"Script"&lt;/span&gt; 
            &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/handlers&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;httpPlatform&lt;/span&gt; 
            &lt;span class="na"&gt;processPath=&lt;/span&gt;&lt;span class="s"&gt;"C:\Program Files\nodejs\node.exe"&lt;/span&gt; 
            &lt;span class="na"&gt;arguments=&lt;/span&gt;&lt;span class="s"&gt;".\.output\server\index.mjs"&lt;/span&gt; 
            &lt;span class="na"&gt;startupTimeLimit=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt; 
            &lt;span class="na"&gt;stdoutLogEnabled=&lt;/span&gt;&lt;span class="s"&gt;"true"&lt;/span&gt; 
            &lt;span class="na"&gt;stdoutLogFile=&lt;/span&gt;&lt;span class="s"&gt;".\node.log"&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;environmentVariables&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;environmentVariable&lt;/span&gt; 
                    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"PORT"&lt;/span&gt; 
                    &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"%HTTP_PLATFORM_PORT%"&lt;/span&gt; 
                &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
                &lt;span class="nt"&gt;&amp;lt;environmentVariable&lt;/span&gt; 
                    &lt;span class="na"&gt;name=&lt;/span&gt;&lt;span class="s"&gt;"NODE_ENV"&lt;/span&gt; 
                    &lt;span class="na"&gt;value=&lt;/span&gt;&lt;span class="s"&gt;"Production"&lt;/span&gt; 
                &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
            &lt;span class="nt"&gt;&amp;lt;/environmentVariables&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;/httpPlatform&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/system.webServer&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/configuration&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;
  Complains
  &lt;br&gt;
The project would progress much more smoothly if the &lt;em&gt;old-school&lt;/em&gt; engineers provided insightful assistance, given that it falls within their area of expertise. However, they seem to push away responsibility instead of offering support. My intuition tells me that they may not even have a deep understanding of how IIS works under the hood, as they are accustomed to simply clicking buttons on the screen.&lt;br&gt;


&lt;/p&gt;

</description>
      <category>iis</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Auto-route multiple web projects using Traefik</title>
      <dc:creator>Henry Wu</dc:creator>
      <pubDate>Fri, 08 Mar 2024 17:26:08 +0000</pubDate>
      <link>https://forem.com/hwupu/auto-route-multiple-web-projects-using-traefik-3chb</link>
      <guid>https://forem.com/hwupu/auto-route-multiple-web-projects-using-traefik-3chb</guid>
      <description>&lt;p&gt;&lt;em&gt;When I first joined a company, they had a method to deploy web projects onto dev and stage servers. However, I spotted critical flaws and inefficiency immediately.&lt;/em&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Problem:&lt;/strong&gt; The scenario was...
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;To &lt;em&gt;manually&lt;/em&gt; build Docker image on &lt;em&gt;personal&lt;/em&gt; machine, then upload to &lt;em&gt;public&lt;/em&gt; Docker Hub! &lt;em&gt;(Yes, you read it right, cooperate secrets leaked.)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;To &lt;em&gt;manually&lt;/em&gt; edit DNS records each time when setting up new projects. It's not a problem for "production" deployment, but it is not the case here. Those are short-lived projects for reviews, and there are many legacy records cluttered up.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There are multiple &lt;em&gt;over-specced&lt;/em&gt; VMs just to serve static websites, and they had to choose which one to swap with new project each time.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Connections are not HTTPS. Although it is &lt;em&gt;technically&lt;/em&gt; not a problem, but annoying.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;They have also complained significant times wasted to push and pull from Docker Hub, and to manually SSH to VMs to spin up projects. &lt;em&gt;(Though, I didn't experience it since this practice was killed by me immediately.)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Solution:&lt;/strong&gt;
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I set a wild-card in DNS record to point to a single VM.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;That VM serves as Docker host.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I configured Traefix proxy (a container) to handle routing between projects (containers) and to handle HTTPS connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I wrote custom GitHub Actions workflow to handle deployment automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;Repeat above steps for dev and stage VMs.&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  The Main Topic:
&lt;/h2&gt;

&lt;p&gt;&lt;em&gt;I'll talk about how I setup Docker host and Traefix container in this article, and I have separate article about &lt;a href="https://dev.to/hwupu/my-github-actions-workflow-for-deploying-web-projects-37ni"&gt;my custom workflow&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  System Architecture
&lt;/h3&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89m5gfppebw6054b7ec8.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2F89m5gfppebw6054b7ec8.png" alt="system architecture of my solution (explain below)"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;
  &lt;em&gt;I don't know how to post the graph in hi-res, but &lt;strong&gt;expand this for mermaid code&lt;/strong&gt;&lt;/em&gt;
  &lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;flowchart
  subgraph Local Dev Env
  LD1 --&amp;gt; LD2 --&amp;gt; LD3
  LD1[Developing]
  LD2[Testing]
  LD3[Commit]
  end

  LD3 --&amp;gt; GH1

  subgraph GitHub
  GH1 --&amp;gt; GA1
  GH1[main branch]
    subgraph GitHub Actions
    GA1 --&amp;gt; GA2 --&amp;gt; GA3 --&amp;gt; GA4 --&amp;gt; GA5 --&amp;gt; GA6 --&amp;gt; GA7 --&amp;gt; GA8
    GA1[Activate gcloud service account]
    GA2[Config docker to use Artifacts Registry]
    GA3[Generate static webpages]
    GA4[Build custom nginx docker image]
    GA5[Push to Artifacts Registry]
    GA6[Stop existing docker container on remote host]
    GA7[Copy docker-compose to remote host]
    GA8[Docker-compose up on remote host]
    end
  end

  IA1 -. credential .-&amp;gt; GA1
  GA5 -.-&amp;gt; AR1

  subgraph Google Cloud IAM
  IA1[Actifacts Registry Service Account]
  IA2[Cloud DNS Service Account]
  end

  CD1 -.-&amp;gt; TR1

  subgraph Google Cloud DNS
  CD1 --&amp;gt; CD2
  CD1[some-domain-name.com]
  CD2[Manually add records for *.some-domain-name.com]
  end

  subgraph "Docker Host (on Google Cloud)"
  GA8 --&amp;gt; VM1
  VM1 --&amp;gt; VM2 --&amp;gt; VM3 --&amp;gt; AP1
  VM1[docker-compose up]
  VM2[pull new image from repository]
  VM3[docker run]

    subgraph Traefik
    IA2 -. credential .-&amp;gt; TR2
    TR1 -.-&amp;gt; TR2 -.-&amp;gt; TR3 -.-&amp;gt; TR4 --&amp;gt; TR5
    TR1[Setup reverse proxy]
    TR2["Signing TLS (Let's Encrypt)"]
    TR3[Listen on docker.sock]
    TR4[Dynamic config routing]
    TR5[Dynamic config middlewares]
    end

    subgraph App1
    AP1 --&amp;gt; TR4
    AP1[Nginx web server]
    end

  end

  AR1 -.-&amp;gt; VM2
  subgraph Google Cloud Actifacts Registry
  AR1[Repository]
  end
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;/p&gt;

&lt;p&gt;As aforementioned, we will have:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;An Ubuntu VM to run Docker engine.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure Traefik proxy (container) to handle routing between other containers, and to handle HTTPS connections.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Google Artifact Registry to store private Docker images.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Configure GitHub Actions workflow to glue all parts together.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Additionally, we will need a SSH key pair to connect to VM, and IAM credentials to access gcloud services.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h3&gt;
  
  
  Configure Traefik Proxy
&lt;/h3&gt;

&lt;p&gt;&lt;em&gt;Assuming we have Ubuntu VM with Docker and Docker Compose installed, and both Cloud Artifacts Registry and IAM setup properly. (Let me know in the comment if anyone wish for extra walkthrough)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Within the &lt;code&gt;docker-compose.yml&lt;/code&gt; for the Traefik container, we will add:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;3.8"&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# You may want to specify a version.&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik:latest"&lt;/span&gt;

    &lt;span class="c1"&gt;# Name to your own liking.&lt;/span&gt;
    &lt;span class="na"&gt;container_name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik-proxy"&lt;/span&gt;

    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.web.address=:80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.exposedbydefault=false"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--providers.docker.network=proxy"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--api"&lt;/span&gt;

    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;80:80"&lt;/span&gt;

    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;/var/run/docker.sock:/var/run/docker.sock:ro"&lt;/span&gt;

    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.port=80"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik.rule=Host(`traefik.some-domain-name.com`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik.service=api@internal"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.http-catchall.entrypoints=web"&lt;/span&gt;

    &lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy"&lt;/span&gt;

&lt;span class="na"&gt;networks&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;proxy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;driver&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;bridge"&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;proxy"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Before you start the container, you should do the following once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Create a bridge network:
&lt;code&gt;sh
docker network create proxy
&lt;/code&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This configuration allows Traefik to setup new url whenever new docker container spins up, &lt;em&gt;over HTTP for now&lt;/em&gt;.&lt;/p&gt;

&lt;p&gt;For example, your VM is &lt;code&gt;some-domain-name.com&lt;/code&gt; and a new container configured with hostname of &lt;code&gt;aaa.some-domain-name.com&lt;/code&gt;. Traefik will configure the route of &lt;code&gt;aaa.some-domain-name.com&lt;/code&gt; to the container automatically.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure Traefik to redirect to HTTPS
&lt;/h3&gt;

&lt;p&gt;We will configure Traefik to renew certificate with Let's Encrypt by using DNS challenge. Merge following lines into &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;traefik&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="c1"&gt;# For simplicity, let's ignore aforementioned lines.&lt;/span&gt;

    &lt;span class="na"&gt;command&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--entrypoints.websecure.address=:443"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.leresolver.acme.dnschallenge=true"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.leresolver.acme.dnschallenge.provider=gcloud"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.leresolver.acme.email=your-registered@email.com"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--certificatesresolvers.leresolver.acme.storage=/acme.json"&lt;/span&gt;

    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GCE_PROJECT=your_GCP_project_id"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;GCE_SERVICE_ACCOUNT_FILE=lets-encrypt.json"&lt;/span&gt;

    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;443:443"&lt;/span&gt;

    &lt;span class="na"&gt;volumes&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./acme.json:/acme.json"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;./lets-encrypt.json:/lets-encrypt.json"&lt;/span&gt;

    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik.tls.certresolver=leresolver"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik.tls.domains[0].main=*.some-domain-name.com"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik.tls.domains[0].sans=some-domain-name.com"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.traefik.entrypoints=websecure"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.http-catchall.rule=hostregexp(`{host:.+}`)"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.http-catchall.entrypoints=web"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.http-catchall.middlewares=redirect-to-https"&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;traefik.http.middlewares.redirect-to-https.redirectscheme.scheme=https"&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Please notes that the example shown is using Google Cloud DNS, you may have to set to a different challenger and different environment variables. Follow this &lt;a href="https://go-acme.github.io/lego/dns/gcloud/" rel="noopener noreferrer"&gt;doc for gcloud challenger&lt;/a&gt;, or follow &lt;a href="https://doc.traefik.io/traefik/https/acme/#providers" rel="noopener noreferrer"&gt;this index for specific instruction of your DNS providers&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Before you start the container, you should do the following once:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Create empty ACME file for Traefik to store Let's Encrypt info:&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;touch &lt;/span&gt;acme.json
&lt;span class="nb"&gt;chmod &lt;/span&gt;600 acme.json
&lt;/code&gt;&lt;/pre&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Download the Google Cloud Artifacts Registry service account credentials (json file), and rename it to &lt;code&gt;lets-encrypt.json&lt;/code&gt;. &lt;em&gt;(or do the reverse in &lt;code&gt;docker-compose.yml&lt;/code&gt;.)&lt;/em&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Kindly note that both &lt;code&gt;acme.json&lt;/code&gt; and &lt;code&gt;lets-encrypt.json&lt;/code&gt; should be within the same folder of &lt;code&gt;docker-compose.yml&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;🎉 Walla! You should have working Traefik proxy running and should have valid certificate to serve HTTPS. &lt;em&gt;(Actually it's not that easy, please refer to &lt;a href="https://doc.traefik.io/traefik/user-guides/docker-compose/acme-dns/" rel="noopener noreferrer"&gt;official docs&lt;/a&gt; for troubleshooting; or, leave comments below, if I happened to pickup, no guaranty.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I hope this article is inspiring for you, and maybe, assist you to learn, or setup, your own Traefik proxy. I found it quite a deep learn curve initially, as there aren't much example configs that suites my scenario.&lt;/p&gt;

&lt;h3&gt;
  
  
  Configure other projects to use this Traefik proxy:
&lt;/h3&gt;

&lt;p&gt;As it is quite lengthly, please follow my second article for &lt;a href="https://dev.to/hwupu/my-github-actions-workflow-for-deploying-web-projects-37ni"&gt;custom GitHub Actions workflow and docker-compose.yml&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  &lt;strong&gt;Feedbacks:&lt;/strong&gt; from colleague,
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;em&gt;"They said" it was a hour of "labour" work&lt;/em&gt; to deploy a version of project before, but I made it &lt;strong&gt;done under 3 minutes&lt;/strong&gt;, and automatically.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I cut &lt;strong&gt;cloud expenses by half&lt;/strong&gt;.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

</description>
      <category>traefik</category>
      <category>docker</category>
      <category>webdev</category>
    </item>
    <item>
      <title>My Github Actions workflow for deploying web projects</title>
      <dc:creator>Henry Wu</dc:creator>
      <pubDate>Fri, 08 Mar 2024 11:02:15 +0000</pubDate>
      <link>https://forem.com/hwupu/my-github-actions-workflow-for-deploying-web-projects-37ni</link>
      <guid>https://forem.com/hwupu/my-github-actions-workflow-for-deploying-web-projects-37ni</guid>
      <description>&lt;p&gt;This is a follow up article of my setup to use &lt;a href="https://dev.to/hwupu/auto-route-multiple-web-projects-using-traefik-3chb"&gt;Docker and  Traefik proxy to host multiple web projects&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;In this article, I'll share my Github Actions workflow to builds and deploy docker images automatically.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fva9769v1t4thknfxikon.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media.dev.to/cdn-cgi/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Farticles%2Fva9769v1t4thknfxikon.png" alt="illustration of steps (explains below)" width="800" height="169"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;The steps are simple:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Configure web project's docker-compose.yml.&lt;/li&gt;
&lt;li&gt;Push code to Github and trigger Actions workflow.&lt;/li&gt;
&lt;li&gt;Specify runner and Node version.&lt;/li&gt;
&lt;li&gt;Run necessary setups.&lt;/li&gt;
&lt;li&gt;Build web project within runner.&lt;/li&gt;
&lt;li&gt;Build docker image.&lt;/li&gt;
&lt;li&gt;Push docker image to private repository.&lt;/li&gt;
&lt;li&gt;SSH remote commands to spin down existing docker container on remote server.&lt;/li&gt;
&lt;li&gt;SCP docker-compose.yml to remote server.&lt;/li&gt;
&lt;li&gt;SSH remote command to spin up the image on remote server.&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Assumptions
&lt;/h2&gt;

&lt;p&gt;Say, we have a Nuxt SSG project and we use Bun runtime. We have configured Dockerfile to host built files using Nginx. (Assumptions are chosen for simplicity).&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight docker"&gt;&lt;code&gt;&lt;span class="k"&gt;FROM&lt;/span&gt;&lt;span class="s"&gt; nginx&lt;/span&gt;
&lt;span class="k"&gt;COPY&lt;/span&gt;&lt;span class="s"&gt; .output/public /usr/share/nginx/html&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We have a server running Docker and Traefik container, and both &lt;code&gt;some-domain-name.com&lt;/code&gt; and &lt;code&gt;*.some-domain-name.com&lt;/code&gt; are pointing to the server.&lt;/p&gt;

&lt;p&gt;We will use a private SSH key to send remote commands and SCP files within workflow script.&lt;/p&gt;

&lt;p&gt;Lastly, we have a private container registry to store our docker images. Example here is using Google Cloud Artifact Registry.&lt;/p&gt;

&lt;p&gt;All keys are saved as private variables in GitHub.&lt;/p&gt;

&lt;h2&gt;
  
  
  Setup &lt;code&gt;docker-compose.yml&lt;/code&gt;
&lt;/h2&gt;

&lt;p&gt;We will use our private container registry, and add Traefik configurations to &lt;code&gt;docker-compose.yml&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;3.8'&lt;/span&gt;

&lt;span class="na"&gt;services&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;your-container-name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;image&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;XXX-location.pkg.dev/XXX-project/XXX-repo/XXX-image'&lt;/span&gt;
    &lt;span class="na"&gt;ports&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;80'&lt;/span&gt;
    &lt;span class="na"&gt;labels&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;traefik.enable=true'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;traefik.port=80'&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;traefik.http.routers.xxx.rule=Host(`xxx.your-domain-name.com`)'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will pull image from private registry, so refer to &lt;a href="https://cloud.google.com/artifact-registry/docs/docker/pushing-and-pulling#tag"&gt;Google's documentation on Artifact Registry&lt;/a&gt; for the actual tag.&lt;/p&gt;

&lt;p&gt;Both &lt;code&gt;ports&lt;/code&gt; and &lt;code&gt;traefik.port&lt;/code&gt; should match Nginx container's port. (Default 80 in this case).&lt;/p&gt;

&lt;p&gt;Lastly, set the url for Traefik to route traffics to your website (container).&lt;/p&gt;

&lt;h2&gt;
  
  
  We want to trigger deployment workflow on push:
&lt;/h2&gt;

&lt;p&gt;We will setup our workflow as:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  First, we will perform all necessary setups
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;REGISTRY_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-location.pkg.dev&lt;/span&gt;
  &lt;span class="na"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-project&lt;/span&gt;
  &lt;span class="na"&gt;REPO_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-repo&lt;/span&gt;
  &lt;span class="na"&gt;IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-image&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;name-of-your-liking&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;credentials_json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.XXXX }}&lt;/span&gt;
          &lt;span class="c1"&gt;# you may want to use workload_identity_provider instead,&lt;/span&gt;
          &lt;span class="c1"&gt;# but this project was setup years ago using credentials json.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/setup-gcloud@v2&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;gcloud --quiet auth configure-docker $REGISTRY_URL&lt;/span&gt;

      &lt;span class="c1"&gt;# To be continue on next section...&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;We specify to use Ubuntu runner.&lt;/li&gt;
&lt;li&gt;Use Node version 20 (LTS as of current writing).&lt;/li&gt;
&lt;li&gt;Set env variable to production.&lt;/li&gt;
&lt;li&gt;Perform checkout, setup, auth...&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Second, generate the website:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oven-sh/setup-bun@v1&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;bun install&lt;/span&gt;
          &lt;span class="s"&gt;bun run generate&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To run &lt;code&gt;generate&lt;/code&gt; or &lt;code&gt;build&lt;/code&gt; depends on your scripts in &lt;code&gt;package.json&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  Third, build Docker image.:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;docker build \&lt;/span&gt;
            &lt;span class="s"&gt;--tag "$REGISTRY_URL/$PROJECT_ID/$REPO_NAME/$IMAGE" \&lt;/span&gt;
            &lt;span class="s"&gt;.&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Then, push to private registry:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;docker push $REGISTRY_URL/$PROJECT_ID/$REPO_NAME/$IMAGE&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Configure SSH
&lt;/h2&gt;

&lt;p&gt;Here comes the interesting part. We are going to setup ssh config, remotely stop containers, scp new compose file, then spin up the new image.&lt;/p&gt;

&lt;p&gt;We will add our key to ssh config like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;echo "$SSH_KEY" &amp;gt; ~/.ssh/some_key&lt;/span&gt;
          &lt;span class="s"&gt;chmod 600 ~/.ssh/some_key&lt;/span&gt;
          &lt;span class="s"&gt;cat &amp;gt;&amp;gt;~/.ssh/config &amp;lt;&amp;lt;END&lt;/span&gt;
          &lt;span class="s"&gt;Host remote_server&lt;/span&gt;
            &lt;span class="s"&gt;HostName $SSH_HOST&lt;/span&gt;
            &lt;span class="s"&gt;User $SSH_USER&lt;/span&gt;
            &lt;span class="s"&gt;IdentityFile ~/.ssh/some_key&lt;/span&gt;
            &lt;span class="s"&gt;StrictHostKeyChecking no&lt;/span&gt;
          &lt;span class="s"&gt;END&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SSH_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_user_name_on_server&lt;/span&gt;
          &lt;span class="na"&gt;SSH_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.XXX }}&lt;/span&gt;
          &lt;span class="na"&gt;SSH_HOST&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your-domain-name.com&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We will stop and remove old image if exist:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server sudo docker compose -f $IMAGE/docker-compose.yml down&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server sudo docker image rm  $REGISTRY_URL/$PROJECT_ID/$REPO_NAME/$IMAGE&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;We then copy &lt;code&gt;docker-compose.yml&lt;/code&gt; to the server (assume it doesn't exist, or modified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server mkdir $IMAGE&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scp docker-compose.yml remote_server:$IMAGE/docker-compose.yml&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Finally, we spin up the newly build image:
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server sudo docker compose -f $IMAGE/docker-compose.yml up -d&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Final words
&lt;/h2&gt;

&lt;p&gt;There are some improvements can be made.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;I should tag the image instead of all using 'latest', but in doing so, it introduces complexity in the script to pull correct version, and to remove old versions (so it will not occupy server storage).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;I'm not sure if the method of setting ssh key in ssh config is a secure option. I assume the runner (system) and workflow scripts are "clean" in this case.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;There is minor work to manually delete old Docker images both in private registry and remote server.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Final yaml
&lt;/h2&gt;

&lt;p&gt;Here is the full yaml for your reference:&lt;/p&gt;

&lt;p&gt;
  main.yaml
  &lt;br&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build and Deploy to GCP&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;branches&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;main&lt;/span&gt;

&lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;REGISTRY_URL&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-location.pkg.dev&lt;/span&gt;
  &lt;span class="na"&gt;PROJECT_ID&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-project&lt;/span&gt;
  &lt;span class="na"&gt;REPO_NAME&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-repo&lt;/span&gt;
  &lt;span class="na"&gt;IMAGE&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;XXX-image&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;setup-build-publish-deploy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Setup, Build, Publish, and Deploy&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;strategy&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;matrix&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;20&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;
    &lt;span class="na"&gt;environment&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;production&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;use&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup gCloud CLI&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/auth@v2&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;credentials_json&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.XXXX }}&lt;/span&gt;
          &lt;span class="c1"&gt;# you may want to use workload_identity_provider instead,&lt;/span&gt;
          &lt;span class="c1"&gt;# but this project was setup years ago using credentials json.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;google-github-actions/setup-gcloud@v2&lt;/span&gt;

      &lt;span class="c1"&gt;# Configure Docker to use the gCloud CLI as credential helper for authentication.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;gcloud --quiet auth configure-docker $REGISTRY_URL&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup Bun runtime (I mean CLI)&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;oven-sh/setup-bun@v1&lt;/span&gt;

      &lt;span class="c1"&gt;# Generate static files&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Generate&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;bun install&lt;/span&gt;
          &lt;span class="s"&gt;bun run generate&lt;/span&gt;

      &lt;span class="c1"&gt;# Build Docker image&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Build&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;docker build \&lt;/span&gt;
            &lt;span class="s"&gt;--tag "$REGISTRY_URL/$PROJECT_ID/$REPO_NAME/$IMAGE" \&lt;/span&gt;
            &lt;span class="s"&gt;.&lt;/span&gt;

      &lt;span class="c1"&gt;# Push it to Google Container Registry&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|-&lt;/span&gt;
          &lt;span class="s"&gt;docker push $REGISTRY_URL/$PROJECT_ID/$REPO_NAME/$IMAGE&lt;/span&gt;

      &lt;span class="c1"&gt;# Setup SSH config for remote Docker host&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Configure SSH&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;mkdir -p ~/.ssh&lt;/span&gt;
          &lt;span class="s"&gt;echo "$SSH_KEY" &amp;gt; ~/.ssh/some_key&lt;/span&gt;
          &lt;span class="s"&gt;chmod 600 ~/.ssh/some_key&lt;/span&gt;
          &lt;span class="s"&gt;cat &amp;gt;&amp;gt;~/.ssh/config &amp;lt;&amp;lt;END&lt;/span&gt;
          &lt;span class="s"&gt;Host remote_server&lt;/span&gt;
            &lt;span class="s"&gt;HostName $SSH_HOST&lt;/span&gt;
            &lt;span class="s"&gt;User $SSH_USER&lt;/span&gt;
            &lt;span class="s"&gt;IdentityFile ~/.ssh/some_key&lt;/span&gt;
            &lt;span class="s"&gt;StrictHostKeyChecking no&lt;/span&gt;
          &lt;span class="s"&gt;END&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;SSH_USER&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;your_user_name_on_server&lt;/span&gt;
          &lt;span class="na"&gt;SSH_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;${{ secrets.XXX }}&lt;/span&gt;

      &lt;span class="c1"&gt;# SSH remote execute to stop existing Docker container &lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Stop old container&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server sudo docker compose -f $IMAGE/docker-compose.yml down&lt;/span&gt;

      &lt;span class="c1"&gt;# SSH remote execute to remove old image&lt;/span&gt;
      &lt;span class="c1"&gt;# because we are using same 'latest' tag for all image versions.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Remove old image&lt;/span&gt;
        &lt;span class="na"&gt;continue-on-error&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server sudo docker image rm  $REGISTRY_URL/$PROJECT_ID/$REPO_NAME/$IMAGE&lt;/span&gt;

      &lt;span class="c1"&gt;# SSH remote execute to create new folder&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Mkdir&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server mkdir -p $IMAGE&lt;/span&gt;

      &lt;span class="c1"&gt;# SCP docker-compose.yml&lt;/span&gt;
      &lt;span class="c1"&gt;# because it may not exist on server, or outdated.&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;SCP&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;scp docker-compose.yml remote_server:$IMAGE/docker-compose.yml&lt;/span&gt;

      &lt;span class="c1"&gt;# SSH remote execute to start new container&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ssh remote_server sudo docker compose -f $IMAGE/docker-compose.yml up -d&lt;/span&gt;

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;/p&gt;

</description>
      <category>traefik</category>
      <category>docker</category>
      <category>githubactions</category>
      <category>ssh</category>
    </item>
    <item>
      <title>Avoid `sudo n`</title>
      <dc:creator>Henry Wu</dc:creator>
      <pubDate>Sun, 25 Feb 2024 17:04:48 +0000</pubDate>
      <link>https://forem.com/hwupu/avoid-sudo-n-205m</link>
      <guid>https://forem.com/hwupu/avoid-sudo-n-205m</guid>
      <description>&lt;p&gt;&lt;em&gt;&lt;strong&gt;Note:&lt;/strong&gt; It's acutally written in &lt;a href="https://github.com/tj/n?tab=readme-ov-file#installation"&gt;README.md&lt;/a&gt;, but if you are lazy like me who skips it, let me point it out for you.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;If you found it troublesome to have to &lt;code&gt;sudo&lt;/code&gt; every time to switch Node.js versions, then you are configuring &lt;a href="https://github.com/tj/n"&gt;n&lt;/a&gt; incorrectly &lt;em&gt;(or, you are not configuring it at all).&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;I assume you have just got your new Mac and simply &lt;em&gt;copy-and-paste&lt;/em&gt; installation commands from docs, so I assume you are using &lt;code&gt;zsh&lt;/code&gt; and &lt;code&gt;Homebrew&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;em&gt;Instead of using &lt;code&gt;chown&lt;/code&gt; for &lt;code&gt;/usr/local/n&lt;/code&gt; (and associated folders; which is safe to do), I choose to set &lt;code&gt;N_PREFIX&lt;/code&gt; in &lt;code&gt;.zshrc&lt;/code&gt; based on my preference to keep configs in one place (and with comments to remind me).&lt;/em&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;p&gt;Make &lt;code&gt;.n&lt;/code&gt; folder under &lt;code&gt;$HOME&lt;/code&gt; (your home directory) to store &lt;code&gt;n&lt;/code&gt; installations:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir&lt;/span&gt; &lt;span class="nt"&gt;-p&lt;/span&gt; ~/.n/bin
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href=""&gt; &lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add &lt;code&gt;N_PREFIX&lt;/code&gt; to &lt;code&gt;.zshrc&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export N_PREFIX="$HOME/.n"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href=""&gt; &lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Add it to &lt;code&gt;$PATH&lt;/code&gt; too:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;echo&lt;/span&gt; &lt;span class="s1"&gt;'export PATH="$HOME/.n/bin:$PATH"'&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&amp;gt;&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href=""&gt; &lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Load zsh config to current shell:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;source&lt;/span&gt; ~/.zshrc
&lt;/code&gt;&lt;/pre&gt;

&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install &lt;code&gt;n&lt;/code&gt; using Homebrew:&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;n
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href=""&gt; &lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Install Node.js (LTS):&lt;br&gt;
&lt;/p&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;n lts
&lt;/code&gt;&lt;/pre&gt;


&lt;p&gt;&lt;a href=""&gt; &lt;/a&gt;&lt;/p&gt;
&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;🎉 Walla! Now you can &lt;code&gt;n&lt;/code&gt; &lt;del&gt;without &lt;code&gt;sudo&lt;/code&gt;&lt;/del&gt;. &lt;em&gt;No thank you, no thank you.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;By the way, if you have an existing system that you would like to configure, you may run &lt;code&gt;sudo n uninstall&lt;/code&gt; to remove &lt;code&gt;/usr/local/n&lt;/code&gt; (and associated files). But do it first before setting &lt;code&gt;N_PREFIX&lt;/code&gt;.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Cover image by &lt;a href="https://unsplash.com/@gundim"&gt;Dim Gunger&lt;/a&gt; on  &lt;a href="https://unsplash.com/photos/a-colorful-background-with-lines-and-curves-Am9Se-CMNvM"&gt;Unsplash&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;

</description>
    </item>
    <item>
      <title>Add Powerline glyphs to IBM Plex fonts</title>
      <dc:creator>Henry Wu</dc:creator>
      <pubDate>Fri, 01 Apr 2022 07:25:28 +0000</pubDate>
      <link>https://forem.com/hwupu/add-powerline-glyphs-to-ibm-plex-fonts-1a80</link>
      <guid>https://forem.com/hwupu/add-powerline-glyphs-to-ibm-plex-fonts-1a80</guid>
      <description>&lt;p&gt;&lt;a href="https://www.ibm.com/plex/"&gt;IBM Plex&lt;/a&gt; is an interesting font that I'm looking forward to, and I would like to try it out. However, you may be in similar setup as I am, which relays on &lt;a href="https://github.com/powerline/powerline"&gt;Powerline&lt;/a&gt; glyphs in order to display vim/statusline/prompt correctly.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--t4A0j3sf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f1adeig27xvee9kh9s3d.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--t4A0j3sf--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/f1adeig27xvee9kh9s3d.png" alt="Terminal prompt with missing glyphs" width="880" height="105"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;This walkthrough was performed on macOS and has patched IBM Plex OpenType fonts, but you should be able to patch your own fonts (any fonts) on any OS, as long as you have Python installed.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Download IBM Plex:&lt;br&gt;
&lt;a href="https://github.com/IBM/plex/releases"&gt;https://github.com/IBM/plex/releases&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Install fontforge:&lt;br&gt;
&lt;code&gt;brew install fontforge&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Clone the font patch script from &lt;a href="https://github.com/sgolovine/nerdfont-patcher"&gt;sgolovine&lt;/a&gt; on GitHub:&lt;br&gt;
&lt;code&gt;git clone https://github.com/sgolovine/nerdfont-patcher&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Navigate to the directory:&lt;br&gt;
&lt;code&gt;cd nerdfont-patcher&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Add Powerline glyphs to IBM Plex font:&lt;br&gt;
&lt;code&gt;fontforge -script font-patcher --powerline ../OpenType/IBM-Plex-Mono/IBMPlexMono-Regular.otf&lt;/code&gt;&lt;br&gt;
(You may need to change the &lt;em&gt;path-to-file&lt;/em&gt;, more info please refer to the &lt;a href="https://github.com/sgolovine/nerdfont-patcher"&gt;README of nerdfont-patcher&lt;/a&gt;)&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--Xl4kQW4m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hl7y1yxldma5yo8r8zx.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--Xl4kQW4m--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_880/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3hl7y1yxldma5yo8r8zx.png" alt="Terminal prompt with Powerline glyphs" width="880" height="149"&gt;&lt;/a&gt;&lt;/p&gt;

</description>
    </item>
  </channel>
</rss>
