<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.3.3">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2024-09-16T05:49:03+00:00</updated><id>/feed.xml</id><entry><title type="html">A Ruby Script to Deploy Rails Docker App to Hetzner</title><link href="/2024/09/14/docker-rails-deploy-script.html" rel="alternate" type="text/html" title="A Ruby Script to Deploy Rails Docker App to Hetzner" /><published>2024-09-14T00:00:00+00:00</published><updated>2024-09-14T00:00:00+00:00</updated><id>/2024/09/14/docker-rails-deploy-script</id><content type="html" xml:base="/2024/09/14/docker-rails-deploy-script.html"><![CDATA[<h2 class="display-6 fw-bold">
    A Ruby Script to Deploy Rails Docker App to Hetzner
</h2>

<p>
    When new code is committed, I need to build a new Docker image and repeat the process of:
    <ol>
        <li>docker save and ssh docker load it in the server</li>
        <li>Stop old Rails app Docker container</li>
        <li>Start new Rails app Docker container</li>
        <li>Remove old Rails app Docker image</li>
        <li>Tag new Rails app Docker image</li>
    </ol>
</p>
<h3 class="fw-bold">
    The Problem
</h3>

<p>
    This whole process is kind of boring, I want to automate it. Like we used to have "kamal deploy", and "git push dokku main". 
</p>

<p>
    Best hope is I can just run "ruby deploy.rb" and go get a cup of tea, when I come back the new code base is live under my domain name!
</p>

<h3 class="fw-bold">
    A Ruby Script for Automated Deploy
</h3>

<p>
    Once I've set up the server and deployed the first image manually, I wrote a Ruby script to automate the consecutive deploy process.
</p>

<p>
    First I need to set some project-specific variables, such as Environment variables, the app name, the docker network or container names.
</p>


<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="nb">require</span> <span class="s1">'colorize'</span>

<span class="n">app_name</span> <span class="o">=</span> <span class="s2">"my-app"</span> <span class="c1"># specify your app name</span>
<span class="n">rails_master_key</span> <span class="o">=</span> <span class="no">File</span><span class="p">.</span><span class="nf">read</span><span class="p">(</span><span class="s1">'config/master.key'</span><span class="p">)</span>
<span class="n">db_password</span> <span class="o">=</span> <span class="s2">"dumbpass"</span> <span class="c1"># same as postgresql container's setting</span>
<span class="n">db_host_or_ip</span> <span class="o">=</span> <span class="s2">"app-pg"</span> <span class="c1"># the container name of pg db, to let Rails database.yml know where to connect</span>
<span class="n">docker_subnet</span> <span class="o">=</span> <span class="s2">"app-net"</span> <span class="c1"># The rails, db, frp, redis Docker internal network, can be "bridge" or "host" or "anything", need to create in docker with docker network create</span>
<span class="n">server_user</span> <span class="o">=</span> <span class="s2">"root"</span> <span class="c1"># the user of the remote server (pi, or an Hetzner vps, etc)</span>
<span class="n">server_ip</span> <span class="o">=</span> <span class="s2">"docker-app-ip"</span> <span class="c1"># the IP of the server</span>
<span class="n">use_bzip</span> <span class="o">=</span> <span class="kp">false</span> <span class="c1"># whether to compress docker image when transmitting it to server, if server can't decompress fast or net traffic is free, don't zip. If server traffic is slow, zip it.</span></code></pre></figure>

<p>
    And then I wrote two proc to run commands in my local development machine or remote server (VPS machine). I also record time elapsed when running each task, so I know what can cause error and how long is used everytime.
</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="n">deploy_start_at</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span> <span class="c1"># To count how many seconds is used for the whole deploy process</span>

<span class="c1"># Run a shell command on remote machine</span>
<span class="n">run_remote</span> <span class="o">=</span> <span class="no">Proc</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span>
  <span class="n">start_at</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"Start CMD: ssh </span><span class="si">#{</span><span class="n">server_user</span><span class="si">}</span><span class="s2">@</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2"> </span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="s2"> 2&gt;&amp;1"</span><span class="p">.</span><span class="nf">colorize</span><span class="p">(</span><span class="ss">:blue</span><span class="p">)</span>  <span class="c1"># 2&gt;&amp;1 will show the stderr in return value</span>
  <span class="n">out</span> <span class="o">=</span> <span class="sb">`ssh </span><span class="si">#{</span><span class="n">server_user</span><span class="si">}</span><span class="sb">@</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="sb"> </span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="sb"> 2&gt;&amp;1`</span>
  <span class="n">finish_at</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"Finished CMD: </span><span class="si">#{</span><span class="p">(</span><span class="n">finish_at</span> <span class="o">-</span> <span class="n">start_at</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="si">}</span><span class="s2"> seconds.</span><span class="se">\n\n</span><span class="s2">"</span><span class="p">.</span><span class="nf">colorize</span><span class="p">(</span><span class="ss">:green</span><span class="p">)</span>
  <span class="n">out</span>
<span class="k">end</span>

<span class="c1"># Run a shell command on local machine</span>
<span class="n">run_local</span> <span class="o">=</span> <span class="no">Proc</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">cmd</span><span class="o">|</span> 
  <span class="n">start_at</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"Start CMD: </span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="s2"> 2&gt;&amp;1"</span><span class="p">.</span><span class="nf">colorize</span><span class="p">(</span><span class="ss">:blue</span><span class="p">)</span>
  <span class="n">out</span> <span class="o">=</span> <span class="sb">`</span><span class="si">#{</span><span class="n">cmd</span><span class="si">}</span><span class="sb"> 2&gt;&amp;1`</span>
  <span class="n">finish_at</span> <span class="o">=</span> <span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span>
  <span class="nb">puts</span> <span class="s2">"Finished CMD: </span><span class="si">#{</span><span class="p">(</span><span class="n">finish_at</span> <span class="o">-</span> <span class="n">start_at</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="si">}</span><span class="s2"> seconds.</span><span class="se">\n\n</span><span class="s2">"</span><span class="p">.</span><span class="nf">colorize</span><span class="p">(</span><span class="ss">:green</span><span class="p">)</span>
  <span class="n">out</span>
<span class="k">end</span></code></pre></figure>

<p>
    Then I wrote a method for Health Check. This is learned from Kamal, but I didn't look into Kamal's implementation, I just use a cURL docker image running in the same Docker subnetwork to "ping" the "/up" route.
</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Healthcheck a Rails app is running successfully by inspect the container status and then GET /up.</span>
<span class="n">test_app_running</span> <span class="o">=</span> <span class="no">Proc</span><span class="p">.</span><span class="nf">new</span> <span class="k">do</span> <span class="o">|</span><span class="n">container_name</span><span class="o">|</span>
  <span class="mi">10</span><span class="p">.</span><span class="nf">times</span> <span class="k">do</span> <span class="o">|</span><span class="n">i</span><span class="o">|</span>
    <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Healthcheck new container status - Round: </span><span class="si">#{</span><span class="n">i</span><span class="si">}</span><span class="s2">"</span>
    <span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker inspect --format </span><span class="se">\"\"</span><span class="s2"> </span><span class="si">#{</span><span class="n">container_name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">out</span><span class="p">.</span><span class="nf">include?</span> <span class="s2">"true"</span>
      <span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker run --rm --network </span><span class="si">#{</span><span class="n">docker_subnet</span><span class="si">}</span><span class="s2"> curlimages/curl --silent -LI -o /dev/null </span><span class="si">#{</span><span class="n">container_name</span><span class="si">}</span><span class="s2">:3000/up -w '%{http_code}</span><span class="se">\n</span><span class="s2">'"</span><span class="p">)</span>
      <span class="k">if</span> <span class="n">out</span><span class="p">.</span><span class="nf">include?</span> <span class="s2">"200"</span>
        <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Container is ready."</span>
        <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Success!"</span>
        <span class="k">break</span>
      <span class="k">else</span>
        <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Container is up but Rails server is not ready, waiting 3 seconds..."</span>
        <span class="nb">sleep</span><span class="p">(</span><span class="mi">3</span><span class="p">)</span>
      <span class="k">end</span>
    <span class="k">elsif</span> <span class="n">i</span> <span class="o">&lt;</span> <span class="mi">9</span>
      <span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Container is not ready, waiting 1 second..."</span>
      <span class="nb">sleep</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
    <span class="k">else</span>
      <span class="k">raise</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Container is not ready after waits. Aborting. Manual fix is required."</span>
    <span class="k">end</span>
  <span class="k">end</span>
<span class="k">end</span></code></pre></figure>

<p>
    Those are the methods to be invoked, then I need to run some commands on my mac and on the Hetzner VPS.
</p>
<h3 class="fw-bold">
    The Deploy Process
</h3>

<p>
    Phase 1 is building the Docker image on my development machine, it uses the app specific variables I defined before. And if successful it would use Docker load and ssh to load the image on the Hetzner server with a new Docker image tag "new_build".
</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Phase 1: Build Docker Image for Rails App</span>
<span class="nb">puts</span> <span class="s2">"local: Building Docker image for </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">."</span>
<span class="n">build_image_cmd</span> <span class="o">=</span> <span class="s2">"docker build -t </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:new_build ."</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_local</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="n">build_image_cmd</span><span class="p">)</span>
<span class="k">if</span> <span class="n">out</span><span class="p">.</span><span class="nf">include?</span> <span class="s2">"DONE"</span>
  <span class="nb">puts</span> <span class="s2">"local: Docker Build Successful."</span>
<span class="k">elsif</span> <span class="n">out</span><span class="p">.</span><span class="nf">include?</span> <span class="s2">"ERROR"</span>
  <span class="nb">puts</span> <span class="n">out</span>
  <span class="k">raise</span> <span class="s2">"local: Docker Build Error."</span>
<span class="k">else</span>
  <span class="k">raise</span> <span class="s2">"local: Docker build process status unknown, check manually."</span>
<span class="k">end</span>

<span class="nb">puts</span> <span class="s2">"local+remote: Compressing Image and Loading it in the Remote Server"</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_local</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker save </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:new_build </span><span class="si">#{</span><span class="n">use_bzip</span> <span class="p">?</span> <span class="s2">"| bzip2 |"</span> <span class="p">:</span> <span class="s2">"|"</span><span class="si">}</span><span class="s2"> ssh </span><span class="si">#{</span><span class="n">server_user</span><span class="si">}</span><span class="s2">@</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2"> docker load"</span><span class="p">)</span></code></pre></figure>

<p>
    Phase 2 is testing if the new image can run on the server. At this moment, the old and new rails app containers will run together.
</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Phase 2: Test the Docker image of Rails app on remote server. If successful, stop it and run it later.</span>
<span class="c1"># Because port mapping of docker and container name can't have conflict, but FRP uses a static web app container name in frps.ini, can't change it dynamically.</span>
<span class="c1"># So, have to stop and rerun the new app container.</span>
<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Starting Rails app with new built image"</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker run --name </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app-new-build -e RAILS_MASTER_KEY=</span><span class="si">#{</span><span class="n">rails_master_key</span><span class="si">}</span><span class="s2"> -e POSTGRES_PASSWORD=</span><span class="si">#{</span><span class="n">db_password</span><span class="si">}</span><span class="s2"> -e DB_HOST=</span><span class="si">#{</span><span class="n">db_host_or_ip</span><span class="si">}</span><span class="s2"> --network </span><span class="si">#{</span><span class="n">docker_subnet</span><span class="si">}</span><span class="s2"> -d </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:new_build"</span><span class="p">)</span>
<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: </span><span class="si">#{</span><span class="n">out</span><span class="si">}</span><span class="s2">"</span>

<span class="nb">puts</span> <span class="s2">"Test if app can start"</span>
<span class="n">test_app_running</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app-new-build"</span><span class="p">)</span>

<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Stopping new container, will restart it later after stopping the old one."</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker container stop </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app-new-build"</span><span class="p">)</span>

<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Removing new build container"</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker container rm </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app-new-build"</span><span class="p">)</span></code></pre></figure>

<p>
    Since I can't find an easy way to just change the reverse proxy's port to the new container, I have to stop both old and new containers, and start a new container again. This will cause a few seconds' downtime. Haven't found a good solution to that yet. Kamal does this with Traefik, but I don't use Traefik so I didn't look into their implementation. And BTW in Kamal 2 I heard they won't be using Traefik either, so I didn't take time to look into it. This script will work for me because I don't need to enforce 0-downtime deploy.
</p>

<p>
    Phase 3 will start new container and do some cleanup, such as removing the old image and container, re-tag the new image from "new_build" to "latest".
</p>
<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Phase 3: Stop old container, start new container for the Rails app</span>
<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Stopping old Rails container"</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker container stop </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app"</span><span class="p">)</span>

<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Removing old Rails container"</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker container rm </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app"</span><span class="p">)</span>

<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Removing old Rails Image"</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker image rm </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>

<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Renaming new rails image tag from 'new_build' to 'latest'"</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker image tag </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:new_build </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:latest"</span><span class="p">)</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker image rm </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:new_build"</span><span class="p">)</span>

<span class="nb">puts</span> <span class="s2">"</span><span class="si">#{</span><span class="n">server_ip</span><span class="si">}</span><span class="s2">: Starting Rails app..."</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_remote</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker run --name </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app -e RAILS_MASTER_KEY=</span><span class="si">#{</span><span class="n">rails_master_key</span><span class="si">}</span><span class="s2"> -e POSTGRES_PASSWORD=</span><span class="si">#{</span><span class="n">db_password</span><span class="si">}</span><span class="s2"> -e DB_HOST=</span><span class="si">#{</span><span class="n">db_host_or_ip</span><span class="si">}</span><span class="s2"> --network </span><span class="si">#{</span><span class="n">docker_subnet</span><span class="si">}</span><span class="s2"> -d </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span></code></pre></figure>

<p>
    The last phase is testing the new rails app Docker container is working correctly. Once it's done I can manually run some db migration through docker exec -it <my-app> /bin/bash. 
</p>
<p>
    And if I'm confident, I can also remove old image and re-tag the image on my development machine from "new_build" to "latest". But that can cause the danger of new container not working properly but I can't rollback to old version quickly. So whether or not to remove old container depends on how critical uptime means for the project.
</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Phase 4: Test new container is ready.</span>
<span class="nb">puts</span> <span class="s2">"Test if app is running"</span>
<span class="n">test_app_running</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"</span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">-app"</span><span class="p">)</span>

<span class="nb">puts</span> <span class="s2">"Deployment finished in </span><span class="si">#{</span><span class="p">(</span><span class="no">Process</span><span class="p">.</span><span class="nf">clock_gettime</span><span class="p">(</span><span class="no">Process</span><span class="o">::</span><span class="no">CLOCK_MONOTONIC</span><span class="p">)</span> <span class="o">-</span> <span class="n">deploy_start_at</span><span class="p">).</span><span class="nf">round</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span><span class="si">}</span><span class="s2"> seconds."</span>

<span class="c1"># Optional: Clean up local docker images</span>
<span class="nb">puts</span> <span class="s2">"local: Retag new build Docker image to latest. Remove old image file."</span>
<span class="n">old_image_id</span> <span class="o">=</span> <span class="n">run_local</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker images -q </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:latest"</span><span class="p">).</span><span class="nf">strip</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_local</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker image tag </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:new_build </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:latest"</span><span class="p">)</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_local</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker image rm </span><span class="si">#{</span><span class="n">old_image_id</span><span class="si">}</span><span class="s2">"</span><span class="p">)</span>
<span class="n">out</span> <span class="o">=</span> <span class="n">run_local</span><span class="p">.</span><span class="nf">call</span><span class="p">(</span><span class="s2">"docker image rm </span><span class="si">#{</span><span class="n">app_name</span><span class="si">}</span><span class="s2">:new_build"</span><span class="p">)</span></code></pre></figure>

<p>
    And that's it! With this deploy script, the Docker build process takes around 30-60 seconds if no significant changes are done to the codebase. And the whole process can be finished within 2 minutes, including transfering a 180MB (after bzip compress and uncompress) to the server at 5MB/s. 
</p>

<h3 class="fw-bold">
    Cheers!
</h3>]]></content><author><name></name></author><summary type="html"><![CDATA[A Ruby Script to Deploy Rails Docker App to Hetzner]]></summary></entry><entry><title type="html">Move Active Storage From Cloud to Local, and then to Another Cloud</title><link href="/2024/09/12/migrate-active-storage-files.html" rel="alternate" type="text/html" title="Move Active Storage From Cloud to Local, and then to Another Cloud" /><published>2024-09-12T00:00:00+00:00</published><updated>2024-09-12T00:00:00+00:00</updated><id>/2024/09/12/migrate-active-storage-files</id><content type="html" xml:base="/2024/09/12/migrate-active-storage-files.html"><![CDATA[<h2 class="display-6 fw-bold">
    Move Active Storage From Cloud to Local, and then to Another Cloud
</h2>

<p>
	(First thing, this will cause downtime, otherwise it can't guarantee consistency between different S3 services.)
</p>

<h3 class="fw-bold">
    The Problem
</h3>

<p>
	Problem is, I run an in-house web app for our company in China. Our sales people don't have very good Internet connection to the Internet outside of China, so I can't use AWS or Cloudflare, I can only use S3-like services provided by local companies, let's call it Company T.
</p>

<p>
	Our app uses S3 service from the company T (let's call it S3-T), but the new server in Hetzner cannot fetch the objects from S3-T because there are many random loss packets, when obtaining files from CN in DE.
</p>

<p>
	But luckily I found another company A who offers S3 like service from Japan (let's call it S3-A), it's not slow when I visit it from China, and it's also stable when visited from Hetzner.
</p>

<p>
	So I decided to move the files from S3-T to S3-A.
</p>

<p>
	But before proceeding, I want to test if downloading files from S3-T and putting them to my "/storage" folder works on my local Rails environment. It can be beneficial for both testing purposes and self-hosting purposes.
</p>

<p>
	Like DHH said: "Moving off the cloud!" Woo hoo!
</p>

<h3 class="fw-bold">
    Move From Cloud S3 to Local Storage
</h3>

<p>
	This is the first time I tinker with active storage's files. When developing I notice Rails stores files in the "/storage" folder. Inside the "/storage" folder are many two letter named subfolders, like: "hz", "va", "tc", etc. I know that's some kind of hash converted into folder and file names so Rails can easily find what it needs. 
</p>

<p>
	But when I look at the files in the Company T's web dashboard, there are no sub folders, only pure files with hash-like filenames. Like "zwjhu5t9d0a62w2mpdlzn3tnehki", "zfhssey9spjsg0z1g9ild3qfnz1k".
</p>

<p>
	That's weird, is it because company T uses a different structure from real AWS S3? Not likely, because I only have the "aws-s3-sdk" gem, but not "t-s3-sdk" gem, so S3-T must speak the same language as AWS S3.
</p>

<p>
	I found this guy on Github has a gist: https://gist.github.com/LucasKuhn/f436c4e94991bc44c43b2669ae0057fe. It just renames the files downloaded from AWS S3 and also generates intermediate sub folders, so folder structure will match what Rails' :local should look like.
</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1">#script.rb </span>

<span class="n">storage_folder</span> <span class="o">=</span> <span class="no">Rails</span><span class="p">.</span><span class="nf">root</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="s1">'storage'</span><span class="p">)</span> 
<span class="n">files</span> <span class="o">=</span> <span class="n">storage_folder</span><span class="p">.</span><span class="nf">children</span><span class="p">.</span><span class="nf">select</span> <span class="p">{</span> <span class="o">|</span><span class="n">file</span><span class="o">|</span> <span class="n">file</span><span class="p">.</span><span class="nf">file?</span> <span class="o">&amp;&amp;</span> <span class="o">!</span><span class="n">file</span><span class="p">.</span><span class="nf">empty?</span> <span class="p">}</span>

<span class="n">files</span><span class="p">.</span><span class="nf">each</span> <span class="k">do</span> <span class="o">|</span><span class="n">path_name</span><span class="o">|</span>
	<span class="n">dir</span><span class="p">,</span> <span class="n">basename</span> <span class="o">=</span> <span class="n">path_name</span><span class="p">.</span><span class="nf">split</span>
	<span class="n">file_name</span> <span class="o">=</span> <span class="n">basename</span><span class="p">.</span><span class="nf">to_s</span>
	<span class="n">sub_folders</span> <span class="o">=</span> <span class="n">dir</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">file_name</span><span class="p">[</span><span class="mi">0</span><span class="o">..</span><span class="mi">1</span><span class="p">],</span> <span class="n">file_name</span><span class="p">[</span><span class="mi">2</span><span class="o">..</span><span class="mi">3</span><span class="p">])</span>
	<span class="n">sub_folders</span><span class="p">.</span><span class="nf">mkpath</span> <span class="c1"># Create the subfolder used by active_record</span>
	<span class="n">path_name</span><span class="p">.</span><span class="nf">rename</span><span class="p">(</span><span class="n">dir</span> <span class="o">+</span> <span class="n">sub_folders</span> <span class="o">+</span> <span class="n">basename</span><span class="p">)</span> <span class="c1"># Renames file to be moved into subfolder</span>
<span class="k">end</span></code></pre></figure>

<p>
	Looks promising. Having some confidence, I started to download the files from S3-T to my local machine to have a test of this script.
</p>

<p>
	To download all the files from S3-T to my local machine, Company T provides a tool called "coscli". AWS has a similar program called "awscli" I can brew install. But they don't work interchangeably. So I downloaded "coscli", set some keys like the api secret_key and secret_id. And then it's just a single simple command:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>./coscli <span class="nb">cp</span> <span class="nt">-r</span> cos://bucket-images-66666 ./backup/

<span class="c"># Succeed: Total num: 1223, size: 404,661,084 Byte (385.91 MB). OK num: 1223(download 1223 objects). </span></code></pre></figure>

<p>
	Now I've got all the images of our in-house Rails app on my local machine.
</p>

<p>
	Then I backup my development database, development storage folder. Change my storage.yml, and started to test this downloaded folder with the script above. 
</p>


<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>rails console

<span class="c"># run the script above...</span>

<span class="nv">$ </span>bin/dev</code></pre></figure>

<p>
	Refresh the page in browser...
</p>

<p>
	Nice, the images are displaying!
</p>

<p>
	That's very easy! To move active storage files from cloud to local storage, just: 
	<ol>
		<li>Modify storage.yml.</li>
		<li>Download the files from S3-T.</li>
		<li>Rename them with the script to generate intermediate sub folders.</li>
	</ol>
</p>

<h3 class="fw-bold">
    Move From Local Storage to Another Cloud S3 Service
</h3>

<p>
	Now that I've tested, whenever I wish, I can just host the active storage on my local server. The next step is to move from local to another cloud S3 storage service. Because for a 12-factor-app, I wouldn't want to copy the files every time I migrate an app. So using an S3 service is very beneficial.
</p>

<p>
	I've got around 1200 files to upload, but they're not very large, just ~380MB in total. I uploaded them from S3-A provider's web dashboard in the browser. 
</p>

<p>
	This step is even easier than the last, just upload the files to a bucket, modify storage.yml again and that's it.
</p>

<p>
	Verify it on my development machine, I see the URL of my image assets have changed from "https://s3-t...jpg" to "https://s3-a...jpg" in the browser developer console, and the images are displaying correctly. I tested uploading and deleting files, after correctly setting CORS rules, it also works perfectly. I can also verify in the S3-A's dashboard that there are new images added after I upload in my Rails app.
</p>

<p>
	Perfect.
</p>

<h3 class="fw-bold">
    Rename Storage.yml Service Name
</h3>

<p>
	Since I migrated active storage S3 service from S3-T to S3-A, I want to change my storage.yml's service provider name.
</p>

<figure class="highlight"><pre><code class="language-yaml" data-lang="yaml"><span class="na">S3-T</span><span class="pi">:</span>
  <span class="na">service</span><span class="pi">:</span> <span class="s">S3</span>
  <span class="na">access_key_id</span><span class="pi">:</span> <span class="s">&lt;%= Rails.application.credentials.dig(:s3t, :access_key_id) %&gt;</span>
  <span class="na">secret_access_key</span><span class="pi">:</span> <span class="s">&lt;%= Rails.application.credentials.dig(:s3t, :secret_access_key) %&gt;</span>

<span class="c1"># To</span>

<span class="na">S3-A</span><span class="pi">:</span>
  <span class="na">service</span><span class="pi">:</span> <span class="s">S3</span>
  <span class="na">access_key_id</span><span class="pi">:</span> <span class="s">&lt;%= Rails.application.credentials.dig(:s3a, :access_key_id) %&gt;</span>
  <span class="na">secret_access_key</span><span class="pi">:</span> <span class="s">&lt;%= Rails.application.credentials.dig(:s3a, :secret_access_key) %&gt;</span></code></pre></figure>

<p>
	Now I want that S3-T to become S3-A, it avoids the future me of confusing which S3 service I'm using for this app. But Rails active_storage_blobs table has a column "service_name", their values are now "S3-T". I can't just rename here (storage.yml), I've got to update the column too.
</p>


<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="c1"># Can use a migration as well.</span>

<span class="c1"># But console is more convenient!</span>

<span class="no">ActiveStorage</span><span class="o">::</span><span class="no">Blob</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">service_name: </span><span class="s1">'S3-T'</span><span class="p">).</span><span class="nf">update_all</span><span class="p">(</span><span class="ss">service_name: </span><span class="s1">'S3-A'</span><span class="p">)</span>	</code></pre></figure>

<p>
	If you happen to want to use :local Disk service to serve them from "/storage" folder, like I did in the <i>Move From Cloud S3 to Local Storage</i> section, you can:
</p>

<figure class="highlight"><pre><code class="language-ruby" data-lang="ruby"><span class="no">ActiveStorage</span><span class="o">::</span><span class="no">Blob</span><span class="p">.</span><span class="nf">where</span><span class="p">(</span><span class="ss">service_name: </span><span class="s1">'S3-T'</span><span class="p">).</span><span class="nf">update_all</span><span class="p">(</span><span class="ss">service_name: </span><span class="s1">'local'</span><span class="p">)</span>	</code></pre></figure>

<p>
	Done!
</p>

<p>
	Now our in-house sales people can upload files like they used to, and my server in Hetzner running on ARM CPUs can also fetch the images for resizing etc. I think I got the best of both worlds, after 3 days of testing and thinking.
</p>

<p>
	(BTW, Docker images built from M1 Mac can run directly on Raspberry Pi and Hetzner ARM machines!)
</p>

<h3 class="fw-bold">
    Cheers!
</h3>]]></content><author><name></name></author><summary type="html"><![CDATA[Move Active Storage From Cloud to Local, and then to Another Cloud]]></summary></entry><entry><title type="html">Enable HTTPS for Rails App with acme.sh and Letsencrypt</title><link href="/2024/09/08/enable-https-with-acme-sh-for-rails.html" rel="alternate" type="text/html" title="Enable HTTPS for Rails App with acme.sh and Letsencrypt" /><published>2024-09-08T00:00:00+00:00</published><updated>2024-09-08T00:00:00+00:00</updated><id>/2024/09/08/enable-https-with-acme-sh-for-rails</id><content type="html" xml:base="/2024/09/08/enable-https-with-acme-sh-for-rails.html"><![CDATA[<h2 class="display-6 fw-bold">
    Enable HTTPS for Rails App with acme.sh and Letsencrypt
</h2>


<h3 class="fw-bold">
    The Problem
</h3>

<p>
	I've got my Rails app successfully running on my Raspberry Pi, I can also expose it to the public (Internet) with an FRP tunnel (something like ngrok or zerotier but is self hosted), but it doesn't support HTTPS, now I need an HTTPS cert to enable HTTPS for the Rails app.
</p>

<p>
	I know Kamal and Dokku does this for me automatically, I can also enable "Proxy" mode in Cloudflare in DNS, such that CF issues the cert for me, or I can also set up a Cloudflare argo tunnel. But sometimes due to the network condition I don't have those options. So, I need to get a cert with acme.sh for my web app.
</p>

<h3 class="fw-bold">
    Start acme.sh in Docker
</h3>

<p>
	There is a docker image: neilpang/acme.sh, I can run it on my Pi server. But before that, I need to do two things first. 
</p>

<p>
	First thing is, I need to create a folder on the pi and share it with the acme docker container. Because the acme.sh will run in a Docker container, if I want to use the certs issued by it, I need to get it out of the container. One good approach is to use a Docker mount volume, so I create some folders on my server first.
</p>


<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">cd</span> ~
<span class="nv">$ </span><span class="nb">mkdir </span>acme_data</code></pre></figure>

<p>
	And then after the cert is issued, I need to install the cert to another folder, this is not required, but acme.sh's folder structure may change, so I create another folder to store the certs.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">cd </span>acme_data
<span class="nv">$ </span><span class="nb">mkdir </span>installs</code></pre></figure>


<p>
	This ~/acme_data/installs folder will be in the acme.sh container later.
</p>

<p>
	Second thing is, because I'm in a local network, I don't have an IPv4 address, so I need to use DNS challenge to ask letsencrypt to issue a cert for me. Acme.sh will need to change my DNS. So I need to tell it how to do it with my DNS provider.
</p>

<p>
	To set this for my DNS provider (Cloudflare), I need to go to Cloudflare to generate an API token. This token should have the permission to manage my DNS record (but not others!). Such that acme.sh can use this token to complete its challenge to prove I own the domain name.
</p>

<p>
	Beware that acme.sh allows two Cloudflare token/key, the "CF_Token" and "CF_Key", the "CF_Token" is what I need here. The "CF_Key" seems to be the key(or another kind of token) that can manage all my Cloudflare account settings, sounds like a dangerous thing, I won't use the "CF_Key". So I can just use the "CF_Token".
</p>


<p>
	Then start the acme docker container, with these two Docker ENV (CF_Token and CF_Email), CF_Email is my Cloudflare account's email.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker run <span class="nt">-it</span> <span class="nt">-d</span> <span class="nt">--name</span> acme <span class="nt">--restart</span><span class="o">=</span>always <span class="nt">--net</span> host <span class="nt">-v</span> /home/pi/acme_data:/acme.sh <span class="nt">-e</span> <span class="nv">CF_Token</span><span class="o">=</span>&lt;MY_CFToken_From_The_CF_Dashboard&gt; <span class="nt">-e</span> <span class="nv">CF_Email</span><span class="o">=</span>&lt;MY_CF_Email&gt; neilpang/acme.sh daemon</code></pre></figure>


<p>
	After that, register an account for letsencrypt to contact me with acme.sh:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker <span class="nb">exec </span>acme <span class="nt">--set-default-ca</span> <span class="nt">--server</span> letsencrypt
docker <span class="nb">exec </span>acme <span class="nt">--register-account</span> <span class="nt">-m</span> myname@myemail</code></pre></figure>

<p>
	And now I can ask for a HTTPS cert, I use DNS challenge here, it will give me an wildcard domain cert:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker <span class="nb">exec </span>acme <span class="nt">--issue</span> <span class="nt">--dns</span> dns_cf <span class="nt">-d</span> printfinn.com <span class="nt">-d</span> <span class="k">*</span>.printfinn.com</code></pre></figure>

<p>
	If the process is successful, I am able to check the certs are already stored in my folder:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">ls </span>acme_data/printfinn.com_ecc/

backup  fullchain.cer      printfinn.com.conf  printfinn.com.csr.conf
ca.cer  printfinn.com.cer  printfinn.com.csr   printfinn.com.key</code></pre></figure>

<p>
	Then as said before, install the cert to another folder, maybe I use nginx, or apache, or just want to store it in some folder for future use, like for an FRP tunnel. 
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># you may use .crt or .cer as you need</span>
docker <span class="nb">exec </span>acme <span class="nt">--install-cert</span> <span class="nt">-d</span> printfinn.com <span class="nt">--cert-file</span> /acme.sh/installs/printfinn.com_cert.crt <span class="nt">--key-file</span> /acme.sh/installs/printfinn.com.key <span class="nt">--fullchain-file</span> /acme.sh/installs/printfinn.com.crt</code></pre></figure>

<p>
	It gives me a few certs, I need to use the fullchain cert later.
</p>

<p>
	The cert is valid for 90 days, and should usually renew at the 60th's day. To renew the cert automatically, use a cronjob:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># auto update acme.sh in the container</span>
docker <span class="nb">exec </span>acme <span class="nt">--upgrade</span> <span class="nt">--auto-upgrade</span>

docker <span class="nb">exec </span>acme <span class="nt">--cron</span></code></pre></figure>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>crontab <span class="nt">-e</span>
<span class="c"># In crontab, put this below:</span>
10 0 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> docker <span class="nb">exec </span>acme <span class="nt">--cron</span> <span class="o">&gt;</span> /dev/null</code></pre></figure>

<p>
	This will renew (if needed) the cert at 0:10 everyday midnight. Once it's renewed, it will be put into the "installs" folder I appointed before automatically. (Have some doubt about this, will check it after my first cert expires :) ).
</p>

<p>
	If I wish to restart nginx or apache or maybe frp, I can append the command after the cronjob:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>crontab <span class="nt">-e</span>
<span class="c"># In crontab, put this below:</span>
10 0 <span class="k">*</span> <span class="k">*</span> <span class="k">*</span> docker <span class="nb">exec </span>acme <span class="nt">--cron</span> <span class="o">&gt;</span> /dev/null <span class="o">&amp;&amp;</span> docker restart nginx-container-or-frp-container</code></pre></figure>

<p>
	Then I have an HTTPS cert now! And it auto renews!
</p>

<h3 class="fw-bold">
    Cheers!
</h3>

<p>
	(Note for myself, the command to start the frp server is:)
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker run <span class="se">\</span>
    <span class="nt">-d</span> <span class="se">\</span>
    <span class="nt">--restart</span><span class="o">=</span>always <span class="se">\</span>
    <span class="nt">--network</span><span class="o">=</span>rails_net <span class="se">\</span>
    <span class="nt">--pull</span><span class="o">=</span>always <span class="se">\</span>
    <span class="nt">--name</span><span class="o">=</span>frp-my-app <span class="se">\</span>
    <span class="nt">-v</span> /home/pi/acme_data/installs/printfinn.com.crt:/run/frpc/printfinn.com.crt:ro <span class="se">\</span>
    <span class="nt">-v</span> /home/pi/acme_data/installs/printfinn.com.key:/run/frpc/printfinn.com.key:ro <span class="se">\</span>
    natfrp.com/frpc <span class="se">\</span>
    <span class="nt">--disable_log_color</span> <span class="se">\</span>
    <span class="nt">-f</span> &lt;frp_config_params&gt;</code></pre></figure>]]></content><author><name></name></author><summary type="html"><![CDATA[Enable HTTPS for Rails App with acme.sh and Letsencrypt]]></summary></entry><entry><title type="html">How to Deploy a Rails app to Raspberry Pi with Docker in 2024</title><link href="/2024/09/07/deploy-rails-with-docker.html" rel="alternate" type="text/html" title="How to Deploy a Rails app to Raspberry Pi with Docker in 2024" /><published>2024-09-07T00:00:00+00:00</published><updated>2024-09-07T00:00:00+00:00</updated><id>/2024/09/07/deploy-rails-with-docker</id><content type="html" xml:base="/2024/09/07/deploy-rails-with-docker.html"><![CDATA[<h2 class="display-6 fw-bold">
    Deploy a Rails app to Raspberry Pi with Docker in 2024
</h2>


<h3 class="fw-bold">
    The Problem
</h3>

<p>
	I want to deploy a Rails app in production mode to one of my spare Raspberry Pi, or any other Linux server such as a VPS from Hetzner or Digital Ocean or AWS. What is the minimum configuration?
</p>

<p>
	We now have Kamal, we have Dokku, these are very good tools, but sometimes I don't have open network, maybe I'm in a local network without Internet, can I just "docker build" from my Mac, scp my docker image to my server, then load and run it on my server?
</p>

<p>
	Let's try it!
</p>

<h3 class="fw-bold">
    The Reasoning
</h3>

<p>
	So, after a "rails new my-app", which ships with a default Dockerfile since Rails7, I should run
</p>


<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker run my-app</code></pre></figure>

<p>
	It's the thing I know. The my-app image is the Rails app I want to build with "docker build", how should I build it? I can build it on my local development machine with:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">cd </span>my-app
<span class="nv">$ </span>docker build <span class="nt">-t</span> my-app .</code></pre></figure>

<p>
	And that's it. I now have a image, I checked with:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker image <span class="nb">ls</span></code></pre></figure>

<p>
	It gives me:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">REPOSITORY      TAG               IMAGE ID       CREATED          SIZE
my-app          latest            548628d797db   43 minutes ago   554MB</code></pre></figure>

<p>
	Then I want to transfer this image to my Raspberry Pi, the server I want to run the app on, I can:
</p>


<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker save my-app | bzip2 | ssh pi@raspberrypi.local docker load</code></pre></figure>

<p>
	And now I have my docker image on my server! I can check that with:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># On the pi:</span>
<span class="nv">$ </span>docker image <span class="nb">ls</span></code></pre></figure>

<p>
	And it gives me the my-app image just the same size as it was on my development machine (my mac). So that's a progress, what's next?
</p>

<p>
	Simply run the command written in the Rails new Dockerfile template, and see how it goes:
</p>

<p>
	(5000:3000 means, the rails app runs itself on port 3000, docker will respond to port 5000 and forward the traffic to port 3000. In another word, I set Dockerfile to run 3000, but I visit http://raspberrypi.local:5000 to open the website.)
</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker run <span class="nt">-p</span> 5000:3000 <span class="nt">--name</span> my-app <span class="nt">-e</span> <span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;value from config/master.key&gt; my-app</code></pre></figure>

<p>
	It shows:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bin/rails aborted!
ActiveRecord::ConnectionNotEstablished: connection to server on socket <span class="s2">"/var/run/postgresql/.s.PGSQL.5432"</span> failed: No such file or directory <span class="o">(</span>ActiveRecord::ConnectionNotEstablished<span class="o">)</span></code></pre></figure>

<p>
	Well not so good, it complains cannot connect to postgres server. Of course! I haven't started my pg server!
</p>


<h3 class="fw-bold">
    Start Postgres Server
</h3>

<p>
	So I need to start a postgresql server, how? What is the easiest way? I went to docker hub and checked the README: https://hub.docker.com/_/postgres. Well for a postgres db I apparently need to set a user and a password, so I pass it in the Docker ENV:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">docker run <span class="nt">--name</span> my-app-pg <span class="nt">-e</span> <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>dumbpass <span class="nt">-e</span> <span class="nv">POSTGRES_USER</span><span class="o">=</span>my-app <span class="nt">-d</span> postgres:16 </code></pre></figure>

<p>
	The password of my pg database is "dumbpass", and I set the username to "my-app". Why "my-app"? Because it's the default pg username when rails new generates the app. I can find it in:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">Inside: <span class="s1">'config/database.yml'</span>

production:
  &lt;&lt;: <span class="k">*</span>default
  database: my-app_production
  username: my-app
  password: &lt;%<span class="o">=</span> ENV[<span class="s2">"MY-APP_DATABASE_PASSWORD"</span><span class="o">]</span> %&gt;</code></pre></figure>

<p>
	That's what Rails gives me by default. Good! Postgres database is now running on my server. But how to connect to this DB from my Rails app? I figure at least I need to pass the pg password to let Rails know it?
</p>

<p>
	Well I see there is a "password: <%= ENV["MY-APP_DATABASE_PASSWORD"] %>", if I pass this in the docker run command, Rails should be able to pick it up, right? Let me have a try:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker run <span class="nt">-p</span> 5000:3000 <span class="nt">--name</span> my-app <span class="nt">-e</span> <span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;value from config/master.key&gt; <span class="nt">-e</span> MY-APP_DATABASE_PASSWORD<span class="o">=</span>dumbpass my-app</code></pre></figure>


<h3 class="fw-bold">
    A Docker sub Network
</h3>

<p>
	This time rails should know both the pg username and password, it shouldn't complain anymore, but wait? It says connection error, not authentication error, I still have problem to fix before running the rails image.
</p>

<p>
	I figure I should create a virtual docker network to let the rails container and the postgres container find each other, so I ran:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker network create rails_net</code></pre></figure>

<p>
	And now I have a sub network, I can let these two containers run in the same network, and then try to let them find each other, maybe by IP (some 172.17.xxx thing) or, maybe by name? But first I need to re-run the postgres container to let it run inside this new virtual network:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker run <span class="nt">--name</span> my-app-pg <span class="nt">-e</span> <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>dumbpass <span class="nt">-e</span> <span class="nv">POSTGRES_USER</span><span class="o">=</span>my-app <span class="nt">--network</span> rails_net <span class="nt">-d</span> postgres:16 </code></pre></figure>

<p>
	Then I can start the rails app:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker run <span class="nt">-p</span> 5000:3000 <span class="nt">--name</span> my-app <span class="nt">-e</span> <span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;value from config/master.key&gt; <span class="nt">-e</span> MY-APP_DATABASE_PASSWORD<span class="o">=</span>dumbpass <span class="nt">--network</span> rails_net my-app</code></pre></figure>

<p>
	But it still gives the same error, Rails still can't find where the DB is running. Of course! Rails by default looks for localhost:5432, but the DB is in another container, it's still on port 5432 but not on Rails container's localhost. So I need to tell Rails where to find the DB. 
</p>

<p>
	Since we know we ran the postgres db container with a name: "my-app-pg", we can use this name like a computer in the "rails_net" network. The Rails container knows it, it can use the other container's name to find that container in the Docker network.
</p>

<p>
	So, we need to modify one rails file, to add a host param:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">Inside: <span class="s1">'config/database.yml'</span>

production:
  &lt;&lt;: <span class="k">*</span>default
  database: my-app_production
  username: my-app
  password: &lt;%<span class="o">=</span> ENV[<span class="s2">"MY-APP_DATABASE_PASSWORD"</span><span class="o">]</span> %&gt;
  host: &lt;%<span class="o">=</span> ENV[<span class="s2">"DB_HOST"</span><span class="o">]</span> %&gt;</code></pre></figure>

<p>
	I don't want to hardcode the "host" to some string, but to a ENV parameter, because I know I can make mistake and I don't want to build the docker image every time I change something in the rails app. So setting it to <%= ENV["DB_HOST"] %>, then pass a ENV in the docker run command makes things easier:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker run <span class="nt">-p</span> 5000:3000 <span class="nt">--name</span> my-app <span class="nt">-e</span> <span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;value from config/master.key&gt; <span class="nt">-e</span> MY-APP_DATABASE_PASSWORD<span class="o">=</span>dumbpass <span class="nt">-e</span> <span class="nv">DB_HOST</span><span class="o">=</span>my-app-pg <span class="nt">--network</span> rails_net my-app</code></pre></figure>

<p>
	In the last command, I specify DB_HOST=my-app-pg, this "my-app-pg" is just the name I ran the pg container with. Looks good to me, instead of immediate quit, it is now showing:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">Created database <span class="s1">'my-app_production'</span>
<span class="o">=&gt;</span> Booting Puma
<span class="o">=&gt;</span> Rails 7.2.1 application starting <span class="k">in </span>production 
<span class="o">=&gt;</span> Run <span class="sb">`</span>bin/rails server <span class="nt">--help</span><span class="sb">`</span> <span class="k">for </span>more startup options
Puma starting <span class="k">in </span>single mode...
<span class="k">*</span> Puma version: 6.4.2 <span class="o">(</span>ruby 3.2.2-p53<span class="o">)</span> <span class="o">(</span><span class="s2">"The Eagle of Durango"</span><span class="o">)</span>
<span class="k">*</span>  Min threads: 3
<span class="k">*</span>  Max threads: 3
<span class="k">*</span>  Environment: production
<span class="k">*</span>          PID: 1
<span class="k">*</span> Listening on http://0.0.0.0:3000
Use Ctrl-C to stop</code></pre></figure>

<p>
	But rails ships with force_ssl=true by default, whenever I visit a page, it goes to "https://..." but since I don't have a valid certificate(will be in another post), it can't display anything. I can easily fix it by changing:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># In config/environments/production.rb</span>
<span class="c"># Force all access to the app over SSL, use strict-Transport-Security, and use secure cookies.</span>
<span class="c"># config.force_ssl = true</span>
config.force_ssl <span class="o">=</span> <span class="nb">false</span></code></pre></figure>

<p>
	Now everything is working.
</p>

<p>
	But this doesn't cover mounting a volume for postgres db, which will cause database data to lose once the container is restarted, you should be able to figure it out easily :) A quick command:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker volume create pgdata

<span class="nv">$ </span>docker run <span class="nt">--name</span> my-app-pg <span class="nt">-e</span> <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>dumbpass <span class="nt">-e</span> <span class="nv">POSTGRES_USER</span><span class="o">=</span>my-app <span class="nt">-v</span> pgdata:/var/lib/postgresql/data <span class="nt">--network</span> rails_net <span class="nt">-d</span> postgres:16 </code></pre></figure>



<h3 class="fw-bold">
    Summary
</h3>

<p>
	In the end, I changed 2 files generated from rails new, they are:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># In config/database.yml</span>

production:
  &lt;&lt;: <span class="k">*</span>default
  database: my-app_production
  username: my-app
  password: &lt;%<span class="o">=</span> ENV[<span class="s2">"MY-APP_DATABASE_PASSWORD"</span><span class="o">]</span> %&gt;
  host: &lt;%<span class="o">=</span> ENV[<span class="s2">"DB_HOST"</span><span class="o">]</span> %&gt;</code></pre></figure>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="c"># In config/environments/production.rb</span>
<span class="c"># Force all access to the app over SSL, use strict-Transport-Security, and use secure cookies.</span>
<span class="c"># config.force_ssl = true</span>
config.force_ssl <span class="o">=</span> <span class="nb">false</span></code></pre></figure>

<p>
	One Docker build command to run on my development machine:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">cd </span>my-app
<span class="nv">$ </span>docker build <span class="nt">-t</span> my-app .</code></pre></figure>

<p>
	One command to transfer the docker image from my development machine to the server, you can also chain a "pv" to show the progress.
</p>
<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker save my-app | bzip2 | pv | ssh pi@raspberrypi.local docker load</code></pre></figure>

<p>
	One command to create a Docker network, one command to run a postgres db, one command to run the Rails app:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>docker network create rails_net

<span class="nv">$ </span>docker volume create pgdata

<span class="nv">$ </span>docker run <span class="nt">--name</span> my-app-pg <span class="nt">-e</span> <span class="nv">POSTGRES_PASSWORD</span><span class="o">=</span>dumbpass <span class="nt">-e</span> <span class="nv">POSTGRES_USER</span><span class="o">=</span>my-app <span class="nt">-v</span> pgdata:/var/lib/postgresql/data <span class="nt">--network</span> rails_net <span class="nt">-d</span> postgres:16 

<span class="nv">$ </span>docker run <span class="nt">-p</span> 5000:3000 <span class="nt">--name</span> my-app <span class="nt">-e</span> <span class="nv">RAILS_MASTER_KEY</span><span class="o">=</span>&lt;value from config/master.key&gt; <span class="nt">-e</span> MY-APP_DATABASE_PASSWORD<span class="o">=</span>dumbpass <span class="nt">-e</span> <span class="nv">DB_HOST</span><span class="o">=</span>my-app-pg <span class="nt">--network</span> rails_net <span class="nt">-d</span> my-app</code></pre></figure>

<p>
    This is the most simple way I can figure out to run a Rails app with Docker, maybe we can use Kamal and Dokku, but knowing the "manual" mode is very valuable as well!
</p>

<h3 class="fw-bold">
    Cheers!
</h3>]]></content><author><name></name></author><summary type="html"><![CDATA[Deploy a Rails app to Raspberry Pi with Docker in 2024]]></summary></entry><entry><title type="html">How to Control Chipsee PC Buzzer with Python and Browser</title><link href="/2023/04/13/chipsee-hmi-buzzer.html" rel="alternate" type="text/html" title="How to Control Chipsee PC Buzzer with Python and Browser" /><published>2023-04-13T00:00:00+00:00</published><updated>2023-04-13T00:00:00+00:00</updated><id>/2023/04/13/chipsee-hmi-buzzer</id><content type="html" xml:base="/2023/04/13/chipsee-hmi-buzzer.html"><![CDATA[<h2>
    How to Control Chipsee PC Buzzer with Python and Browser
</h2>
<p class="text-center text-muted">By Printfinn, last updated at 2023-04-13</p>
<p>
    Today let's see how we can control the builtin buzzer of a Chipsee industrial Pi PC.
</p>

<p>
    So, in the last <a href="/2023/03/21/chipsee-hmi-backlight.html">post</a> we learned about how to control the backlight of Chipsee industrial Pi PC. We also built an HMI using the browser to adjust the backlight. We also learned that we can write and read a Linux text file to control the backlight.
</p>

<p>
    Today let's use the same technique to control the buzzer. The basic idea is also writing to a Linux text file. To build an HMI, We will write a Python Flask server as the backend, an HTML file plus some Javascript in the browser as the frontend.
</p>

<p>
    In this post we'll skip the Flask installation part, if you're not familiar with it, you can refer to the post on <a href="/2023/03/21/chipsee-hmi-backlight.html#flask-hello-world">Start Python Flask Server: Hello World</a> section. Also, when in doubt, don't forget search engine is a developer's friend.
</p>

<h3>    
    Investigate Hardware Documentation
</h3>

<p>
    When I first got this Chipsee industrial Pi PC, I didn't know how to control these peripherals either. Thankfully, I found a software documentation online provided by Chipsee about this embedded PC: <a href="https://docs.chipsee.com/PCs/Pi/Software/Debian.html">link to the doc</a>. Under its "System Resource -> BUZZER" part, I found the solution. Chipsee engineers seem to want me to <b>echo</b> a <b>1</b> or a <b>0</b> to a Linux file <b>/dev/buzzer</b>.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">echo </span>1 <span class="o">&gt;</span> /dev/buzzer <span class="c"># Turn on Buzzer</span>
<span class="nv">$ </span><span class="nb">echo </span>0 <span class="o">&gt;</span> /dev/buzzer <span class="c"># Turn off Buzzer</span></code></pre></figure>

<p>
    Easy, eh?
</p>

<p>
    So I tested it myself manually and Bi--, the buzzer is on. While it's too loud even in my home I'm afraid my mom would yell and ask me what happend, I quickly turned it off with <b>echo 0 > /dev/buzzer</b>. Phew!
</p>

<h3>The Python Server Controller</h3>
<p>
    So it's clear that if I can build a simple HTML page to invoke a Python script to write a "1" or a "0" to this /dev/buzzer file, I'll be able to control the buzzer on this touch screen. We can ask a Flask server to do this:
</p>

<p>
    Let's create an <code>app.py</code> file, add a Flask route and action:
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1"># /app.py
</span><span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span>
<span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">request</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">"</span><span class="s">/buzzer</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">buzzer</span><span class="p">():</span>
    <span class="k">return</span> <span class="nf">render_template</span><span class="p">(</span><span class="sh">'</span><span class="s">buzzer.html</span><span class="sh">'</span><span class="p">)</span></code></pre></figure>

<h3>The Browser Part</h3>
<p>
    This means we can visit <code>localhost:5000/buzzer</code> to render an HTML file called <code>buzzer.html</code>. But we haven't added the HTML file, so let's add an HTML first in the <code>buzzer.html</code> under <code>templates</code> folder (create a <code>template</code> folder if you don't have one, it's the Flask's convention of where to store HTML templates):
</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"># templates/buzzer.html
<span class="cp">&lt;!doctype html&gt;</span>
<span class="nt">&lt;html&gt;</span>

<span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;title&gt;</span>Buzzer | Chipsee Industrial PC Demo<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/bootstrap.min.css') }}"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/style.css') }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/head&gt;</span>

<span class="nt">&lt;body</span> <span class="na">class=</span><span class="s">"dark-bg sticky-body"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"d-flex align-items-center justify-content-center"</span> <span class="na">style=</span><span class="s">"min-height: 90vh;"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row justify-content-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-auto buzzer-desc"</span><span class="nt">&gt;</span>Buzzer<span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row justify-content-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-auto form-check form-switch d-flex justify-content-center"</span> <span class="na">style=</span><span class="s">"font-size: 8em"</span><span class="nt">&gt;</span>
                        <span class="nt">&lt;input</span> <span class="na">class=</span><span class="s">"form-check-input my-2"</span> <span class="na">type=</span><span class="s">"checkbox"</span> <span class="na">role=</span><span class="s">"switch"</span> <span class="na">id=</span><span class="s">"buzzer-switch"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row justify-content-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-auto buzzer-btn"</span> <span class="na">id=</span><span class="s">"buzzer-btn"</span><span class="nt">&gt;&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/body&gt;</span></code></pre></figure>

<p>
    To start the Flask server, run:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>flask run <span class="nt">--host</span><span class="o">=</span>0.0.0.0 <span class="nt">--debug</span></code></pre></figure>

<p>
    Now you can visit this HTML, but it's just an ugly checkbox and a line of text "buzzer", in a weird position. We haven't added any CSS styles for it. Now let's add some CSS styles to make the page look better. Because the CSS is complex, I will skip how to write these styles, but just add the files to this project. Anyone who is familiar with frontend programming (know the CSS basics) should be able to do this by themselves.
</p>

<p>
    You should copy and paste two files to your project from our demo, the source code of these CSS files are on the Github: <a href="https://github.com/Chipsee/chipsee-industrial-pc-web/tree/master/static/css" target="_blank">in this folder</a>, you need the <code>bootstrap.min.css</code> file and <code>style.css</code> file. Copy and paste the content to your project's <code>static/css</code> folder. You should create this folder if you don't have one (A css folder inside a static folder). In the end, you should have <code>static/css/bootstrap.min.css</code> and <code>static/css/style.css</code> with the content of our demo code.
</p>

<p>
    
</p>
<p>
    Now let's visit <code>localhost:5000/buzzer</code> again on the Chromium browser of our Chipsee industrial Pi. You should see your app's webpage like this:
</p>
<div class="text-center my-2">
    <img src="/assets/images/posts/2023-04-13/buzzer_1.png" alt="webpage_buzzer" class="img-fluid rounded-2">
    <figcaption class="fw-light">What our buzzer switches will look like</figcaption>
</div>

<p>
    That's how our page will look like. You can try turning on and off the buzzer switch or touch the round button, but nothing will happen at the moment. Because we haven't told the switches what to do when we touch them.
</p>

<h3>Write to /dev/buzzer file with Python</h3>
<p>
    Let's get back to our Flask backend. At the beginning we tried to write to a Linux file with <code>echo 1 > /dev/buzzer</code>, now we will replicate this with a Python script. How? With Python's <code>with open & write</code>.
</p>

<p>
    We will create a folder named <code>models</code> in the root folder of our project, and then create a file named <code>buzzer.py</code>. You should have <code>models/buzzer.py</code> in your project folder.
</p>

<p>
    Then let's create a <code>Buzzer</code> class and write some really simple code, just write "1" or "0" to the Linux text file.
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1"># models/buzzer.py
</span><span class="k">class</span> <span class="nc">Buzzer</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
    <span class="n">ON</span> <span class="o">=</span> <span class="sh">"</span><span class="s">1</span><span class="sh">"</span>
    <span class="n">OFF</span> <span class="o">=</span> <span class="sh">"</span><span class="s">0</span><span class="sh">"</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="o">=</span> <span class="sh">"</span><span class="s">/dev/buzzer</span><span class="sh">"</span>

    <span class="k">def</span> <span class="nf">set_to</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">status</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="n">Buzzer</span><span class="p">.</span><span class="n">ON</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">set_on</span><span class="p">()</span>
        <span class="k">elif</span> <span class="n">status</span> <span class="o">==</span> <span class="n">Buzzer</span><span class="p">.</span><span class="n">OFF</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">set_off</span><span class="p">()</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">False</span>

    <span class="k">def</span> <span class="nf">set_on</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">'</span><span class="s">w</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
                <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">Buzzer</span><span class="p">.</span><span class="n">ON</span><span class="p">)</span>
                <span class="k">return</span> <span class="bp">True</span>
        <span class="k">except</span> <span class="n">PermissionError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[PermissionError]: Buzzer device cannot be found or cannot be operated.</span><span class="sh">"</span>
        <span class="k">except</span> <span class="nb">FileNotFoundError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[FileNotFoundError]: Buzzer device cannot be found on this machine.</span><span class="sh">"</span>

    <span class="k">def</span> <span class="nf">set_off</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">'</span><span class="s">w</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
                <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">Buzzer</span><span class="p">.</span><span class="n">OFF</span><span class="p">)</span>
                <span class="k">return</span> <span class="bp">True</span>
        <span class="k">except</span> <span class="n">PermissionError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[PermissionError]: Buzzer device cannot be found or cannot be operated.</span><span class="sh">"</span>
        <span class="k">except</span> <span class="nb">FileNotFoundError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[FileNotFoundError]: Buzzer device cannot be found on this machine.</span><span class="sh">"</span></code></pre></figure>

<p>
    The code explains itself quite well. In general, you will need to call a <code>buzzer.set_to("1")</code> or <code>buzzer.set_to("0")</code> in another place to turn the buzzer on or off.
</p>

<p>
    And our <code>app.py</code> will be this place! Under our first and only route <code>@app.route("/buzzer")</code>, let's add another route and method:
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1"># app.py
</span><span class="kn">from</span> <span class="n">models.buzzer</span> <span class="kn">import</span> <span class="n">Buzzer</span>
<span class="n">dev_buzzer</span> <span class="o">=</span> <span class="nc">Buzzer</span><span class="p">()</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/api/buzzer</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">api_buzzer</span><span class="p">():</span>
    <span class="n">req</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">json</span>
    <span class="n">new_status</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">req</span><span class="p">[</span><span class="sh">'</span><span class="s">buzzer</span><span class="sh">'</span><span class="p">])</span>
    <span class="n">msg</span> <span class="o">=</span> <span class="n">dev_buzzer</span><span class="p">.</span><span class="nf">set_to</span><span class="p">(</span><span class="n">new_status</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">msg</span> <span class="o">==</span> <span class="bp">True</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span> <span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">Success</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">msg</span><span class="sh">'</span><span class="p">:</span> <span class="n">new_status</span> <span class="p">}</span>
    <span class="k">return</span> <span class="p">{</span> <span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">Error</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">msg</span><span class="sh">'</span><span class="p">:</span> <span class="n">msg</span> <span class="p">}</span></code></pre></figure>

<p>
    This code block means we will accept a POST request from our browser, which has a parameter "buzzer" and the new status we want the buzzer to be. Then this code block will invoke the <code>buzzer.py</code> 's corresponding method and write those Linux text files to control our buzzer.
</p>

<p>
    Now we can ask the browser to send these requests to our Flask backend for us, but before implementing that, we can also test our backend in the terminal with the <code>cURL</code> program. You should already have that installed on your Linux operating system. I'll give you an example to ask cURL to invoke our <code>/api/buzzer</code> action.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">curl <span class="nt">--data</span> <span class="s1">'{"buzzer":"1"}'</span> http://localhost:5000/api/buzzer <span class="nt">--header</span> <span class="s2">"Content-Type:application/json"</span>
curl <span class="nt">--data</span> <span class="s1">'{"buzzer":"0"}'</span> http://localhost:5000/api/buzzer <span class="nt">--header</span> <span class="s2">"Content-Type:application/json"</span></code></pre></figure>

<p>
    The first line of the bash script turns on our buzzer by POSTing a JSON string to our <code>/api/buzzer</code> route, with an "on" instruction. The second obviously turns it off. By running the first commands, you should hear your Chipsee industrial Pi PC's buzzer Bi--. <span class="text-muted">(Don't do this when other people are sleeping!)</span>
</p>

<h3>Add Javascript to Our Webpage</h3>
<p>
    Now we can be sure our backend works fine, when we give it the correct instruction, it can turn on or off the buzzer. But we were using cURL, now let's replicate this process with Javascript in the browser. We want to touch our Chipsee PC's panel to turn on or off the switches, then we want to hear the Bi--.
</p>

<p>
    Open the <code>templates/buzzer.html</code> file, add the following code to the end of the file:
</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"># templates/buzzer.html

<span class="nt">&lt;script&gt;</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">oncontextmenu</span> <span class="o">=</span> <span class="nf">function </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// When pushing buzzer button, a long press will call out browser context menu(light mouse right click), this will forbid it.</span>
        <span class="nx">event</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span>
        <span class="nx">event</span><span class="p">.</span><span class="nf">stopImmediatePropagation</span><span class="p">();</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">};</span>
    <span class="kd">class</span> <span class="nc">BuzzerButton</span> <span class="p">{</span>
        <span class="nf">constructor</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/api/buzzer</span><span class="dl">"</span><span class="p">;</span>
            <span class="kd">let</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#buzzer-btn</span><span class="dl">"</span><span class="p">)</span>
            <span class="nx">btn</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">touchstart</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
            <span class="p">});</span>
            <span class="nx">btn</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">touchend</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
            <span class="p">})</span>
        <span class="p">}</span>

        <span class="nf">setBuzzer</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span><span class="p">,</span> <span class="p">{</span>
                <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// or 'PUT'</span>
                <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
                    <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
                <span class="p">},</span>
                <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="dl">'</span><span class="s1">buzzer</span><span class="dl">'</span><span class="p">:</span> <span class="nx">value</span> <span class="p">}),</span>
            <span class="p">})</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">())</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="k">if </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">status</span><span class="dl">'</span><span class="p">]</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Success</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
                        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Success:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                        <span class="k">throw </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">msg</span><span class="dl">'</span><span class="p">])</span>
                    <span class="p">}</span>
                <span class="p">})</span>
                <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
                <span class="p">});</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="kd">class</span> <span class="nc">BuzzerSwitch</span> <span class="p">{</span>
        <span class="nf">constructor</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/api/buzzer</span><span class="dl">"</span><span class="p">;</span>
            <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#buzzer-switch</span><span class="dl">"</span><span class="p">).</span><span class="nx">onchange</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="kd">let</span> <span class="nx">checked</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">checked</span><span class="p">;</span>
                <span class="k">if </span><span class="p">(</span><span class="nx">checked</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
                <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                    <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="nf">setBuzzer</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span><span class="p">,</span> <span class="p">{</span>
                <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// or 'PUT'</span>
                <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
                    <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
                <span class="p">},</span>
                <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="dl">'</span><span class="s1">buzzer</span><span class="dl">'</span><span class="p">:</span> <span class="nx">value</span> <span class="p">}),</span>
            <span class="p">})</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">())</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="k">if </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">status</span><span class="dl">'</span><span class="p">]</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Success</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
                        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Success:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                        <span class="k">throw </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">msg</span><span class="dl">'</span><span class="p">])</span>
                    <span class="p">}</span>
                <span class="p">})</span>
                <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
                <span class="p">});</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="kd">let</span> <span class="nx">buzzerSwitch</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BuzzerSwitch</span><span class="p">()</span>
    <span class="kd">let</span> <span class="nx">buzzerBtn</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BuzzerButton</span><span class="p">()</span>
<span class="nt">&lt;/script&gt;</span></code></pre></figure>

<p class="text-muted">
    You can of course put these in a separate JS file, that's good. But for educational purposes, I'll just leave them inside HTML.
</p>
<p>
    The code does three things:
</p>

<p>
    First, it tells the buzzer switch what to do when it is turned on or off. 
</p>

<p>
    Second, it tells the buzzer button what to do when it is pressed or released. 
</p>

<p>
    Third, because this is a browser, long pressing the button will call the context menu out (like a right click on the browser's any page with a mouse), we need to forbid this context menu from displaying.
</p>

<p>
    The code should explain itself if you are familiar with Javascript. They simply add several event listeners to the button or switch. If an event is triggered, like a 'touchstart' or 'touchend' or 'checked', the JS then initiates a HTTP POST request to our Flask backend server with a JSON object that our '/api/buzzer' route understands.
</p>

<h3>Test Your Buzzer HMI</h3>

<p>
    Now the code is complete, you can play around with the buzzer button or switch in the browser. When you turn on the switch, you should hear constant Bi--. This switch acts as a always-on, always-off switch.
</p>

<p>
    The buzzer button acts as a contact switch, only when you're touching or pressing the button should you hear the Bi--. Imagine you have a lifting machine and you're adjusting the angle of the arm and the height of the hook. And when you release the button, the Bi-- should stop immediately.
</p>

<h3>Final Code</h3>

<p>
    In the end, the code should look like this (except the two CSS files, which we didn't touch at all):
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1"># /app.py
</span><span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span>
<span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">request</span>

<span class="kn">from</span> <span class="n">models.buzzer</span> <span class="kn">import</span> <span class="n">Buzzer</span>
<span class="n">dev_buzzer</span> <span class="o">=</span> <span class="nc">Buzzer</span><span class="p">()</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">"</span><span class="s">/buzzer</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">buzzer</span><span class="p">():</span>
    <span class="k">return</span> <span class="nf">render_template</span><span class="p">(</span><span class="sh">'</span><span class="s">buzzer.html</span><span class="sh">'</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/api/buzzer</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">api_buzzer</span><span class="p">():</span>
    <span class="nf">print</span><span class="p">(</span><span class="n">request</span><span class="p">)</span>
    <span class="n">req</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">json</span>
    <span class="n">new_status</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">req</span><span class="p">[</span><span class="sh">'</span><span class="s">buzzer</span><span class="sh">'</span><span class="p">])</span>
    <span class="n">msg</span> <span class="o">=</span> <span class="n">dev_buzzer</span><span class="p">.</span><span class="nf">set_to</span><span class="p">(</span><span class="n">new_status</span><span class="p">)</span>
    <span class="k">if</span> <span class="n">msg</span> <span class="o">==</span> <span class="bp">True</span><span class="p">:</span>
        <span class="k">return</span> <span class="p">{</span> <span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">Success</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">msg</span><span class="sh">'</span><span class="p">:</span> <span class="n">new_status</span> <span class="p">}</span>
    <span class="k">return</span> <span class="p">{</span> <span class="sh">'</span><span class="s">status</span><span class="sh">'</span><span class="p">:</span> <span class="sh">'</span><span class="s">Error</span><span class="sh">'</span><span class="p">,</span> <span class="sh">'</span><span class="s">msg</span><span class="sh">'</span><span class="p">:</span> <span class="n">msg</span> <span class="p">}</span></code></pre></figure>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="c1"># models/buzzer.py
</span><span class="k">class</span> <span class="nc">Buzzer</span><span class="p">(</span><span class="nb">object</span><span class="p">):</span>
    <span class="n">ON</span> <span class="o">=</span> <span class="sh">"</span><span class="s">1</span><span class="sh">"</span>
    <span class="n">OFF</span> <span class="o">=</span> <span class="sh">"</span><span class="s">0</span><span class="sh">"</span>

    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="o">=</span> <span class="sh">"</span><span class="s">/dev/buzzer</span><span class="sh">"</span>

    <span class="k">def</span> <span class="nf">set_to</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">status</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">status</span> <span class="o">==</span> <span class="n">Buzzer</span><span class="p">.</span><span class="n">ON</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">set_on</span><span class="p">()</span>
        <span class="k">elif</span> <span class="n">status</span> <span class="o">==</span> <span class="n">Buzzer</span><span class="p">.</span><span class="n">OFF</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">self</span><span class="p">.</span><span class="nf">set_off</span><span class="p">()</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="k">return</span> <span class="bp">False</span>

    <span class="k">def</span> <span class="nf">set_on</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">'</span><span class="s">w</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
                <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">Buzzer</span><span class="p">.</span><span class="n">ON</span><span class="p">)</span>
                <span class="k">return</span> <span class="bp">True</span>
        <span class="k">except</span> <span class="n">PermissionError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[PermissionError]: Buzzer device cannot be found or cannot be operated.</span><span class="sh">"</span>
        <span class="k">except</span> <span class="nb">FileNotFoundError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[FileNotFoundError]: Buzzer device cannot be found on this machine.</span><span class="sh">"</span>

    <span class="k">def</span> <span class="nf">set_off</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">'</span><span class="s">w</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
                <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">Buzzer</span><span class="p">.</span><span class="n">OFF</span><span class="p">)</span>
                <span class="k">return</span> <span class="bp">True</span>
        <span class="k">except</span> <span class="n">PermissionError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[PermissionError]: Buzzer device cannot be found or cannot be operated.</span><span class="sh">"</span>
        <span class="k">except</span> <span class="nb">FileNotFoundError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">return</span> <span class="sh">"</span><span class="s">[FileNotFoundError]: Buzzer device cannot be found on this machine.</span><span class="sh">"</span></code></pre></figure>

<figure class="highlight"><pre><code class="language-html" data-lang="html"># templates/buzzer.html
<span class="cp">&lt;!doctype html&gt;</span>
<span class="nt">&lt;html&gt;</span>

<span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;title&gt;</span>Buzzer | Chipsee Industrial PC Demo<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/bootstrap.min.css') }}"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/style.css') }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/head&gt;</span>

<span class="nt">&lt;body</span> <span class="na">class=</span><span class="s">"dark-bg sticky-body"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"container"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"d-flex align-items-center justify-content-center"</span> <span class="na">style=</span><span class="s">"min-height: 90vh;"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row justify-content-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-auto buzzer-desc"</span><span class="nt">&gt;</span>Buzzer<span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row justify-content-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-auto form-check form-switch d-flex justify-content-center"</span> <span class="na">style=</span><span class="s">"font-size: 8em"</span><span class="nt">&gt;</span>
                        <span class="nt">&lt;input</span> <span class="na">class=</span><span class="s">"form-check-input my-2"</span> <span class="na">type=</span><span class="s">"checkbox"</span> <span class="na">role=</span><span class="s">"switch"</span> <span class="na">id=</span><span class="s">"buzzer-switch"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row justify-content-center"</span><span class="nt">&gt;</span>
                    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col-auto buzzer-btn"</span> <span class="na">id=</span><span class="s">"buzzer-btn"</span><span class="nt">&gt;&lt;/div&gt;</span>
                <span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/body&gt;</span>


<span class="nt">&lt;script&gt;</span>
    <span class="nb">document</span><span class="p">.</span><span class="nx">oncontextmenu</span> <span class="o">=</span> <span class="nf">function </span><span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="p">{</span>
        <span class="c1">// When pushing buzzer button, a long press will call out browser context menu(light mouse right click), this will forbid it.</span>
        <span class="nx">event</span><span class="p">.</span><span class="nf">preventDefault</span><span class="p">();</span>
        <span class="nx">event</span><span class="p">.</span><span class="nf">stopImmediatePropagation</span><span class="p">();</span>
        <span class="k">return</span> <span class="kc">false</span><span class="p">;</span>
    <span class="p">};</span>
    <span class="kd">class</span> <span class="nc">BuzzerButton</span> <span class="p">{</span>
        <span class="nf">constructor</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/api/buzzer</span><span class="dl">"</span><span class="p">;</span>
            <span class="kd">let</span> <span class="nx">btn</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#buzzer-btn</span><span class="dl">"</span><span class="p">)</span>
            <span class="nx">btn</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">touchstart</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
            <span class="p">});</span>
            <span class="nx">btn</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">touchend</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
            <span class="p">})</span>
        <span class="p">}</span>

        <span class="nf">setBuzzer</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span><span class="p">,</span> <span class="p">{</span>
                <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// or 'PUT'</span>
                <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
                    <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
                <span class="p">},</span>
                <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="dl">'</span><span class="s1">buzzer</span><span class="dl">'</span><span class="p">:</span> <span class="nx">value</span> <span class="p">}),</span>
            <span class="p">})</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">())</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="k">if </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">status</span><span class="dl">'</span><span class="p">]</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Success</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
                        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Success:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                        <span class="k">throw </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">msg</span><span class="dl">'</span><span class="p">])</span>
                    <span class="p">}</span>
                <span class="p">})</span>
                <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
                <span class="p">});</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="kd">class</span> <span class="nc">BuzzerSwitch</span> <span class="p">{</span>
        <span class="nf">constructor</span><span class="p">()</span> <span class="p">{</span>
            <span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span> <span class="o">=</span> <span class="dl">"</span><span class="s2">/api/buzzer</span><span class="dl">"</span><span class="p">;</span>
            <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">"</span><span class="s2">#buzzer-switch</span><span class="dl">"</span><span class="p">).</span><span class="nx">onchange</span> <span class="o">=</span> <span class="p">(</span><span class="nx">e</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                <span class="kd">let</span> <span class="nx">checked</span> <span class="o">=</span> <span class="nx">e</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">checked</span><span class="p">;</span>
                <span class="k">if </span><span class="p">(</span><span class="nx">checked</span><span class="p">)</span> <span class="p">{</span>
                    <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">1</span><span class="p">);</span>
                <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                    <span class="k">this</span><span class="p">.</span><span class="nf">setBuzzer</span><span class="p">(</span><span class="mi">0</span><span class="p">);</span>
                <span class="p">}</span>
            <span class="p">}</span>
        <span class="p">}</span>

        <span class="nf">setBuzzer</span><span class="p">(</span><span class="nx">value</span><span class="p">)</span> <span class="p">{</span>
            <span class="nf">fetch</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">endPoint</span><span class="p">,</span> <span class="p">{</span>
                <span class="na">method</span><span class="p">:</span> <span class="dl">'</span><span class="s1">POST</span><span class="dl">'</span><span class="p">,</span> <span class="c1">// or 'PUT'</span>
                <span class="na">headers</span><span class="p">:</span> <span class="p">{</span>
                    <span class="dl">'</span><span class="s1">Content-Type</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">application/json</span><span class="dl">'</span><span class="p">,</span>
                <span class="p">},</span>
                <span class="na">body</span><span class="p">:</span> <span class="nx">JSON</span><span class="p">.</span><span class="nf">stringify</span><span class="p">({</span> <span class="dl">'</span><span class="s1">buzzer</span><span class="dl">'</span><span class="p">:</span> <span class="nx">value</span> <span class="p">}),</span>
            <span class="p">})</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">response</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="nx">response</span><span class="p">.</span><span class="nf">json</span><span class="p">())</span>
                <span class="p">.</span><span class="nf">then</span><span class="p">((</span><span class="nx">data</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="k">if </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">status</span><span class="dl">'</span><span class="p">]</span> <span class="o">===</span> <span class="dl">'</span><span class="s1">Success</span><span class="dl">'</span><span class="p">)</span> <span class="p">{</span>
                        <span class="nx">console</span><span class="p">.</span><span class="nf">log</span><span class="p">(</span><span class="dl">'</span><span class="s1">Success:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">data</span><span class="p">);</span>
                    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
                        <span class="k">throw </span><span class="p">(</span><span class="nx">data</span><span class="p">[</span><span class="dl">'</span><span class="s1">msg</span><span class="dl">'</span><span class="p">])</span>
                    <span class="p">}</span>
                <span class="p">})</span>
                <span class="p">.</span><span class="k">catch</span><span class="p">((</span><span class="nx">error</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
                    <span class="nx">console</span><span class="p">.</span><span class="nf">error</span><span class="p">(</span><span class="dl">'</span><span class="s1">Error:</span><span class="dl">'</span><span class="p">,</span> <span class="nx">error</span><span class="p">);</span>
                <span class="p">});</span>
        <span class="p">}</span>
    <span class="p">}</span>
    <span class="kd">let</span> <span class="nx">buzzerSwitch</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BuzzerSwitch</span><span class="p">()</span>
    <span class="kd">let</span> <span class="nx">buzzerBtn</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">BuzzerButton</span><span class="p">()</span>
<span class="nt">&lt;/script&gt;</span></code></pre></figure>

<h3>Conclusion</h3>

<p>
    That's quite a lot of work! Hopefully your code works and works fine, if not don't hesitate to ask me.
</p>

<p>
    To wrap up, we built an HMI to control the buzzer of the Chipsee industrial Pi PC. We made it by writing to a Linux file with Python, and we asked our webpage to invoke this Python script with HTTP POST requests. The request is triggered any time when the switches on our webpage is turned on or off, or is pressed or released.
</p>

<p>
     So that's it, now you can control your Chipsee PC's buzzer with an HMI! (Pro tip: don't test this when other people are sleeping!)
</p>]]></content><author><name></name></author><summary type="html"><![CDATA[How to Control Chipsee PC Buzzer with Python and Browser By Printfinn, last updated at 2023-04-13 Today let's see how we can control the builtin buzzer of a Chipsee industrial Pi PC.]]></summary></entry><entry><title type="html">How to Control Chipsee PC Backlight with Python and Browser</title><link href="/2023/03/21/chipsee-hmi-backlight.html" rel="alternate" type="text/html" title="How to Control Chipsee PC Backlight with Python and Browser" /><published>2023-03-21T00:00:00+00:00</published><updated>2023-03-21T00:00:00+00:00</updated><id>/2023/03/21/chipsee-hmi-backlight</id><content type="html" xml:base="/2023/03/21/chipsee-hmi-backlight.html"><![CDATA[<h2>
    How to Control Chipsee PC Backlight with Python and Browser
</h2>
<p class="text-center text-muted">By Printfinn, last updated at 2023-04-13</p>
<p>
    Today let's see how we can adjust the screen backlight of a Chipsee industrial Pi PC.
</p>

<p>
So, a Chipsee engineer told me adjusting backlight is just writing some text file, and I thought, oh we have a Debian Linux operating system, writing text file is not hard. So I started to think about which file I should write to. And then luckily I found Chipsee has a document website, I then looked up if my model is in the document website. It turns out they have many docs online and my Industrial Pi model is among one of those.
</p>

<p>
So I opened the docs web page, and skimmed a little bit, then found the backlight part. It says I can get or set the brightness from these two files. So I went to the folder and checked what files are there, and if there are other files there which might be of interest to me.
</p>

<h3>Initial Attempt by Reading/Writing Linux Files</h3>
<p>
Luckily, there is a pwm-backlight folder in the backlight directory, there are files named max_brightness and brightness and some other files, according to the file names, which happen to be the same as those in the document.
</p>

<p>
So I cat a file, at first I tried the max_brightness, from its name, I assume it should list the maximum available brightness of this device, it returns a 99. It makes sense, the minimum be zero, the maximum be 100, or sometimes 99, so we have a 100 level pwm backlight.
</p>

<p>
Then I cat the brightness and actual_brightness files, they all returned 90 for me. I'm a little bit confused now, which one should I read to get the actual brightness? They all seem to be the same. My first assumption is they might have different Linux file permissions, or to avoid read/write conflicts on the same file. I'm not an expert on Linux drivers, so I just put it aside and proceeded. So I went back to the official document again, and the example Chipsee gives is writing to the brightness file to adjust the brightness. Let's try it, let's copy and paste to the terminal and OK, I can see from my Chipsee Pi, the screen is a lot darker now. Can I write something else? I then write 90 again to see if I can get it brighter. Cool. So I understand if I write to this file, then I can adjust the screen brightness of this device with any programming language.
</p>

<p>
Although not documented, I'd still love to see what's the difference between the actual_brightness and brightness file. I think they might not want me to write to the actual_brightness file, so I checked the file permission with ls -la, it gives only read permission, OK cool, so my assumption might be correct. But what about the brightness file, it has both read and write permission. But whatever, I'll just read the actual_brightness file to get the current value, and only write the brightness file to set a new value. I don't know the mechanism behind the files, maybe to avoid transaction problems? In the end it's just the backlight, so not a big deal, since it gives me the option, and the values are the same, then I'd pick a safe way to read.
</p>

<h3 id="flask-hello-world">Start Python Flask Server: Hello World</h3>
<p>
So now the first step investigation is done, let's write the actual GUI application.
</p>

<p>
Our aim is now to show the actual brightness value on a browser, and then try to modify this value in the browser, so we can adjust the brightness of our Chipsee industrial Pi.
</p>

<p>
I will use Python, Flask, Javascript, HTML, CSS and a web browser here. They are all very basic knowledge of web development. If you don't understand some steps, you can search for the keywords I mentioned on the Internet. These technologies are not limited to Chipsee PC, they're all very popular solutions in software engineering.
</p>

<p>
I use Flask as a lightweight web server, because it uses Python, and can understand HTML and HTTP, we don't need to know everything about Flask to start, we will only use a tiny small part of it, I also try to avoid the framework specific conventions, and try only to use the plain JS and HTML in the browser, so you can reuse the code on your favorite frameworks.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">mkdir </span>backlight
<span class="nv">$ </span><span class="nb">cd </span>backlight
<span class="nv">$ </span>python3 <span class="nt">-m</span> venv venv
<span class="nv">$ </span><span class="nb">.</span> venv/bin/activate
<span class="nv">$ </span>pip <span class="nb">install </span>Flask</code></pre></figure>

<p>
Let's create a default Flask app. Then let's run flask, and see if our Chipsee Pi can display a hello world. Cool.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span><span class="nb">touch </span>app.py</code></pre></figure>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">"</span><span class="s">/</span><span class="sh">"</span><span class="p">)</span>
<span class="k">def</span> <span class="nf">hello_world</span><span class="p">():</span>
    <span class="k">return</span> <span class="sh">"</span><span class="s">&lt;p&gt;Hello, World!&lt;/p&gt;</span><span class="sh">"</span></code></pre></figure>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash"><span class="nv">$ </span>flask run <span class="nt">--host</span><span class="o">=</span>0.0.0.0 <span class="nt">--debug</span></code></pre></figure>

<p>
On our PC we can visit the http://IP:5000 (change IP to your Chipsee PC IP address, you can get this IP by entering ifconfig in your Chipsee PC terminal and look around) address and see Hello World. It means our Flask demo app is running on the Chipsee PC.
</p>

<h3>A Web Page to Show Dummy Brightness</h3>
<p>
Then let's create a brightness.html file in the templates folder. The templates folder is Flask's convention of storing html template files, this is one of a few things about Flask we need to memorize for building this app. An HTML file has some markup structures, you can refer to the Mozilla web doc to learn more, but for our use case, we don't need too much, will just copy from our demo code, they're the html, head, body, and that's it.
</p>

<p>
To add some CSS styles, let's also copy the CSS files from the static folder of our demo. Otherwise our page will look too tedious.
</p>

<p> 
So let's change our code, we will return a brightness.html template, with a actual_brightness=actual_b, max_brightness=max_b, let assign a new variable named actual_b and give it a value, say 80. And also a max_b, say 100. And make sure that there is an actual_brightness, a max_brightness with double curly brackets in our HTML template file. I'll explain later what this means, let's first check what it will display in the browser. 
</p>

The brightness.html should look like this:

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="cp">&lt;!doctype html&gt;</span>
<span class="nt">&lt;html&gt;</span>

<span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;title&gt;</span>Brightness | Chipsee Industrial PC Demo<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/bootstrap.min.css') }}"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/style.css') }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/head&gt;</span>

<span class="nt">&lt;body</span> <span class="na">class=</span><span class="s">"dark-bg sticky-body"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row w-100"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col col-9 vh-100"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"brightness-text d-flex flex-column align-items-center justify-content-center"</span>
                <span class="na">style=</span><span class="s">"height:95vh;"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"brightness-desc"</span><span class="nt">&gt;</span>Brightness<span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"brightness-value"</span><span class="nt">&gt;</span>{{ actual_brightness }}<span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col position-relative"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"range"</span> <span class="na">class=</span><span class="s">"brightness-slider brightness-slider-position"</span>
                <span class="na">id=</span><span class="s">"brightness-slider"</span> <span class="na">name=</span><span class="s">"brightness"</span> <span class="na">min=</span><span class="s">"0"</span> <span class="na">max=</span><span class="s">"{{ max_brightness }}"</span> <span class="na">step=</span><span class="s">"1"</span>
                <span class="na">value=</span><span class="s">"{{ actual_brightness }}"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/body&gt;</span>

<span class="nt">&lt;/html&gt;</span></code></pre></figure>

The app.py should look like this:

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span>
<span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">request</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/brightness</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">brightness</span><span class="p">():</span>
    <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">actual_b</span> <span class="o">=</span> <span class="sh">"</span><span class="s">80</span><span class="sh">"</span>
        <span class="n">max_b</span> <span class="o">=</span> <span class="sh">"</span><span class="s">100</span><span class="sh">"</span>
        <span class="k">return</span> <span class="nf">render_template</span><span class="p">(</span><span class="sh">'</span><span class="s">brightness.html</span><span class="sh">'</span><span class="p">,</span> <span class="n">actual_brightness</span><span class="o">=</span><span class="n">actual_b</span><span class="p">,</span> <span class="n">max_brightness</span><span class="o">=</span><span class="n">max_b</span><span class="p">)</span></code></pre></figure>

<p>
Let's visit the http://IP:5000/brightness page of the browser, and see there is a 80 displaying alongside the slider. This 80 comes from the brightness inside curly brackets, and, the value is passed by the same name variable when we return the render template, see there is a actual_brightness=actual_b, it's the 80 that is passed to the actual_brightness inside the curly brackets in the brightness.html. Now you can play around and change this value.
</p>

<p>
For the max_brightness=max_b, this informs the slider the max value it can approach. Some Chipsee industrial PCs have 255 levels of backlight, and some 100 levels. This value should be obtained from the OS files we looked at in the beginning, remember the pwm-backlight folder? There is a max_brightness file.
</p>

<p>
So, things get easier now. If we can grep the actual brightness value from Chipsee operating system files, then we can display the actual brightness in our GUI application right? It's even easier, let's just read the file with Python.
</p>

<h3>Get the Actual Brightness from File with Python</h3>
<p> 
Let's create a function to read this file. I'll just copy paste the file from our demo code. And import it in our app.py.
</p>

<p>
The models/brightness.py file should look like this:
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">os</span>
<span class="kn">import</span> <span class="n">subprocess</span>

<span class="k">class</span> <span class="nc">Brightness</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="o">=</span> <span class="sh">"</span><span class="s">/sys/class/backlight/pwm-backlight</span><span class="sh">"</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Brightness: cannot find brightness device in config file, brightness model not initialized.</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">return</span>
        <span class="n">self</span><span class="p">.</span><span class="n">max_brightness_f</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">"</span><span class="s">max_brightness</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">actual_brightness_f</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">"</span><span class="s">actual_brightness</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">brightness_f</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">"</span><span class="s">brightness</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">give_linux_permission</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">init_max_brightness</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">get_actual_brightness</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="mi">0</span>

        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">actual_brightness_f</span><span class="p">,</span> <span class="sh">'</span><span class="s">r</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">int</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">set_brightness</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">brightness</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="mi">0</span>

        <span class="n">_b</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">brightness</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">_b</span> <span class="o">&lt;</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">_b</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">_b</span> <span class="o">&gt;</span> <span class="n">self</span><span class="p">.</span><span class="n">max_brightness</span><span class="p">:</span>
            <span class="n">_b</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">max_brightness</span>
        <span class="n">brightness</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">_b</span><span class="p">)</span>
        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">brightness_f</span><span class="p">,</span> <span class="sh">'</span><span class="s">w</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">brightness</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">init_max_brightness</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">max_brightness_f</span><span class="p">,</span> <span class="sh">'</span><span class="s">r</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">max_brightness</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">give_linux_permission</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="sh">"""</span><span class="s">
        Some Industrial PC doesn</span><span class="sh">'</span><span class="s">t allow write permission of brightness file, like PX30.
        This method gives write permission of backlight brightness Linux file to the user running this program.
        </span><span class="sh">"""</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">([</span><span class="sh">"</span><span class="s">sudo</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">chmod</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">a+w</span><span class="sh">"</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">brightness_f</span><span class="p">])</span></code></pre></figure>

<p>
After importing the Brightness class, the app.py file should look like this:
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span>
<span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">request</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="kn">from</span> <span class="n">models.brightness</span> <span class="kn">import</span> <span class="n">Brightness</span>
<span class="n">dev_brightness</span> <span class="o">=</span> <span class="nc">Brightness</span><span class="p">()</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/brightness</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">brightness</span><span class="p">():</span>
    <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">actual_b</span> <span class="o">=</span> <span class="n">dev_brightness</span><span class="p">.</span><span class="nf">get_actual_brightness</span><span class="p">()</span>
        <span class="n">max_b</span> <span class="o">=</span> <span class="n">dev_brightness</span><span class="p">.</span><span class="n">max_brightness</span> <span class="ow">or</span> <span class="sh">"</span><span class="s">100</span><span class="sh">"</span>
        <span class="k">return</span> <span class="nf">render_template</span><span class="p">(</span><span class="sh">'</span><span class="s">brightness.html</span><span class="sh">'</span><span class="p">,</span> <span class="n">actual_brightness</span><span class="o">=</span><span class="n">actual_b</span><span class="p">,</span> <span class="n">max_brightness</span><span class="o">=</span><span class="n">max_b</span><span class="p">)</span></code></pre></figure>

<p>
Then when we refresh the page, we should see on the webpage that the value is the current brightness value of our Chipsee industrial PC. And the slider should be in a place where it represents the same value.
</p>

<p>
The models/brightness.py does a few things: It gives Linux permission to the backlight driver file, and reads values from them. For example to get the maximum available brightness or current actual brightness. It also has a method to set brightness to the Chipsee PC. We will try this method now, and see if we can set a new brightness to this Chipsee PC.
</p>

<h3>Setting a New Brightness from Browser</h3>
<p>
So to set a value we should use a POST http request. Let's first ask our Flask server to listen to POST request, we can write a method like this in the app.py file:
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/api/brightness</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">api_brightness</span><span class="p">():</span>
    <span class="n">new_brightness</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">form</span><span class="p">[</span><span class="sh">"</span><span class="s">brightness</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">dev_brightness</span><span class="p">.</span><span class="nf">set_brightness</span><span class="p">(</span><span class="n">brightness</span><span class="o">=</span><span class="n">new_brightness</span><span class="p">)</span>
    <span class="n">actual_brightness</span> <span class="o">=</span> <span class="n">dev_brightness</span><span class="p">.</span><span class="nf">get_actual_brightness</span><span class="p">()</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">brightness</span><span class="sh">"</span><span class="p">:</span> <span class="n">actual_brightness</span><span class="p">}</span></code></pre></figure>

<p>
And in our templates/brightness.html, we can add a Javascript function, to trigger a POST request whenever the slider's value changes:
</p>

<figure class="highlight"><pre><code class="language-js" data-lang="js"><span class="o">&lt;</span><span class="nx">script</span><span class="o">&gt;</span>
    <span class="kd">const</span> <span class="nx">brightnessSlider</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">#brightness-slider</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">curr_brightness</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.brightness-value</span><span class="dl">'</span><span class="p">);</span>
    <span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">DOMContentLoaded</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">brightnessSlider</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">curr_brightness</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nf">trim</span><span class="p">()}</span><span class="s2">`</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="nx">brightnessSlider</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">input</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">xhr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">XMLHttpRequest</span><span class="p">();</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/api/brightness</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nf">setRequestHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/x-www-form-urlencoded</span><span class="dl">"</span><span class="p">);</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nx">onreadystatechange</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Call a function when the state changes.</span>
            <span class="k">if </span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">DONE</span> <span class="o">&amp;&amp;</span> <span class="nx">xhr</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
                <span class="c1">// Request finished. Do processing here.</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="s2">`brightness=</span><span class="p">${</span><span class="nx">brightnessSlider</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="nx">curr_brightness</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
    <span class="p">});</span>
<span class="o">&lt;</span><span class="sr">/script&gt;</span></code></pre></figure>

<p>
These two parts of code do something like this: whenever the slider value changes, for example a user touches or slides the slider, then Javascript can listen to the change and perform a XML http request to our Flask server. The JS will send a POST request to /api/brightness endpoint, sending the slider's value together with this request. In the meantime, JS will also change the value displayed on the browser to the slider's value.
</p>

<p>
And when Flask server receives this XML http request that the browser's JS sends, it will read the value this request carries with. And then call a Python method to write to a file, which is the file of Chipsee PC's brightness controller.
</p>

<p>
Then the screen backlight brightness will change to the value the slider indicates, thus setting a new brightness.
</p>

<p>
Not too complicated hah?
</p>

<p>
Now we can set our Chipsee PC's brightness through a browser GUI, woo hoo!
</p>

<h3>Final Code</h3>
<p>
In the end, your files should look like this:
First, you have an app.py, which is the entrypoint of your Flask server app:
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">Flask</span><span class="p">,</span> <span class="n">render_template</span>
<span class="kn">from</span> <span class="n">flask</span> <span class="kn">import</span> <span class="n">request</span>

<span class="n">app</span> <span class="o">=</span> <span class="nc">Flask</span><span class="p">(</span><span class="n">__name__</span><span class="p">)</span>

<span class="kn">from</span> <span class="n">models.brightness</span> <span class="kn">import</span> <span class="n">Brightness</span>
<span class="n">dev_brightness</span> <span class="o">=</span> <span class="nc">Brightness</span><span class="p">()</span>

<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/brightness</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">brightness</span><span class="p">():</span>
    <span class="k">if</span> <span class="n">request</span><span class="p">.</span><span class="n">method</span> <span class="o">==</span> <span class="sh">'</span><span class="s">GET</span><span class="sh">'</span><span class="p">:</span>
        <span class="n">actual_b</span> <span class="o">=</span> <span class="n">dev_brightness</span><span class="p">.</span><span class="nf">get_actual_brightness</span><span class="p">()</span>
        <span class="n">max_b</span> <span class="o">=</span> <span class="n">dev_brightness</span><span class="p">.</span><span class="n">max_brightness</span> <span class="ow">or</span> <span class="sh">"</span><span class="s">100</span><span class="sh">"</span>
        <span class="k">return</span> <span class="nf">render_template</span><span class="p">(</span><span class="sh">'</span><span class="s">brightness.html</span><span class="sh">'</span><span class="p">,</span> <span class="n">actual_brightness</span><span class="o">=</span><span class="n">actual_b</span><span class="p">,</span> <span class="n">max_brightness</span><span class="o">=</span><span class="n">max_b</span><span class="p">)</span>
    
<span class="nd">@app.route</span><span class="p">(</span><span class="sh">'</span><span class="s">/api/brightness</span><span class="sh">'</span><span class="p">,</span> <span class="n">methods</span><span class="o">=</span><span class="p">[</span><span class="sh">'</span><span class="s">POST</span><span class="sh">'</span><span class="p">])</span>
<span class="k">def</span> <span class="nf">api_brightness</span><span class="p">():</span>
    <span class="n">new_brightness</span> <span class="o">=</span> <span class="n">request</span><span class="p">.</span><span class="n">form</span><span class="p">[</span><span class="sh">"</span><span class="s">brightness</span><span class="sh">"</span><span class="p">]</span>
    <span class="n">dev_brightness</span><span class="p">.</span><span class="nf">set_brightness</span><span class="p">(</span><span class="n">brightness</span><span class="o">=</span><span class="n">new_brightness</span><span class="p">)</span>
    <span class="n">actual_brightness</span> <span class="o">=</span> <span class="n">dev_brightness</span><span class="p">.</span><span class="nf">get_actual_brightness</span><span class="p">()</span>
    <span class="k">return</span> <span class="p">{</span><span class="sh">"</span><span class="s">brightness</span><span class="sh">"</span><span class="p">:</span> <span class="n">actual_brightness</span><span class="p">}</span></code></pre></figure>

<p>
Second, you have a templates/brightness.html, which is how your GUI looks like in the browser:
</p>

<figure class="highlight"><pre><code class="language-html" data-lang="html"><span class="cp">&lt;!doctype html&gt;</span>
<span class="nt">&lt;html&gt;</span>

<span class="nt">&lt;head&gt;</span>
    <span class="nt">&lt;title&gt;</span>Brightness | Chipsee Industrial PC Demo<span class="nt">&lt;/title&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/bootstrap.min.css') }}"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;link</span> <span class="na">rel=</span><span class="s">"stylesheet"</span> <span class="na">href=</span><span class="s">"{{ url_for('static', filename='css/style.css') }}"</span><span class="nt">&gt;</span>
<span class="nt">&lt;/head&gt;</span>

<span class="nt">&lt;body</span> <span class="na">class=</span><span class="s">"dark-bg sticky-body"</span><span class="nt">&gt;</span>
    <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"row w-100"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col col-9 vh-100"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"brightness-text d-flex flex-column align-items-center justify-content-center"</span>
                <span class="na">style=</span><span class="s">"height:95vh;"</span><span class="nt">&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"brightness-desc"</span><span class="nt">&gt;</span>Brightness<span class="nt">&lt;/div&gt;</span>
                <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"brightness-value"</span><span class="nt">&gt;</span>{{ actual_brightness }}<span class="nt">&lt;/div&gt;</span>
            <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
        <span class="nt">&lt;div</span> <span class="na">class=</span><span class="s">"col position-relative"</span><span class="nt">&gt;</span>
            <span class="nt">&lt;input</span> <span class="na">type=</span><span class="s">"range"</span> <span class="na">class=</span><span class="s">"brightness-slider brightness-slider-position"</span>
                <span class="na">id=</span><span class="s">"brightness-slider"</span> <span class="na">name=</span><span class="s">"brightness"</span> <span class="na">min=</span><span class="s">"0"</span> <span class="na">max=</span><span class="s">"{{ max_brightness }}"</span> <span class="na">step=</span><span class="s">"1"</span>
                <span class="na">value=</span><span class="s">"{{ actual_brightness }}"</span><span class="nt">&gt;</span>
        <span class="nt">&lt;/div&gt;</span>
    <span class="nt">&lt;/div&gt;</span>
<span class="nt">&lt;/body&gt;</span>

<span class="nt">&lt;/html&gt;</span>

<span class="nt">&lt;script&gt;</span>
    <span class="kd">const</span> <span class="nx">brightnessSlider</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">#brightness-slider</span><span class="dl">'</span><span class="p">);</span>
    <span class="kd">const</span> <span class="nx">curr_brightness</span> <span class="o">=</span> <span class="nb">document</span><span class="p">.</span><span class="nf">querySelector</span><span class="p">(</span><span class="dl">'</span><span class="s1">.brightness-value</span><span class="dl">'</span><span class="p">);</span>
    <span class="nb">document</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">"</span><span class="s2">DOMContentLoaded</span><span class="dl">"</span><span class="p">,</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="nx">brightnessSlider</span><span class="p">.</span><span class="nx">value</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">curr_brightness</span><span class="p">.</span><span class="nx">textContent</span><span class="p">.</span><span class="nf">trim</span><span class="p">()}</span><span class="s2">`</span><span class="p">;</span>
    <span class="p">});</span>
    <span class="nx">brightnessSlider</span><span class="p">.</span><span class="nf">addEventListener</span><span class="p">(</span><span class="dl">'</span><span class="s1">input</span><span class="dl">'</span><span class="p">,</span> <span class="p">(</span><span class="nx">event</span><span class="p">)</span> <span class="o">=&gt;</span> <span class="p">{</span>
        <span class="kd">const</span> <span class="nx">xhr</span> <span class="o">=</span> <span class="k">new</span> <span class="nc">XMLHttpRequest</span><span class="p">();</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nf">open</span><span class="p">(</span><span class="dl">"</span><span class="s2">POST</span><span class="dl">"</span><span class="p">,</span> <span class="dl">'</span><span class="s1">/api/brightness</span><span class="dl">'</span><span class="p">,</span> <span class="kc">true</span><span class="p">);</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nf">setRequestHeader</span><span class="p">(</span><span class="dl">"</span><span class="s2">Content-Type</span><span class="dl">"</span><span class="p">,</span> <span class="dl">"</span><span class="s2">application/x-www-form-urlencoded</span><span class="dl">"</span><span class="p">);</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nx">onreadystatechange</span> <span class="o">=</span> <span class="p">()</span> <span class="o">=&gt;</span> <span class="p">{</span> <span class="c1">// Call a function when the state changes.</span>
            <span class="k">if </span><span class="p">(</span><span class="nx">xhr</span><span class="p">.</span><span class="nx">readyState</span> <span class="o">===</span> <span class="nx">XMLHttpRequest</span><span class="p">.</span><span class="nx">DONE</span> <span class="o">&amp;&amp;</span> <span class="nx">xhr</span><span class="p">.</span><span class="nx">status</span> <span class="o">===</span> <span class="mi">200</span><span class="p">)</span> <span class="p">{</span>
                <span class="c1">// Request finished. Do processing here.</span>
            <span class="p">}</span>
        <span class="p">}</span>
        <span class="nx">xhr</span><span class="p">.</span><span class="nf">send</span><span class="p">(</span><span class="s2">`brightness=</span><span class="p">${</span><span class="nx">brightnessSlider</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">`</span><span class="p">);</span>
        <span class="nx">curr_brightness</span><span class="p">.</span><span class="nx">textContent</span> <span class="o">=</span> <span class="s2">`</span><span class="p">${</span><span class="nx">event</span><span class="p">.</span><span class="nx">target</span><span class="p">.</span><span class="nx">value</span><span class="p">}</span><span class="s2">`</span><span class="p">;</span>
    <span class="p">});</span>
<span class="nt">&lt;/script&gt;</span></code></pre></figure>

<p>
Third, you have a models/brightness.py, which deals with the reading and writing of Chipsee pwm-backlight:
</p>

<figure class="highlight"><pre><code class="language-python" data-lang="python"><span class="kn">import</span> <span class="n">os</span>
<span class="kn">import</span> <span class="n">subprocess</span>

<span class="k">class</span> <span class="nc">Brightness</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="o">=</span> <span class="sh">"</span><span class="s">/sys/class/backlight/pwm-backlight</span><span class="sh">"</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="nf">print</span><span class="p">(</span><span class="sh">"</span><span class="s">Brightness: cannot find brightness device in config file, brightness model not initialized.</span><span class="sh">"</span><span class="p">)</span>
            <span class="k">return</span>
        <span class="n">self</span><span class="p">.</span><span class="n">max_brightness_f</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">"</span><span class="s">max_brightness</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">actual_brightness_f</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">"</span><span class="s">actual_brightness</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="n">brightness_f</span> <span class="o">=</span> <span class="n">os</span><span class="p">.</span><span class="n">path</span><span class="p">.</span><span class="nf">join</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">device</span><span class="p">,</span> <span class="sh">"</span><span class="s">brightness</span><span class="sh">"</span><span class="p">)</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">give_linux_permission</span><span class="p">()</span>
        <span class="n">self</span><span class="p">.</span><span class="nf">init_max_brightness</span><span class="p">()</span>

    <span class="k">def</span> <span class="nf">get_actual_brightness</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="mi">0</span>

        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">actual_brightness_f</span><span class="p">,</span> <span class="sh">'</span><span class="s">r</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="k">return</span> <span class="nf">int</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">set_brightness</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">brightness</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span> <span class="mi">0</span>

        <span class="n">_b</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">brightness</span><span class="p">)</span>
        <span class="k">if</span> <span class="n">_b</span> <span class="o">&lt;</span> <span class="mi">1</span><span class="p">:</span>
            <span class="n">_b</span> <span class="o">=</span> <span class="mi">1</span>
        <span class="k">if</span> <span class="n">_b</span> <span class="o">&gt;</span> <span class="n">self</span><span class="p">.</span><span class="n">max_brightness</span><span class="p">:</span>
            <span class="n">_b</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="n">max_brightness</span>
        <span class="n">brightness</span> <span class="o">=</span> <span class="nf">str</span><span class="p">(</span><span class="n">_b</span><span class="p">)</span>
        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">brightness_f</span><span class="p">,</span> <span class="sh">'</span><span class="s">w</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="k">return</span> <span class="n">f</span><span class="p">.</span><span class="nf">write</span><span class="p">(</span><span class="n">brightness</span><span class="p">)</span>

    <span class="k">def</span> <span class="nf">init_max_brightness</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="k">with</span> <span class="nf">open</span><span class="p">(</span><span class="n">self</span><span class="p">.</span><span class="n">max_brightness_f</span><span class="p">,</span> <span class="sh">'</span><span class="s">r</span><span class="sh">'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
            <span class="n">self</span><span class="p">.</span><span class="n">max_brightness</span> <span class="o">=</span> <span class="nf">int</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="nf">read</span><span class="p">())</span>

    <span class="k">def</span> <span class="nf">give_linux_permission</span><span class="p">(</span><span class="n">self</span><span class="p">):</span>
        <span class="sh">"""</span><span class="s">
        Some Industrial PCs don</span><span class="sh">'</span><span class="s">t allow write permission for brightness file, like the PX30.
        This method gives write permission of the backlight brightness Linux file to the user running this program.
        </span><span class="sh">"""</span>
        <span class="k">if</span> <span class="n">self</span><span class="p">.</span><span class="n">device</span> <span class="ow">is</span> <span class="bp">None</span><span class="p">:</span>
            <span class="k">return</span>
        <span class="n">subprocess</span><span class="p">.</span><span class="nf">run</span><span class="p">([</span><span class="sh">"</span><span class="s">sudo</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">chmod</span><span class="sh">"</span><span class="p">,</span> <span class="sh">"</span><span class="s">a+w</span><span class="sh">"</span><span class="p">,</span> <span class="n">self</span><span class="p">.</span><span class="n">brightness_f</span><span class="p">])</span></code></pre></figure>

<p>
Also, you should have static/css/style.css and bootstrap.min.css, which sets how things look like in the browser, you should copy them from the demo app. If you find your brightness web page looks weird, you should remember to check these two CSS files, see if you copied them currently.
</p>

<h3>Conclusion</h3>
<p>
    To sum up, to get the actual brightness, we ask Python to read a file, and to set a new brightness, we touch the slider, which triggers a POST request to our Flask backend, which will then write to a Linux file. This way, we're able to get or set the Chipsee PC backlight brightness by reading or writing to Linux files.
</p>
<p>
So that's it, now you can set your Chipsee PC's brightness with an HMI!
</p>]]></content><author><name></name></author><summary type="html"><![CDATA[How to Control Chipsee PC Backlight with Python and Browser By Printfinn, last updated at 2023-04-13 Today let's see how we can adjust the screen backlight of a Chipsee industrial Pi PC.]]></summary></entry><entry><title type="html">How to Deploy a Rails app to Ubuntu with Dokku in 2023</title><link href="/2022/03/18/deploy-rails-with-dokku.html" rel="alternate" type="text/html" title="How to Deploy a Rails app to Ubuntu with Dokku in 2023" /><published>2022-03-18T00:00:00+00:00</published><updated>2022-03-18T00:00:00+00:00</updated><id>/2022/03/18/deploy-rails-with-dokku</id><content type="html" xml:base="/2022/03/18/deploy-rails-with-dokku.html"><![CDATA[<h2 class="display-6 fw-bold">
    Deploy a Rails app to Ubuntu with Dokku in 2023
</h2>

<p class="text-center text-muted">
    by printfinn, Last updated on Mar 18, 2022.
</p>

<h3 class="fw-bold">
    Foreword: Who should read this?
</h3>

<p>
    In this guide, I will introduce how to deploy a Ruby on Rails app from scratch with the Dokku open source program. This guide aims at beginners who are passionate about deploying their Ruby on Rails applications to the Internet.
</p>

<p>
    After you practice with the guide, you should be able to push your Rails app to the Internet with about ~20 lines of easy to understand commands(even for beginners without too much Linux knowledge!). It will give you a working online Rails app without much overhead: a database connected with your app, a domain name guarded with https, a redirect from non-www to www or vice versa. After the initial deployment, the proceeding deployments only take a simple "git push".
</p>

<h3 class="fw-bold">
    Introduction: Four approaches commonly used to deploy a Ruby on Rails web application
</h3>

<ul>
    <li>Set up rbenv/rvm, Ruby and Rails then upload code manually.</li>
    <li>Set up rbenv/rvm, Ruby and Rails then deploy with Capistrano.</li>
    <li>Container based approaches: Docker and k8s.</li>
    <li>Git push to Heroku / <span class="text-danger fw-bold">Git push with Dokku</span>.</li>
    <li>With the MRSK Gem</li>
</ul>

<p>
    I'll briefly talk about the 4 approaches then focus on the 4th approach: <span class="text-danger fw-bold">deploy with git push Dokku</span>.
</p>

<p>
    Option No.1 is the most straightforward approach, like setting up a Rails develop environment on your local machine, a similar environment should be set up on the server manually by you. The only two differences are:
</p>

<p>
    First, on your local dev machine(your desktop PC or Mac laptop), you generate default Rails scaffold code with rails new and then type your business code through a keyboard. But instead of typing them again on the server, you need to upload the code to your server from your local machine, maybe with a SCP upload GUI or through the command line.
</p>

<p>
    Second, when developing, you run the Rails server with bin/rails server or bin/dev, which serves the HTTP server in development mode. But when deploying your Ruby on Rails app, you need to specify the environment to be production mode.
</p>

<p>
    Option No.2 is similar to option No.1, but saves you some effort in uploading your code, it also manages running your application in stage/production mode for you. In this approach, setting up the rbenv/rvm and Ruby like that in option No.1 is still required. Additionally, the Capistrano software needs to be set up by yourself. I've heard of people running 5 year old profitable Ruby on Rails applications with this approach.
</p>

<p>
    Option No.3 is more of an enterprise selection. Basecamp runs their Hey product with both k8s and Docker. Container based deployment is good when you have a team and a single server is not enough for serving your app. But extra effort needs to be put into maintaining the Docker configurations.
</p>

<p>
    Option No.4 is the main topic of this guide. Heroku is a managed cloud service, Dokku is an open source software which you can install on any of your servers. After installing the Dokku software on your server, you can get a "herokuish" flavored deployment environment: deploy with a single line of "git push".
</p>

<h3 class="fw-bold">
    Obtaining a domain name
</h3>

<p>
    If this is your first time to deploy a website, you need to buy a domain name, so that people could find your website with your domain name like www.domain.name (e.g. this site uses www.railsbay.com, Google uses www.google.com).
</p>

<p>
    My favorite domain registrar is Namesilo.
</p>

<h3 class="fw-bold">
    Setting up your cloud server
</h3>

<p>
    Besides purchasing a domain name, another infrastructure you have to buy is a virtual private server(or cloud server). I used to deploy my apps on Digital Ocean's vps but nowadays I'm also happy with Linode's services. Apart from DO, you can also use AWS ec2 or other cloud servers, as long as they provide a command line interface that you can ssh into, they are all fine.
</p>

<h3 class="fw-bold">
    Linking your domain name to your cloud server
</h3>

<p>
    When you're done provisioning a server and purchasing a domain name, don't forget to link them! You need to set an "A Record" in your DNS provider(usually the company where you bought your domain) to point to the public IP address provided by your VPS vendor(Digital Ocean, AWS EC2, Linode for example).
</p>

<p>
    I usually point www.railsbay.com and railsbay.com both to my server IP, e.g.: 198.11.5.256(<span class="text-warning">Don't use this one, use the IP your server possesses!</span>). After doing this, when people input the domain name in the browser address bar, the browser can find the actual server that domain lives in by looking up the DNS servers. Be sure to wait a few hours for the DNS to propagate.
</p>

<h3 class="fw-bold">
    Installing Dokku
</h3>

<p>
    To install the Dokku open source software, and set a global domain, run the following three commands in your Ubuntu cloud server.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span>wget https://raw.githubusercontent.com/dokku/dokku/v0.26.8/bootstrap.sh<span class="p">;</span>
<span class="nv">$ </span><span class="nb">sudo </span><span class="nv">DOKKU_TAG</span><span class="o">=</span>v0.26.8 bash bootstrap.sh
<span class="nv">$ </span>dokku domains:set-global 198.11.5.256 <span class="c"># use your server IP address here</span></code></pre></figure>

<p>
    If you are not comfortable running some random scripts, you can also refer to Dokku's official website: <a href="https://dokku.com/docs/getting-started/installation">Getting Started with Dokku</a> to install Dokku.
</p>

<h3 class="fw-bold">
    Installing a Dokku database plugin
</h3>

<p>
    To install a PostgreSQL or MySQL plugin, managed by Dokku on your server (take PostgreSQL for example):
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span><span class="nb">sudo </span>dokku plugin:install https://github.com/dokku/dokku-postgres.git</code></pre></figure>

<h3 class="fw-bold">
    Installing a Dokku Redis plugin
</h3>

<p>
    To install a redis plugin, managed by Dokku on your server:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span><span class="nb">sudo </span>dokku plugin:install https://github.com/dokku/dokku-redis.git redis</code></pre></figure>

<h3 class="fw-bold">
    Creating your first Dokku based Ruby on Rails application
</h3>

<p>
    Now that our infrastructure is ready, we may proceed to push our Rails app to the cloud server. First we need to create a Dokku app, then we'll create a corresponding database for this app, finally we'll connect the database to the app.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span>dokku apps:create sample-app
<span class="nv">$ </span>dokku postgres:create sample-app-db
<span class="nv">$ </span>dokku postgres:link sample-app-db sample-app
<span class="nv">$ </span>dokku redis:create sample-app-redis
<span class="nv">$ </span>dokku redis:link sample-app-redis sample-app</code></pre></figure>

<p>
    A Dokku app container named "sample-app" is created, but no code is inside. We then need to push our Rails app's code from our local machine.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your <span class="nb">local </span>machine
<span class="nv">$ </span>git remote add dokku dokku@198.11.5.256:sample-app
<span class="nv">$ </span>git push dokku main</code></pre></figure>

<p>
    You can then find from the terminal that your Rails app is already running. You should be able to visit the app with your <b>"IP:port"</b>, where the port is generated by Dokku automatically, such as: <b>http://198.11.5.256:54930</b>, you can find this address from the terminal output after running the git command, if your app deployed successfully.
</p>

<p>
    And don't forget to migrate the database if you haven't:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span>dokku run sample-app rails db:migrate</code></pre></figure>

<h3 class="fw-bold">
    Setting up domain name and HTTPS/SSL cert for your Rails app
</h3>

<p>
    Now that we can visit our app through an IP address with a port number, but how to visit the app by entering our domain name in the browser? To do this, we need to set a Dokku domain for our app:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span>dokku domains:set sample-app www.domain.name <span class="c"># use the domain name you bought here</span>
<span class="nv">$ </span>dokku domains:set sample-app domain.name <span class="c"># use the domain name you bought here</span></code></pre></figure>

<p>
    Then you should be able to visit your Rails app by entering the domain name: http://www.domain.name from the browser! Don't worry if it doesn't work, if you have successfully set the DNS for your app, wait a few minutes or an hour. The DNS record needs a couple of minutes to propagate until people can visit your app by that domain name.
</p>

<p>
    Thanks to Let's Encrypt, setting a SSL cert (HTTPS connection) to your Rails app with Dokku is now super easy. Just use the following scripts and a cert will be issued for your app, and will be automatically updated when it gets near to the date to expire.
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span><span class="nb">sudo </span>dokku plugin:install https://github.com/dokku/dokku-letsencrypt.git
<span class="nv">$ </span>dokku config:set <span class="nt">--global</span> <span class="nv">DOKKU_LETSENCRYPT_EMAIL</span><span class="o">=</span>your-email@www.domain.name
<span class="nv">$ </span>dokku letsencrypt:enable sample-app
<span class="nv">$ </span>dokku letsencrypt:cron-job <span class="nt">--add</span></code></pre></figure>

<p>
    Now you should be able to visit your app with HTTPS connection in the browser: https://www.domain.name!
</p>

<h3 class="fw-bold">
    Redirect from non-www(bare domain) to www
</h3>

<p>
    Usually we want our users and search engines to stay in a single domain, instead of handling this redirect with Rails, we ask Dokku to set that for us. Dokku will set a redirect within it's Nginx config file, so we don't have to write that ourselves:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your cloud server
<span class="nv">$ </span>dokku plugin:install https://github.com/dokku/dokku-redirect.git
<span class="nv">$ </span>dokku redirect:set sample-app domain.name www.domain.name</code></pre></figure>

<p>
    By default a 301 redirect will be the response.
</p>

<h3 class="fw-bold">
Further deploys
</h3>

<p>
    Congratulations if you've made your way here! Now we're 100% done with deploying your Rails app! If you've made any fantastic changes to your app and want your users to enjoy them right then, only a single line of command is required to push your new release to the Internet:
</p>

<figure class="highlight"><pre><code class="language-bash" data-lang="bash">bash - on your <span class="nb">local </span>machine
<span class="nv">$ </span>git push dokku main</code></pre></figure>

<p>
    And you're all set!
</p>

<h3 class="fw-bold">
    Cheers!
</h3>]]></content><author><name></name></author><summary type="html"><![CDATA[Deploy a Rails app to Ubuntu with Dokku in 2023]]></summary></entry></feed>